From 64096309fc2bde9a7e855695d6e70d104a7be478 Mon Sep 17 00:00:00 2001 From: Siva Mahadevan Date: Fri, 24 Oct 2025 14:55:18 -0400 Subject: [PATCH 01/18] meson.build: add back pipewire.desktop autostart entry --- .gitlab-ci.yml | 7 ++++++- src/daemon/meson.build | 30 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 448bfa06e..bdeaefac9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,6 +46,7 @@ include: bluez-libs-devel clang dbus-devel + desktop-file-utils doxygen fdk-aac-free-devel file @@ -113,6 +114,7 @@ include: FDO_DISTRIBUTION_VERSION: '22.04' FDO_DISTRIBUTION_PACKAGES: >- debhelper-compat + desktop-file-utils findutils git libapparmor-dev @@ -148,12 +150,14 @@ include: .debian: variables: # Update this tag when you want to trigger a rebuild - BASE_TAG: '2025-08-10.0' + BASE_TAG: '2025-08-10.1' FDO_DISTRIBUTION_VERSION: 'trixie' FDO_DISTRIBUTION_PACKAGES: >- build-essential + desktop-file-utils dpkg-dev findutils + gettext git meson @@ -182,6 +186,7 @@ include: gcc g++ dbus-dev + desktop-file-utils doxygen elogind-dev eudev-dev diff --git a/src/daemon/meson.build b/src/daemon/meson.build index e7e482f73..3e612d1f0 100644 --- a/src/daemon/meson.build +++ b/src/daemon/meson.build @@ -144,21 +144,21 @@ custom_target('pipewire-uninstalled', command: [ln, '-fs', meson.project_build_root() + '/@INPUT@', '@OUTPUT@'], ) -#desktop_file = i18n.merge_file( -# input : 'pipewire.desktop.in', -# output : 'pipewire.desktop', -# po_dir : po_dir, -# type : 'desktop', -# install : true, -# install_dir : pipewire_sysconfdir / 'xdg' / 'autostart' -#) -# -#desktop_utils = find_program('desktop-file-validate', required: false) -#if desktop_utils.found() -# test('Validate desktop file', desktop_utils, -# args: [ desktop_file ], -# ) -#endif +desktop_file = i18n.merge_file( + input : 'pipewire.desktop.in', + output : 'pipewire.desktop', + po_dir : po_dir, + type : 'desktop', + install : true, + install_dir : pipewire_sysconfdir / 'xdg' / 'autostart' +) + +desktop_utils = find_program('desktop-file-validate', required: false) +if desktop_utils.found() + test('Validate desktop file', desktop_utils, + args: [ desktop_file ], + ) +endif subdir('filter-chain') subdir('systemd') From 614186a59076e8c857eaf7d94702b20e5f5030e9 Mon Sep 17 00:00:00 2001 From: Jonas Holmberg Date: Wed, 22 Oct 2025 13:50:24 +0200 Subject: [PATCH 02/18] module-echo-cancel: Sync capture and sink buffers Call process() when capture and sink ringbuffers contain data from the same graph cycle and only process the latest block from them to avoid adding latency that can accumulate if one of the streams gets more than one buffer before the other gets its first buffer when starting up. --- src/modules/module-echo-cancel.c | 92 +++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 18 deletions(-) diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c index 997506e6f..037509c35 100644 --- a/src/modules/module-echo-cancel.c +++ b/src/modules/module-echo-cancel.c @@ -229,8 +229,10 @@ struct impl { struct spa_audio_aec *aec; uint32_t aec_blocksize; - unsigned int capture_ready:1; - unsigned int sink_ready:1; + struct spa_io_position *capture_position; + struct spa_io_position *sink_position; + uint32_t capture_cycle; + uint32_t sink_cycle; unsigned int do_disconnect:1; @@ -309,11 +311,17 @@ static void process(struct impl *impl) struct spa_data *dd; uint32_t i, size; uint32_t rindex, pindex, oindex, pdindex, avail; + int32_t pavail, pdavail; size = impl->aec_blocksize; - /* First read a block from the playback and capture ring buffers */ - spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + /* First read a block from the capture ring buffer */ + avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + while (avail > size) { + /* drop samples from previous graph cycles */ + spa_ringbuffer_read_update(&impl->rec_ring, rindex + size); + avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + } for (i = 0; i < impl->rec_info.channels; i++) { /* captured samples, with echo from sink */ @@ -331,19 +339,30 @@ static void process(struct impl *impl) out[i] = &out_buf[i][0]; } - spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); - spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); + pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); if (impl->playback != NULL && (pout = pw_stream_dequeue_buffer(impl->playback)) == NULL) { pw_log_debug("out of playback buffers: %m"); /* playback stream may not yet be in streaming state, drop play * data to avoid introducing additional playback latency */ - spa_ringbuffer_read_update(&impl->play_ring, pindex + size); - spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size); + spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail); + spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail); goto done; } + while (pavail > size) { + /* drop samples from previous graph cycles */ + spa_ringbuffer_read_update(&impl->play_ring, pindex + size); + pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); + } + while (pdavail > size) { + /* drop samples from previous graph cycles */ + spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size); + pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + } + for (i = 0; i < impl->play_info.channels; i++) { /* echo from sink */ play[i] = &play_buf[i][0]; @@ -454,8 +473,8 @@ static void process(struct impl *impl) } done: - impl->sink_ready = false; - impl->capture_ready = false; + impl->capture_cycle = 0; + impl->sink_cycle = 0; } static void reset_buffers(struct impl *impl) @@ -479,8 +498,8 @@ static void reset_buffers(struct impl *impl) spa_ringbuffer_get_read_index(&impl->play_ring, &index); spa_ringbuffer_read_update(&impl->play_ring, index + (sizeof(float) * (impl->buffer_delay))); - impl->sink_ready = false; - impl->capture_ready = false; + impl->capture_cycle = 0; + impl->sink_cycle = 0; } static void capture_destroy(void *d) @@ -546,8 +565,11 @@ static void capture_process(void *data) spa_ringbuffer_write_update(&impl->rec_ring, index + size); if (avail + size >= impl->aec_blocksize) { - impl->capture_ready = true; - if (impl->sink_ready) + if (impl->capture_position) + impl->capture_cycle = impl->capture_position->clock.cycle; + else + pw_log_warn("no capture position"); + if (impl->capture_cycle == impl->sink_cycle) process(impl); } @@ -740,12 +762,26 @@ static void input_param_changed(void *data, uint32_t id, const struct spa_pod* p } } +static void capture_io_changed(void *data, uint32_t id, void *area, uint32_t size) +{ + struct impl *impl = data; + + switch (id) { + case SPA_IO_Position: + impl->capture_position = area; + break; + default: + break; + } +} + static const struct pw_stream_events capture_events = { PW_VERSION_STREAM_EVENTS, .destroy = capture_destroy, .state_changed = capture_state_changed, .process = capture_process, - .param_changed = input_param_changed + .param_changed = input_param_changed, + .io_changed = capture_io_changed }; static void source_destroy(void *d) @@ -930,10 +966,15 @@ static void sink_process(void *data) SPA_PTROFF(d->data, offs, void), size); } spa_ringbuffer_write_update(&impl->play_ring, index + size); + spa_ringbuffer_get_write_index(&impl->play_delayed_ring, &index); + spa_ringbuffer_write_update(&impl->play_delayed_ring, index + size); if (avail + size >= impl->aec_blocksize) { - impl->sink_ready = true; - if (impl->capture_ready) + if (impl->sink_position) + impl->sink_cycle = impl->sink_position->clock.cycle; + else + pw_log_warn("no sink position"); + if (impl->capture_cycle == impl->sink_cycle) process(impl); } @@ -955,12 +996,27 @@ static const struct pw_stream_events playback_events = { .state_changed = playback_state_changed, .param_changed = output_param_changed }; + +static void sink_io_changed(void *data, uint32_t id, void *area, uint32_t size) +{ + struct impl *impl = data; + + switch (id) { + case SPA_IO_Position: + impl->sink_position = area; + break; + default: + break; + } +} + static const struct pw_stream_events sink_events = { PW_VERSION_STREAM_EVENTS, .destroy = sink_destroy, .process = sink_process, .state_changed = sink_state_changed, - .param_changed = output_param_changed + .param_changed = output_param_changed, + .io_changed = sink_io_changed }; #define MAX_PARAMS 512u From 0276bb5b063d8b34a55b1292c4a01c1f7b5c9cc4 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 27 Oct 2025 11:43:04 +0100 Subject: [PATCH 03/18] modules: ringbuffer avail is signed --- src/modules/module-echo-cancel.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c index 037509c35..a4cdd2435 100644 --- a/src/modules/module-echo-cancel.c +++ b/src/modules/module-echo-cancel.c @@ -309,15 +309,15 @@ static void process(struct impl *impl) const float *play_delayed[impl->play_info.channels]; float *out[impl->out_info.channels]; struct spa_data *dd; - uint32_t i, size; - uint32_t rindex, pindex, oindex, pdindex, avail; - int32_t pavail, pdavail; + uint32_t i; + uint32_t rindex, pindex, oindex, pdindex, size; + int32_t avail, pavail, pdavail; size = impl->aec_blocksize; /* First read a block from the capture ring buffer */ avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); - while (avail > size) { + while (avail > (int32_t)size) { /* drop samples from previous graph cycles */ spa_ringbuffer_read_update(&impl->rec_ring, rindex + size); avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); @@ -352,12 +352,12 @@ static void process(struct impl *impl) goto done; } - while (pavail > size) { + while (pavail > (int32_t)size) { /* drop samples from previous graph cycles */ spa_ringbuffer_read_update(&impl->play_ring, pindex + size); pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); } - while (pdavail > size) { + while (pdavail > (int32_t)size) { /* drop samples from previous graph cycles */ spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size); pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); @@ -450,7 +450,7 @@ static void process(struct impl *impl) * available on the source */ avail = spa_ringbuffer_get_read_index(&impl->out_ring, &oindex); - while (avail >= size) { + while (avail >= (int32_t)size) { if ((cout = pw_stream_dequeue_buffer(impl->source)) != NULL) { for (i = 0; i < impl->out_info.channels; i++) { dd = &cout->buffer->datas[i]; From 94d0d8bc095b001f36f4e27c23d22c7ce4aca8ca Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 27 Oct 2025 13:32:03 +0100 Subject: [PATCH 04/18] spa: add spa_json_init_relax spa_json_init assumes that we start in an object and always requires a key/value pair. If the last part is a key, it returns and error and does not want to return the key value. This causes problems when parsing AUX0,AUX1,AUX2 or any relaxed array withand odd number of elements. Make a new spa_json_init_relax that takes the type of the container we're assuming we're in and set the state of the parser to array when we are parsing a relaxed array. Fixes #4944 --- spa/include/spa/utils/json-core.h | 9 +++++++++ spa/include/spa/utils/json.h | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/spa/include/spa/utils/json-core.h b/spa/include/spa/utils/json-core.h index 800763571..5616bffe1 100644 --- a/spa/include/spa/utils/json-core.h +++ b/spa/include/spa/utils/json-core.h @@ -54,6 +54,15 @@ SPA_API_JSON void spa_json_init(struct spa_json * iter, const char *data, size_t { *iter = SPA_JSON_INIT(data, size); } + +#define SPA_JSON_INIT_RELAX(type,data,size) \ + ((struct spa_json) { (data), (data)+(size), NULL, (uint32_t)((type) == '[' ? 0x10 : 0x0), 0 }) + +SPA_API_JSON void spa_json_init_relax(struct spa_json * iter, char type, const char *data, size_t size) +{ + *iter = SPA_JSON_INIT_RELAX(type, data, size); +} + #define SPA_JSON_ENTER(iter) ((struct spa_json) { (iter)->cur, (iter)->end, (iter), (iter)->state & 0xff0, 0 }) SPA_API_JSON void spa_json_enter(struct spa_json * iter, struct spa_json * sub) diff --git a/spa/include/spa/utils/json.h b/spa/include/spa/utils/json.h index c8030345e..212637dab 100644 --- a/spa/include/spa/utils/json.h +++ b/spa/include/spa/utils/json.h @@ -105,7 +105,7 @@ SPA_API_JSON_UTILS int spa_json_begin_container(struct spa_json * iter, spa_json_init(iter, data, size); res = spa_json_enter_container(iter, iter, type); if (res == -EPROTO && relax) - spa_json_init(iter, data, size); + spa_json_init_relax(iter, type, data, size); else if (res <= 0) return res; return 1; From 23c449af5d6afc8dde0685d61e6ed11d89b09000 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Mon, 27 Oct 2025 14:20:25 +0100 Subject: [PATCH 05/18] test: add test for an array with odd number of items We have to use the relax version to get the expected container type correct. --- test/test-spa-json.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test-spa-json.c b/test/test-spa-json.c index 66ef2eeac..0c3c46f59 100644 --- a/test/test-spa-json.c +++ b/test/test-spa-json.c @@ -609,7 +609,7 @@ static void test_array(const char *str, const char * const vals[]) spa_json_init(&it[0], str, strlen(str)); if (spa_json_enter_array(&it[0], &it[1]) <= 0) - spa_json_init(&it[1], str, strlen(str)); + spa_json_init_relax(&it[1], '[', str, strlen(str)); for (i = 0; vals[i]; i++) { pwtest_int_gt(spa_json_get_string(&it[1], val, sizeof(val)), 0); pwtest_str_eq(val, vals[i]); @@ -624,6 +624,7 @@ PWTEST(json_array) test_array("[FL FR]", (const char *[]){ "FL", "FR", NULL }); test_array("FL FR", (const char *[]){ "FL", "FR", NULL }); test_array("[ FL FR ]", (const char *[]){ "FL", "FR", NULL }); + test_array("FL FR FC", (const char *[]){ "FL", "FR", "FC", NULL }); return PWTEST_PASS; } From 76a31a47c2cf063abc70c29f42e60fb0fa03dee5 Mon Sep 17 00:00:00 2001 From: Jonas Holmberg Date: Mon, 27 Oct 2025 14:37:03 +0100 Subject: [PATCH 06/18] module-echo-cancel: Avoid discontinuity Keep the samples in the ringbuffer that are needed the next cycle to avoid discontinuity when the aec blocksize is not equal to or divisible by quantum. --- src/modules/module-echo-cancel.c | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/modules/module-echo-cancel.c b/src/modules/module-echo-cancel.c index a4cdd2435..98efa35c5 100644 --- a/src/modules/module-echo-cancel.c +++ b/src/modules/module-echo-cancel.c @@ -317,10 +317,15 @@ static void process(struct impl *impl) /* First read a block from the capture ring buffer */ avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); - while (avail > (int32_t)size) { - /* drop samples from previous graph cycles */ + while (avail >= (int32_t)size * 2) { + /* drop samples that are not needed this or next cycle. Note + * that samples are kept in the ringbuffer until next cycle if + * size is not equal to or divisible by quantum, to avoid + * discontinuity */ + pw_log_debug("avail %d", avail); spa_ringbuffer_read_update(&impl->rec_ring, rindex + size); avail = spa_ringbuffer_get_read_index(&impl->rec_ring, &rindex); + pw_log_debug("new avail %d, size %u", avail, size); } for (i = 0; i < impl->rec_info.channels; i++) { @@ -352,15 +357,19 @@ static void process(struct impl *impl) goto done; } - while (pavail > (int32_t)size) { - /* drop samples from previous graph cycles */ - spa_ringbuffer_read_update(&impl->play_ring, pindex + size); + if (pavail > avail) { + /* drop too old samples from previous graph cycles */ + pw_log_debug("pavail %d, dropping %d", pavail, pavail - avail); + spa_ringbuffer_read_update(&impl->play_ring, pindex + pavail - avail); pavail = spa_ringbuffer_get_read_index(&impl->play_ring, &pindex); + pw_log_debug("new pavail %d, avail %d", pavail, avail); } - while (pdavail > (int32_t)size) { - /* drop samples from previous graph cycles */ - spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + size); + if (pdavail > avail) { + /* drop too old samples from previous graph cycles */ + pw_log_debug("pdavail %d, dropping %d", pdavail, pdavail - avail); + spa_ringbuffer_read_update(&impl->play_delayed_ring, pdindex + pdavail - avail); pdavail = spa_ringbuffer_get_read_index(&impl->play_delayed_ring, &pdindex); + pw_log_debug("new pdavail %d, avail %d", pdavail, avail); } for (i = 0; i < impl->play_info.channels; i++) { From c1e737bbe45bab94c69dbd4fe206c2acd40e7bc0 Mon Sep 17 00:00:00 2001 From: Rui Matos Date: Tue, 26 Aug 2025 10:40:13 +0200 Subject: [PATCH 07/18] module-rtp: Attempt to reconnect the ptp management socket This should gracefully recover the cases where the other end of the socket isn't ready yet when we start or terminates and gets restarted. --- src/modules/module-rtp-sap.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c index 22fe89b68..6ab0c8477 100644 --- a/src/modules/module-rtp-sap.c +++ b/src/modules/module-rtp-sap.c @@ -548,8 +548,13 @@ error: static bool update_ts_refclk(struct impl *impl) { - if (!impl->ptp_mgmt_socket || impl->ptp_fd < 0) + if (!impl->ptp_mgmt_socket) return false; + if (impl->ptp_fd < 0) { + impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket); + if (impl->ptp_fd < 0) + return false; + } // Read if something is left in the socket int avail; @@ -581,6 +586,12 @@ static bool update_ts_refclk(struct impl *impl) if (write(impl->ptp_fd, &req, sizeof(req)) == -1) { pw_log_warn("Failed to send PTP management request: %m"); + if (errno != ENOTCONN) + return false; + close(impl->ptp_fd); + impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket); + if (impl->ptp_fd > -1) + pw_log_info("Reopened PTP management socket"); return false; } From b57bd00be01a66dfb08673491c4412370549a169 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Thu, 23 Oct 2025 16:06:56 +0200 Subject: [PATCH 08/18] module-rtp-sap: Improve names for clearer code --- src/modules/module-rtp-sap.c | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c index 6ab0c8477..1818df13e 100644 --- a/src/modules/module-rtp-sap.c +++ b/src/modules/module-rtp-sap.c @@ -265,7 +265,7 @@ struct impl { struct pw_registry *registry; struct spa_hook registry_listener; - struct pw_timer timer; + struct pw_timer sap_send_timer; char *ifname; uint32_t ttl; @@ -288,7 +288,7 @@ struct impl { char *extra_attrs_preamble; char *extra_attrs_end; - char *ptp_mgmt_socket; + char *ptp_mgmt_socket_path; int ptp_fd; uint32_t ptp_seq; uint8_t clock_id[8]; @@ -383,7 +383,7 @@ static bool is_multicast(struct sockaddr *sa, socklen_t salen) return false; } -static int make_unix_socket(const char *path) { +static int make_unix_ptp_mgmt_socket(const char *path) { struct sockaddr_un addr; spa_autoclose int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC, 0); @@ -419,7 +419,7 @@ static int make_send_socket( af = src->ss_family; if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) { - pw_log_error("socket failed: %m"); + pw_log_error("socket() failed: %m"); return -errno; } if (bind(fd, (struct sockaddr*)src, src_len) < 0) { @@ -451,6 +451,9 @@ static int make_send_socket( pw_log_warn("setsockopt(IPV6_MULTICAST_HOPS) failed: %m"); } } + + pw_log_info("sender socket up and running"); + return fd; error: close(fd); @@ -468,13 +471,13 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, af = sa->ss_family; if ((fd = socket(af, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0)) < 0) { - pw_log_error("socket failed: %m"); + pw_log_error("socket() failed: %m"); return -errno; } val = 1; if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0) { res = -errno; - pw_log_error("setsockopt failed: %m"); + pw_log_error("setsockopt() failed: %m"); goto error; } spa_zero(req); @@ -540,6 +543,9 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, goto error; } } + + pw_log_info("receiver socket up and running"); + return fd; error: close(fd); @@ -548,10 +554,10 @@ error: static bool update_ts_refclk(struct impl *impl) { - if (!impl->ptp_mgmt_socket) + if (!impl->ptp_mgmt_socket_path) return false; if (impl->ptp_fd < 0) { - impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket); + impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path); if (impl->ptp_fd < 0) return false; } @@ -589,7 +595,7 @@ static bool update_ts_refclk(struct impl *impl) if (errno != ENOTCONN) return false; close(impl->ptp_fd); - impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket); + impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path); if (impl->ptp_fd > -1) pw_log_info("Reopened PTP management socket"); return false; @@ -933,7 +939,7 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye) return res; } -static void on_timer_event(void *data) +static void on_sap_send_timer_event(void *data) { struct impl *impl = data; struct session *sess, *tmp; @@ -967,9 +973,9 @@ static void on_timer_event(void *data) } } - pw_timer_queue_add(impl->timer_queue, &impl->timer, - &impl->timer.timeout, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC, - on_timer_event, impl); + pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer, + &impl->sap_send_timer.timeout, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC, + on_sap_send_timer_event, impl); } static struct session *session_find(struct impl *impl, const struct sdp_info *info) @@ -1665,11 +1671,11 @@ static int start_sap(struct impl *impl) int fd = -1, res; char addr[128] = "invalid"; - pw_log_info("starting SAP timer"); - if ((res = pw_timer_queue_add(impl->timer_queue, &impl->timer, + pw_log_info("starting SAP send timer"); + if ((res = pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer, NULL, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC, - on_timer_event, impl)) < 0) { - pw_log_error("can't add timer: %s", spa_strerror(res)); + on_sap_send_timer_event, impl)) < 0) { + pw_log_error("can't add SAP send timer: %s", spa_strerror(res)); goto error; } if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname)) < 0) @@ -1818,7 +1824,7 @@ static void impl_destroy(struct impl *impl) if (impl->core && impl->do_disconnect) pw_core_disconnect(impl->core); - pw_timer_queue_cancel(&impl->timer); + pw_timer_queue_cancel(&impl->sap_send_timer); if (impl->sap_source) pw_loop_destroy_source(impl->loop, impl->sap_source); @@ -1832,7 +1838,7 @@ static void impl_destroy(struct impl *impl) free(impl->extra_attrs_preamble); free(impl->extra_attrs_end); - free(impl->ptp_mgmt_socket); + free(impl->ptp_mgmt_socket_path); free(impl->ifname); free(impl); } @@ -1904,11 +1910,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->ifname = str ? strdup(str) : NULL; str = pw_properties_get(props, "ptp.management-socket"); - impl->ptp_mgmt_socket = str ? strdup(str) : NULL; + impl->ptp_mgmt_socket_path = str ? strdup(str) : NULL; // TODO: support UDP management access as well - if (impl->ptp_mgmt_socket) - impl->ptp_fd = make_unix_socket(impl->ptp_mgmt_socket); + if (impl->ptp_mgmt_socket_path) + impl->ptp_fd = make_unix_ptp_mgmt_socket(impl->ptp_mgmt_socket_path); if ((str = pw_properties_get(props, "sap.ip")) == NULL) str = DEFAULT_SAP_IP; From 80e7302a05cc7d4da3c9a431d64c5eba538483ae Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Thu, 23 Oct 2025 16:39:51 +0200 Subject: [PATCH 09/18] module-rtp-sap: Add retry code for when start_sap() fails due to ENODEV --- src/modules/module-rtp-sap.c | 69 +++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c index 1818df13e..b9404b667 100644 --- a/src/modules/module-rtp-sap.c +++ b/src/modules/module-rtp-sap.c @@ -267,6 +267,10 @@ struct impl { struct pw_timer sap_send_timer; + /* This timer is used when the first start_sap() call fails because + * of an ENODEV error (see the start_sap() code for details) */ + struct pw_timer start_sap_retry_timer; + char *ifname; uint32_t ttl; bool mcast_loop; @@ -322,6 +326,7 @@ static const struct format_info *find_audio_format_info(const char *mime) return NULL; } +static int start_sap(struct impl *impl); static int send_sap(struct impl *impl, struct session *sess, bool bye); @@ -978,6 +983,13 @@ static void on_sap_send_timer_event(void *data) on_sap_send_timer_event, impl); } +static void on_start_sap_retry_timer_event(void *data) +{ + struct impl *impl = data; + pw_log_debug("trying again to start SAP send after previous attempt failed with ENODEV"); + start_sap(impl); +} + static struct session *session_find(struct impl *impl, const struct sdp_info *info) { struct session *sess; @@ -1668,18 +1680,62 @@ on_sap_io(void *data, int fd, uint32_t mask) static int start_sap(struct impl *impl) { - int fd = -1, res; + int fd = -1, res = 0; char addr[128] = "invalid"; pw_log_info("starting SAP send timer"); + /* start_sap() might be called more than once. See the make_recv_socket() + * call below for why that can happen. In such a case, the timer was + * started already. The easiest way of handling it is to just cancel it. + * Such cases are not expected to occur often, so canceling and then + * adding the timer again is acceptable. */ + pw_timer_queue_cancel(&impl->sap_send_timer); if ((res = pw_timer_queue_add(impl->timer_queue, &impl->sap_send_timer, NULL, SAP_INTERVAL_SEC * SPA_NSEC_PER_SEC, on_sap_send_timer_event, impl)) < 0) { pw_log_error("can't add SAP send timer: %s", spa_strerror(res)); goto error; } - if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname)) < 0) - return fd; + if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname)) < 0) { + /* If make_recv_socket() tries to create a socket and join to a multicast + * group while the network interfaces are not ready yet to do so + * (usually because a network manager component is still setting up + * those network interfaces), ENODEV will be returned. This is essentially + * a race condition. There is no discernible way to be notified when the + * network interfaces are ready for that operation, so the next best + * approach is to essentially do a form of polling by retrying the + * start_sap() call after some time. The start_sap_retry_timer exists + * precisely for that purpose. This means that ENODEV is not treated as + * an error, but instead, it triggers the creation of that timer. */ + if (fd == -ENODEV) { + pw_log_warn("failed to create receiver socket because network device " + "is not ready and present yet; will try again"); + + pw_timer_queue_cancel(&impl->start_sap_retry_timer); + /* Use a 1-second retry interval. The network interfaces + * are likely to be up and running then. */ + pw_timer_queue_add(impl->timer_queue, &impl->start_sap_retry_timer, + NULL, 1 * SPA_NSEC_PER_SEC, + on_start_sap_retry_timer_event, impl); + + /* It is important to return 0 in this case. Otherwise, the nonzero return + * value will later be propagated through the core as an error. */ + res = 0; + goto finish; + } else { + pw_log_error("failed to create socket: %s", spa_strerror(-fd)); + /* If ENODEV was returned earlier, and the start_sap_retry_timer + * was consequently created, but then a non-ENODEV error occurred, + * the timer must be stopped and removed. */ + pw_timer_queue_cancel(&impl->start_sap_retry_timer); + res = fd; + goto error; + } + } + + /* Cleanup the timer in case ENODEV occurred earlier, and this time, + * the socket creation succeeded. */ + pw_timer_queue_cancel(&impl->start_sap_retry_timer); pw_net_get_ip(&impl->sap_addr, addr, sizeof(addr), NULL, NULL); pw_log_info("starting SAP listener on %s", addr); @@ -1690,11 +1746,13 @@ static int start_sap(struct impl *impl) goto error; } - return 0; +finish: + return res; + error: if (fd > 0) close(fd); - return res; + goto finish; } static void node_event_info(void *data, const struct pw_node_info *info) @@ -1825,6 +1883,7 @@ static void impl_destroy(struct impl *impl) pw_core_disconnect(impl->core); pw_timer_queue_cancel(&impl->sap_send_timer); + pw_timer_queue_cancel(&impl->start_sap_retry_timer); if (impl->sap_source) pw_loop_destroy_source(impl->loop, impl->sap_source); From f1ffd5e5e83c91cf3176c9940bef6642b4245407 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Sun, 19 Oct 2025 16:19:16 +0200 Subject: [PATCH 10/18] module-rtp-source: Read cleanup.sec property from stream properties This allows for setting the cleanup.sec value in the create-stream block in the module-rtp-sap configuration. --- src/modules/module-rtp-source.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 63f1f399d..26da7c45e 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -797,7 +797,7 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) * till we make it (or get timed out) */ pw_properties_set(stream_props, "rtp.receiving", "true"); - impl->cleanup_interval = pw_properties_get_uint32(props, + impl->cleanup_interval = pw_properties_get_uint32(stream_props, "cleanup.sec", DEFAULT_CLEANUP_SEC); impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); From 5d21e12658f0a67a87f3d9037264cf80919f89f2 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Sun, 19 Oct 2025 19:31:44 +0200 Subject: [PATCH 11/18] module-rtp-source: Use make_socket() error value instead of errno make_socket() already returns the negative errno. --- src/modules/module-rtp-source.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 26da7c45e..3954d3137 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -433,7 +433,7 @@ static void stream_open_connection(void *data, int *result) * stream_start() call after some time. The stream_start_retry_timer exists * precisely for that purpose. This means that ENODEV is not treated as * an error, but instead, it triggers the creation of that timer. */ - if (errno == ENODEV) { + if (fd == -ENODEV) { pw_log_warn("failed to create socket because network device is not ready " "and present yet; will try again"); @@ -449,12 +449,12 @@ static void stream_open_connection(void *data, int *result) res = 0; goto finish; } else { - pw_log_error("failed to create socket: %m"); + pw_log_error("failed to create socket: %s", spa_strerror(fd)); /* If ENODEV was returned earlier, and the stream_start_retry_timer * was consequently created, but then a non-ENODEV error occurred, * the timer must be stopped and removed. */ pw_timer_queue_cancel(&impl->stream_start_retry_timer); - res = -errno; + res = fd; goto finish; } } From 3e0f4daf600de2a04b69c0ba5ca0c26e9e5a04f6 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Thu, 23 Oct 2025 17:53:54 +0200 Subject: [PATCH 12/18] module-rtp-sap: implement IGMP recovery for multicast subscription loss Add IGMP recovery mechanism that monitors SAP packet reception and triggers multicast group refresh when no packets are received if a deadline is reached. The deadline is set to half of the cleanup interval, with a minimum of 1 second. When the deadline is reached, the mechanism performs IGMP leave/rejoin operations to refresh multicast group membership. This ensures SAP announcements continue to be received when network conditions cause IGMP membership to expire or become stale due to router timeouts or network issues. --- src/modules/module-rtp-sap.c | 133 ++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/src/modules/module-rtp-sap.c b/src/modules/module-rtp-sap.c index b9404b667..27df24816 100644 --- a/src/modules/module-rtp-sap.c +++ b/src/modules/module-rtp-sap.c @@ -248,6 +248,16 @@ struct node { struct session *session; }; +struct igmp_recovery { + struct pw_timer timer; + int socket_fd; + struct sockaddr_storage mcast_addr; + socklen_t mcast_len; + uint32_t if_index; + bool is_ipv6; + uint32_t deadline; +}; + struct impl { struct pw_properties *props; @@ -285,6 +295,10 @@ struct impl { struct spa_source *sap_source; uint32_t cleanup_interval; + /* IGMP recovery (triggers when no SAP packets are + * received after the recovery deadline is reached) */ + struct igmp_recovery igmp_recovery; + uint32_t max_sessions; uint32_t n_sessions; struct spa_list sessions; @@ -466,7 +480,7 @@ error: } static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, - char *ifname) + char *ifname, struct igmp_recovery *igmp_recovery) { int af, fd, val, res; struct ifreq req; @@ -536,6 +550,16 @@ static int make_recv_socket(struct sockaddr_storage *sa, socklen_t salen, goto error; } + /* Store multicast info for recovery */ + igmp_recovery->socket_fd = fd; + igmp_recovery->mcast_addr = ba; + igmp_recovery->mcast_len = salen; + igmp_recovery->if_index = req.ifr_ifindex; + igmp_recovery->is_ipv6 = (af == AF_INET6); + pw_log_debug("stored %s multicast info: socket_fd=%d, " + "if_index=%d", igmp_recovery->is_ipv6 ? + "IPv6" : "IPv4", fd, req.ifr_ifindex); + if (bind(fd, (struct sockaddr*)&ba, salen) < 0) { res = -errno; pw_log_error("bind() failed: %m"); @@ -944,6 +968,97 @@ static int send_sap(struct impl *impl, struct session *sess, bool bye) return res; } +static void on_igmp_recovery_timer_event(void *data) +{ + struct impl *impl = data; + char addr[128]; + int res = 0; + + /* Only attempt recovery if we have a valid socket and multicast address */ + if (impl->igmp_recovery.socket_fd < 0) { + pw_log_debug("no socket, skipping IGMP recovery"); + goto finish; + } + + pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL); + pw_log_info("IGMP recovery triggered for %s", addr); + + /* Force IGMP membership refresh by leaving the group first, then rejoin */ + if (impl->igmp_recovery.is_ipv6) { + struct ipv6_mreq mr6; + memset(&mr6, 0, sizeof(mr6)); + mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr; + mr6.ipv6mr_interface = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, + &mr6, sizeof(mr6)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv6 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv6 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv6 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, + &mr6, sizeof(mr6)); + if (res < 0) { + pw_log_warn("failed to re-join IPv6 multicast group: %m"); + } else { + pw_log_info("re-joined IPv6 multicast group successfully"); + } + } else { + struct ip_mreqn mr4; + memset(&mr4, 0, sizeof(mr4)); + mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr; + mr4.imr_ifindex = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv4 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv4 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv4 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (res < 0) { + pw_log_warn("failed to re-join IPv4 multicast group: %m"); + } else { + pw_log_info("re-joined IPv4 multicast group successfully"); + } + } + +finish: + /* If rejoining failed, try again in 1 second. This can happen + * for example when the network interface went down, and is not + * yet up and running again, and ENODEV is returned as a result. + * Otherwise, continue with the usual deadline. */ + pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + &impl->igmp_recovery.timer.timeout, + ((res == 0) ? impl->igmp_recovery.deadline : 1) * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl); +} + +static void rearm_igmp_recovery_timer(struct impl *impl) +{ + pw_timer_queue_cancel(&impl->igmp_recovery.timer); + pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + NULL, impl->igmp_recovery.deadline * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl); +} + static void on_sap_send_timer_event(void *data) { struct impl *impl = data; @@ -1675,6 +1790,8 @@ on_sap_io(void *data, int fd, uint32_t mask) buffer[len] = 0; if ((res = parse_sap(impl, buffer, len)) < 0) pw_log_warn("error parsing SAP: %s", spa_strerror(res)); + + rearm_igmp_recovery_timer(impl); } } @@ -1696,7 +1813,8 @@ static int start_sap(struct impl *impl) pw_log_error("can't add SAP send timer: %s", spa_strerror(res)); goto error; } - if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname)) < 0) { + if ((fd = make_recv_socket(&impl->sap_addr, impl->sap_len, impl->ifname, + &(impl->igmp_recovery))) < 0) { /* If make_recv_socket() tries to create a socket and join to a multicast * group while the network interfaces are not ready yet to do so * (usually because a network manager component is still setting up @@ -1746,6 +1864,8 @@ static int start_sap(struct impl *impl) goto error; } + rearm_igmp_recovery_timer(impl); + finish: return res; @@ -1884,6 +2004,7 @@ static void impl_destroy(struct impl *impl) pw_timer_queue_cancel(&impl->sap_send_timer); pw_timer_queue_cancel(&impl->start_sap_retry_timer); + pw_timer_queue_cancel(&impl->igmp_recovery.timer); if (impl->sap_source) pw_loop_destroy_source(impl->loop, impl->sap_source); @@ -1950,6 +2071,9 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->ptp_fd = -1; spa_list_init(&impl->sessions); + impl->igmp_recovery.socket_fd = -1; + impl->igmp_recovery.if_index = -1; + if (args == NULL) args = ""; @@ -1985,6 +2109,11 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->cleanup_interval = pw_properties_get_uint32(impl->props, "sap.cleanup.sec", DEFAULT_CLEANUP_SEC); + /* We will use half of the cleanup interval for IGMP deadline, minimum 1 second */ + impl->igmp_recovery.deadline = SPA_MAX(impl->cleanup_interval / 2, 1u); + pw_log_info("using IGMP deadline of %" PRIu32 " second(s)", + impl->igmp_recovery.deadline); + impl->ttl = pw_properties_get_uint32(props, "net.ttl", DEFAULT_TTL); impl->mcast_loop = pw_properties_get_bool(props, "net.loop", DEFAULT_LOOP); impl->max_sessions = pw_properties_get_uint32(props, "sap.max-sessions", DEFAULT_MAX_SESSIONS); From 955c9ae837dfdeceb4dd3b2261a32622e12807b4 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Thu, 23 Oct 2025 20:08:22 +0200 Subject: [PATCH 13/18] module-rtp: Get the current stream time in a reusable manner That way, redundant pw_stream_get_nsec() and clock_gettime() calls can be avoided. --- src/modules/module-rtp-session.c | 7 +++++-- src/modules/module-rtp-source.c | 23 +++++++++++++---------- src/modules/module-rtp/audio.c | 5 +++-- src/modules/module-rtp/midi.c | 3 ++- src/modules/module-rtp/opus.c | 3 ++- src/modules/module-rtp/stream.c | 14 +++++++++++--- src/modules/module-rtp/stream.h | 5 ++++- 7 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/modules/module-rtp-session.c b/src/modules/module-rtp-session.c index 2b69a0be7..cd7ca7f4e 100644 --- a/src/modules/module-rtp-session.c +++ b/src/modules/module-rtp-session.c @@ -1043,8 +1043,11 @@ on_data_io(void *data, int fd, uint32_t mask) if (sess == NULL) goto unknown_ssrc; - if (sess->data_ready && sess->receiving) - rtp_stream_receive_packet(sess->recv, buffer, len); + if (sess->data_ready && sess->receiving) { + uint64_t current_time = rtp_stream_get_nsec(sess->recv); + rtp_stream_receive_packet(sess->recv, buffer, len, + current_time); + } } } return; diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 3954d3137..377cc4109 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -227,13 +227,6 @@ struct impl { bool waiting; }; -static inline uint64_t get_time_ns(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return SPA_TIMESPEC_TO_NSEC(&ts); -} - static int do_start(struct spa_loop *loop, bool async, uint32_t seq, const void *data, size_t size, void *user_data) { @@ -261,6 +254,9 @@ on_rtp_io(void *data, int fd, uint32_t mask) struct impl *impl = data; ssize_t len; int suppressed; + uint64_t current_time; + + current_time = rtp_stream_get_nsec(impl->stream); if (mask & SPA_IO_IN) { if ((len = recv(fd, impl->buffer, impl->buffer_size, 0)) < 0) @@ -270,10 +266,17 @@ on_rtp_io(void *data, int fd, uint32_t mask) goto short_packet; if (SPA_LIKELY(impl->stream)) { - if (rtp_stream_receive_packet(impl->stream, impl->buffer, len) < 0) + if (rtp_stream_receive_packet(impl->stream, impl->buffer, len, + current_time) < 0) goto receive_error; } + /* Update last packet timestamp for IGMP recovery. + * The recovery timer will check this to see if recovery + * is necessary. Do this _before_ invoking do_start() + * in case the stream is waking up from standby. */ + SPA_ATOMIC_STORE(impl->last_packet_time, current_time); + if (SPA_ATOMIC_LOAD(impl->state) != STATE_RECEIVING) { if (!SPA_ATOMIC_CAS(impl->state, STATE_PROBE, STATE_RECEIVING)) { if (SPA_ATOMIC_CAS(impl->state, STATE_IDLE, STATE_RECEIVING)) @@ -284,11 +287,11 @@ on_rtp_io(void *data, int fd, uint32_t mask) return; receive_error: - if ((suppressed = spa_ratelimit_test(&impl->rate_limit, get_time_ns())) >= 0) + if ((suppressed = spa_ratelimit_test(&impl->rate_limit, current_time)) >= 0) pw_log_warn("(%d suppressed) recv() error: %m", suppressed); return; short_packet: - if ((suppressed = spa_ratelimit_test(&impl->rate_limit, get_time_ns())) >= 0) + if ((suppressed = spa_ratelimit_test(&impl->rate_limit, current_time)) >= 0) pw_log_warn("(%d suppressed) short packet of len %zd received", suppressed, len); return; diff --git a/src/modules/module-rtp/audio.c b/src/modules/module-rtp/audio.c index 1563e1917..0af38d649 100644 --- a/src/modules/module-rtp/audio.c +++ b/src/modules/module-rtp/audio.c @@ -233,7 +233,8 @@ static void rtp_audio_process_playback(void *data) pw_stream_queue_buffer(impl->stream, buf); } -static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) +static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time) { struct rtp_header *hdr; ssize_t hlen, plen; @@ -273,7 +274,7 @@ static int rtp_audio_receive(struct impl *impl, uint8_t *buffer, ssize_t len) timestamp = ntohl(hdr->timestamp) - impl->ts_offset; impl->receiving = true; - impl->last_recv_timestamp = pw_stream_get_nsec(impl->stream); + impl->last_recv_timestamp = current_time; plen = len - hlen; samples = plen / stride; diff --git a/src/modules/module-rtp/midi.c b/src/modules/module-rtp/midi.c index 498c6e6a9..5fbdf3b63 100644 --- a/src/modules/module-rtp/midi.c +++ b/src/modules/module-rtp/midi.c @@ -318,7 +318,8 @@ static int rtp_midi_receive_midi(struct impl *impl, uint8_t *packet, uint32_t ti return 0; } -static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len) +static int rtp_midi_receive(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time) { struct rtp_header *hdr; ssize_t hlen; diff --git a/src/modules/module-rtp/opus.c b/src/modules/module-rtp/opus.c index 7eeda7f43..d13a4efaf 100644 --- a/src/modules/module-rtp/opus.c +++ b/src/modules/module-rtp/opus.c @@ -99,7 +99,8 @@ static void rtp_opus_process_playback(void *data) pw_stream_queue_buffer(impl->stream, buf); } -static int rtp_opus_receive(struct impl *impl, uint8_t *buffer, ssize_t len) +static int rtp_opus_receive(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time) { struct rtp_header *hdr; ssize_t hlen, plen; diff --git a/src/modules/module-rtp/stream.c b/src/modules/module-rtp/stream.c index 3834206ec..fa055ce0c 100644 --- a/src/modules/module-rtp/stream.c +++ b/src/modules/module-rtp/stream.c @@ -151,7 +151,8 @@ struct impl { * access below for the reason why. */ uint8_t timer_running; - int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len); + int (*receive_rtp)(struct impl *impl, uint8_t *buffer, ssize_t len, + uint64_t current_time); /* Used for resetting the ring buffer before the stream starts, to prevent * reading from uninitialized memory. This can otherwise happen in direct * timestamp mode when the read index is set to an uninitialized location. @@ -1036,10 +1037,17 @@ int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *di return pw_stream_update_properties(impl->stream, dict); } -int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len) +int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len, + uint64_t current_time) { struct impl *impl = (struct impl*)s; - return impl->receive_rtp(impl, buffer, len); + return impl->receive_rtp(impl, buffer, len, current_time); +} + +uint64_t rtp_stream_get_nsec(struct rtp_stream *s) +{ + struct impl *impl = (struct impl*)s; + return pw_stream_get_nsec(impl->stream); } uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate) diff --git a/src/modules/module-rtp/stream.h b/src/modules/module-rtp/stream.h index ea358f350..095b8395c 100644 --- a/src/modules/module-rtp/stream.h +++ b/src/modules/module-rtp/stream.h @@ -62,7 +62,10 @@ void rtp_stream_destroy(struct rtp_stream *s); int rtp_stream_update_properties(struct rtp_stream *s, const struct spa_dict *dict); -int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len); +int rtp_stream_receive_packet(struct rtp_stream *s, uint8_t *buffer, size_t len, + uint64_t current_time); + +uint64_t rtp_stream_get_nsec(struct rtp_stream *s); uint64_t rtp_stream_get_time(struct rtp_stream *s, uint32_t *rate); From 1096d634682bb2c9988c394bfc3e9d8cb5a13160 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Thu, 23 Oct 2025 20:16:14 +0200 Subject: [PATCH 14/18] module-rtp-source: implement IGMP recovery for multicast subscription loss Add IGMP recovery mechanism that monitors RTP packet reception and triggers multicast group refresh when no packets are received if a deadline is reached. The deadline is configurable via a new stream property "igmp.deadline.sec" (in seconds), with the default value being 30 seconds (and a minimum of 5 seconds). A timer checks regularly if the deadline was reached. That timer's interval is set by the igmp.check.interval.sec property (in seconds), with the default value being 5 seconds (and a minimum of 1 second). When the deadline is reached, the mechanism performs IGMP leave/rejoin operations to refresh multicast group membership. This ensures RTP data continues to be received when network conditions cause IGMP membership to expire or become stale due to router timeouts or network issues. --- src/modules/module-rtp-source.c | 196 +++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/src/modules/module-rtp-source.c b/src/modules/module-rtp-source.c index 377cc4109..5c3d1f05f 100644 --- a/src/modules/module-rtp-source.c +++ b/src/modules/module-rtp-source.c @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -156,6 +157,9 @@ PW_LOG_TOPIC(mod_topic, "mod." NAME); #define PW_LOG_TOPIC_DEFAULT mod_topic +#define DEFAULT_IGMP_CHECK_INTERVAL_SEC 5 +#define DEFAULT_IGMP_DEADLINE_SEC 30 + #define DEFAULT_CLEANUP_SEC 60 #define DEFAULT_SOURCE_IP "224.0.0.56" @@ -180,6 +184,23 @@ static const struct spa_dict_item module_info[] = { { PW_KEY_MODULE_VERSION, PACKAGE_VERSION }, }; +struct igmp_recovery { + struct pw_timer timer; + int socket_fd; + struct sockaddr_storage mcast_addr; + socklen_t mcast_len; + uint32_t if_index; + bool is_ipv6; + /* This is the interval the recovery timer runs at. The timer + * checks at each interval if recovery is required. This value + * is defined by the igmp.check.interval.sec property. */ + uint32_t check_interval; + /* This is the deadline for packets to arrive. If the deadline + * is exceeded, an IGMP recovery is attempted. This value is + * defined by the igmp.deadline.sec property. */ + uint32_t deadline; +}; + struct impl { struct pw_impl_module *module; struct spa_hook module_listener; @@ -201,6 +222,15 @@ struct impl { bool always_process; uint32_t cleanup_interval; + /* IGMP recovery (triggers when no RTP packets are + * received after the recovery deadline is reached) */ + struct igmp_recovery igmp_recovery; + + /* Monotonic timestamp of the last time a packet was + * received. This is accessed with atomic accessors + * to avoid race conditions. */ + uint64_t last_packet_time; + struct pw_timer standby_timer; /* This timer is used when the first stream_start() call fails because * of an ENODEV error (see the stream_start() code for details) */ @@ -297,7 +327,138 @@ short_packet: return; } -static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname) +static int rejoin_igmp_group(struct spa_loop *loop, bool async, uint32_t seq, + const void *data, size_t size, void *user_data) +{ + /* IMPORTANT: This must be run from within the data loop. */ + + int res; + struct impl *impl = user_data; + uint64_t current_time; + + /* Force IGMP membership refresh by leaving the group first, then rejoin */ + if (impl->igmp_recovery.is_ipv6) { + struct ipv6_mreq mr6; + memset(&mr6, 0, sizeof(mr6)); + mr6.ipv6mr_multiaddr = ((struct sockaddr_in6*)&impl->igmp_recovery.mcast_addr)->sin6_addr; + mr6.ipv6mr_interface = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, + &mr6, sizeof(mr6)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv6 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv6 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv6 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, + &mr6, sizeof(mr6)); + if (res < 0) { + pw_log_warn("failed to re-join IPv6 multicast group: %m"); + } else { + pw_log_info("re-joined IPv6 multicast group successfully"); + } + } else { + struct ip_mreqn mr4; + memset(&mr4, 0, sizeof(mr4)); + mr4.imr_multiaddr = ((struct sockaddr_in*)&impl->igmp_recovery.mcast_addr)->sin_addr; + mr4.imr_ifindex = impl->igmp_recovery.if_index; + + /* Leave the group first */ + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (SPA_LIKELY(res == 0)) { + pw_log_info("left IPv4 multicast group"); + } else { + if (errno == EADDRNOTAVAIL) { + pw_log_info("attempted to leave IPv4 multicast group, but " + "membership was already silently dropped"); + } else { + pw_log_warn("failed to leave IPv4 multicast group: %m"); + } + } + + res = setsockopt(impl->igmp_recovery.socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &mr4, sizeof(mr4)); + if (res < 0) { + pw_log_warn("failed to re-join IPv4 multicast group: %m"); + } else { + pw_log_info("re-joined IPv4 multicast group successfully"); + } + } + + current_time = rtp_stream_get_nsec(impl->stream); + SPA_ATOMIC_STORE(impl->last_packet_time, current_time); + + return res; +} + +static void on_igmp_recovery_timer_event(void *data) +{ + int res; + struct impl *impl = data; + char addr[128]; + uint64_t current_time, elapsed_seconds, last_packet_time; + + /* Only attempt recovery if we have a valid socket and multicast address */ + if (SPA_UNLIKELY(impl->igmp_recovery.socket_fd < 0)) { + pw_log_trace("no socket, skipping IGMP recovery"); + goto finish; + } + + /* This check if performed even if standby = false or + * receiving != STATE_RECEIVING , because the very reason + * for these states may be that the receiver socket was + * silently kicked out of the IGMP group (which causes data + * to no longer arrive, thus leading to these states). */ + + current_time = rtp_stream_get_nsec(impl->stream); + last_packet_time = SPA_ATOMIC_LOAD(impl->last_packet_time); + elapsed_seconds = (current_time - last_packet_time) / SPA_NSEC_PER_SEC; + + /* Only trigger recovery if enough time has elapsed since last packet */ + if (elapsed_seconds < impl->igmp_recovery.deadline) { + pw_log_trace("IGMP recovery check: %" PRIu64 " seconds elapsed, " + "need %" PRIu32 " seconds", elapsed_seconds, + impl->igmp_recovery.deadline); + goto finish; + } + + pw_net_get_ip(&impl->igmp_recovery.mcast_addr, addr, sizeof(addr), NULL, NULL); + pw_log_info("starting IGMP recovery for %s", addr); + + /* Run the actual recovery in the data loop, since recovery involves + * rejoining the socket to the IGMP group. By running this in the + * data loop, race conditions due to stray packets causing an on_rtp_io() + * invocation at the same time when the IGMP group rejoining takes place + * is avoided, since on_rtp_io() too runs in the data loop. + * This is a blocking call to make sure the rejoin attempt was fully + * done by the time this callback ends. (rejoin_igmp_group() does not + * do work that takes a long time to finish. )*/ + res = pw_loop_locked(impl->data_loop, rejoin_igmp_group, 1, NULL, 0, impl); + + if (SPA_LIKELY(res == 0)) { + pw_log_info("IGMP recovery for %s finished", addr); + } else { + pw_log_error("error while finishing IGMP recovery for %s: %s", + addr, spa_strerror(res)); + } + +finish: + pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + &impl->igmp_recovery.timer.timeout, + impl->igmp_recovery.check_interval * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl); +} + +static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname, + struct igmp_recovery *igmp_recovery) { int af, fd, val, res; struct ifreq req; @@ -377,6 +538,16 @@ static int make_socket(const struct sockaddr* sa, socklen_t salen, char *ifname) goto error; } + /* Store multicast info for recovery */ + igmp_recovery->socket_fd = fd; + igmp_recovery->mcast_addr = ba; + igmp_recovery->mcast_len = salen; + igmp_recovery->if_index = req.ifr_ifindex; + igmp_recovery->is_ipv6 = (af == AF_INET6); + pw_log_debug("stored %s multicast info: socket_fd=%d, " + "if_index=%d", igmp_recovery->is_ipv6 ? + "IPv6" : "IPv4", fd, req.ifr_ifindex); + if (bind(fd, (struct sockaddr*)&ba, salen) < 0) { res = -errno; pw_log_error("bind() failed: %m"); @@ -425,7 +596,8 @@ static void stream_open_connection(void *data, int *result) pw_log_info("starting RTP listener"); if ((fd = make_socket((const struct sockaddr *)&impl->src_addr, - impl->src_len, impl->ifname)) < 0) { + impl->src_len, impl->ifname, + &(impl->igmp_recovery))) < 0) { /* If make_socket() tries to create a socket and join to a multicast * group while the network interfaces are not ready yet to do so * (usually because a network manager component is still setting up @@ -475,6 +647,13 @@ static void stream_open_connection(void *data, int *result) goto finish; } + if ((res = pw_timer_queue_add(impl->timer_queue, &impl->igmp_recovery.timer, + NULL, impl->igmp_recovery.check_interval * SPA_NSEC_PER_SEC, + on_igmp_recovery_timer_event, impl)) < 0) { + pw_log_error("can't add timer: %s", spa_strerror(res)); + goto finish; + } + finish: if (res != 0) { pw_log_error("failed to start RTP stream: %s", spa_strerror(res)); @@ -498,6 +677,7 @@ static void stream_close_connection(void *data, int *result) pw_log_info("stopping RTP listener"); pw_timer_queue_cancel(&impl->stream_start_retry_timer); + pw_timer_queue_cancel(&impl->igmp_recovery.timer); pw_loop_destroy_source(impl->data_loop, impl->source); impl->source = NULL; @@ -636,6 +816,7 @@ static void impl_destroy(struct impl *impl) pw_timer_queue_cancel(&impl->standby_timer); pw_timer_queue_cancel(&impl->stream_start_retry_timer); + pw_timer_queue_cancel(&impl->igmp_recovery.timer); if (impl->data_loop) pw_context_release_loop(impl->context, impl->data_loop); @@ -803,6 +984,17 @@ int pipewire__module_init(struct pw_impl_module *module, const char *args) impl->cleanup_interval = pw_properties_get_uint32(stream_props, "cleanup.sec", DEFAULT_CLEANUP_SEC); + impl->igmp_recovery.check_interval = SPA_MAX(pw_properties_get_uint32(stream_props, + "igmp.check.interval.sec", + DEFAULT_IGMP_CHECK_INTERVAL_SEC), 1u); + pw_log_info("using IGMP check interval of %" PRIu32 " second(s)", + impl->igmp_recovery.check_interval); + + impl->igmp_recovery.deadline = SPA_MAX(pw_properties_get_uint32(stream_props, + "igmp.deadline.sec", DEFAULT_IGMP_DEADLINE_SEC), 5u); + pw_log_info("using IGMP deadline of %" PRIu32 " second(s)", + impl->igmp_recovery.deadline); + impl->core = pw_context_get_object(impl->context, PW_TYPE_INTERFACE_Core); if (impl->core == NULL) { str = pw_properties_get(props, PW_KEY_REMOTE_NAME); From ec11859a48571a08c8803cdf6651d829b3468f81 Mon Sep 17 00:00:00 2001 From: Rui Matos Date: Thu, 18 Sep 2025 13:19:49 +0200 Subject: [PATCH 15/18] spa: Add predefined properties for clock identifiers --- spa/include/spa/param/props-types.h | 3 +++ spa/include/spa/param/props.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/spa/include/spa/param/props-types.h b/spa/include/spa/param/props-types.h index 54d17339f..0a7c916b8 100644 --- a/spa/include/spa/param/props-types.h +++ b/spa/include/spa/param/props-types.h @@ -40,6 +40,9 @@ static const struct spa_type_info spa_type_props[] = { { SPA_PROP_quality, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "quality", NULL }, { SPA_PROP_bluetoothAudioCodec, SPA_TYPE_Id, SPA_TYPE_INFO_PROPS_BASE "bluetoothAudioCodec", spa_type_bluetooth_audio_codec }, { SPA_PROP_bluetoothOffloadActive, SPA_TYPE_Bool, SPA_TYPE_INFO_PROPS_BASE "bluetoothOffloadActive", NULL }, + { SPA_PROP_clockId, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockId", NULL }, + { SPA_PROP_clockDevice, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockDevice", NULL }, + { SPA_PROP_clockInterface, SPA_TYPE_String, SPA_TYPE_INFO_PROPS_BASE "clockInterface", NULL }, { SPA_PROP_waveType, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "waveType", NULL }, { SPA_PROP_frequency, SPA_TYPE_Int, SPA_TYPE_INFO_PROPS_BASE "frequency", NULL }, diff --git a/spa/include/spa/param/props.h b/spa/include/spa/param/props.h index acc314ee1..48338b870 100644 --- a/spa/include/spa/param/props.h +++ b/spa/include/spa/param/props.h @@ -55,6 +55,9 @@ enum spa_prop { SPA_PROP_quality, SPA_PROP_bluetoothAudioCodec, SPA_PROP_bluetoothOffloadActive, + SPA_PROP_clockId, + SPA_PROP_clockDevice, + SPA_PROP_clockInterface, SPA_PROP_START_Audio = 0x10000, /**< audio related properties */ SPA_PROP_waveType, From 752de866aedfb955e61ded767f1263e2d4e6181a Mon Sep 17 00:00:00 2001 From: Rui Matos Date: Wed, 27 Aug 2025 15:50:07 +0200 Subject: [PATCH 16/18] spa: node-driver: Expose the clock id as param properties --- spa/plugins/support/node-driver.c | 359 +++++++++++++++++++++++++----- 1 file changed, 301 insertions(+), 58 deletions(-) diff --git a/spa/plugins/support/node-driver.c b/spa/plugins/support/node-driver.c index e7b2c702b..fa9cf3426 100644 --- a/spa/plugins/support/node-driver.c +++ b/spa/plugins/support/node-driver.c @@ -26,6 +26,8 @@ #include #include #include +#include +#include SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver"); @@ -48,12 +50,16 @@ SPA_LOG_TOPIC_DEFINE_STATIC(log_topic, "spa.driver"); #define BW_PERIOD (3 * SPA_NSEC_PER_SEC) #define MAX_ERROR_MS 1 +#define CLOCK_NAME_MAX 64 + struct props { bool freewheel; - char clock_name[64]; + char clock_name[CLOCK_NAME_MAX]; clockid_t clock_id; uint32_t freewheel_wait; float resync_ms; + char clock_device[CLOCK_NAME_MAX]; + char clock_interface[CLOCK_NAME_MAX]; }; struct clock_offset { @@ -73,7 +79,10 @@ struct impl { uint64_t info_all; struct spa_node_info info; - struct spa_param_info params[1]; +#define NODE_PropInfo 0 +#define NODE_Props 1 +#define N_NODE_PARAMS 2 + struct spa_param_info params[N_NODE_PARAMS]; struct spa_hook_list hooks; struct spa_callbacks callbacks; @@ -99,13 +108,20 @@ struct impl { struct clock_offset nsec_offset; }; +static void reset_props_strings(struct props *props) +{ + spa_zero(props->clock_name); + spa_zero(props->clock_device); + spa_zero(props->clock_interface); +} + static void reset_props(struct props *props) { props->freewheel = DEFAULT_FREEWHEEL; - spa_zero(props->clock_name); props->clock_id = CLOCK_MONOTONIC; props->freewheel_wait = DEFAULT_FREEWHEEL_WAIT; props->resync_ms = DEFAULT_RESYNC_MS; + reset_props_strings(props); } static const struct clock_info { @@ -598,10 +614,280 @@ static int impl_node_process(void *object) return SPA_STATUS_HAVE_DATA | SPA_STATUS_NEED_DATA; } +static int impl_node_enum_params(void *object, int seq, + uint32_t id, uint32_t start, uint32_t num, + const struct spa_pod *filter) +{ + struct impl *this = object; + struct spa_pod *param; + struct spa_pod_builder b = { 0 }; + uint8_t buffer[4096]; + struct spa_result_node_params result; + uint32_t count = 0; + + spa_return_val_if_fail(this != NULL, -EINVAL); + spa_return_val_if_fail(num != 0, -EINVAL); + + result.id = id; + result.next = start; +next: + result.index = result.next++; + + spa_pod_builder_init(&b, buffer, sizeof(buffer)); + + switch (id) { + case SPA_PARAM_PropInfo: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockId), + SPA_PROP_INFO_description, SPA_POD_String("The clock id (monotonic, realtime, etc.)"), + SPA_PROP_INFO_type, SPA_POD_String(clock_id_to_name(p->clock_id))); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockDevice), + SPA_PROP_INFO_description, SPA_POD_String("The clock device (eg. /dev/ptp0)"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->clock_device, sizeof(p->clock_device))); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_PropInfo, id, + SPA_PROP_INFO_id, SPA_POD_Id(SPA_PROP_clockInterface), + SPA_PROP_INFO_description, SPA_POD_String("The clock network interface (eg. eth0)"), + SPA_PROP_INFO_type, SPA_POD_Stringn(p->clock_interface, sizeof(p->clock_interface))); + break; + default: + return 0; + } + + break; + } + + case SPA_PARAM_Props: + { + struct props *p = &this->props; + + switch (result.index) { + case 0: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_clockId, SPA_POD_String(clock_id_to_name(p->clock_id)) + ); + break; + case 1: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_clockDevice, SPA_POD_Stringn(p->clock_device, sizeof(p->clock_device)) + ); + break; + case 2: + param = spa_pod_builder_add_object(&b, + SPA_TYPE_OBJECT_Props, id, + SPA_PROP_clockInterface, SPA_POD_Stringn(p->clock_interface, sizeof(p->clock_interface)) + ); + break; + default: + return 0; + } + + break; + } + + default: + return -ENOENT; + } + + if (spa_pod_filter(&b, &result.param, param, filter) < 0) + goto next; + + spa_node_emit_result(&this->hooks, seq, 0, SPA_RESULT_TYPE_NODE_PARAMS, &result); + + if (++count != num) + goto next; + + return 0; +} + +static int get_phc_index(struct spa_system *s, const char *name) { +#ifdef ETHTOOL_GET_TS_INFO + struct ethtool_ts_info info = {0}; + struct ifreq ifr = {0}; + int fd, err; + + info.cmd = ETHTOOL_GET_TS_INFO; + strncpy(ifr.ifr_name, name, IFNAMSIZ - 1); + ifr.ifr_data = (char *) &info; + + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return -errno; + + err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr); + close(fd); + if (err < 0) + return -errno; + + return info.phc_index; +#else + return -ENOTSUP; +#endif +} + +static bool parse_clock_id(struct impl *this, const char *s) +{ + int id = clock_name_to_id(s); + if (id == -1) { + spa_log_info(this->log, "unknown clock id '%s'", s); + return false; + } + this->props.clock_id = id; + if (this->clock_fd >= 0) { + close(this->clock_fd); + this->clock_fd = -1; + } + return true; +} + +static bool parse_clock_device(struct impl *this, const char *s) +{ + int fd = open(s, O_RDONLY); + if (fd == -1) { + spa_log_info(this->log, "failed to open clock device '%s': %m", s); + return false; + } + if (this->clock_fd >= 0) { + close(this->clock_fd); + } + this->clock_fd = fd; + this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); + return true; +} + +static bool parse_clock_interface(struct impl *this, const char *s) +{ + int phc_index = get_phc_index(this->data_system, s); + if (phc_index < 0) { + spa_log_info(this->log, "failed to get phc device index for interface '%s': %s", + s, spa_strerror(phc_index)); + return false; + } else { + char dev[19]; + spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index); + if (!parse_clock_device(this, dev)) { + spa_log_info(this->log, "failed to open clock device '%s' " + "for interface '%s': %m", dev, s); + return false; + } + } + return true; +} + +static void ensure_clock_name(struct impl *this) +{ + struct props *p = &this->props; + if (p->clock_name[0] == '\0') { + const char *name = clock_id_to_name(p->clock_id); + if (p->clock_device[0]) + name = p->clock_device; + if (p->clock_interface[0]) + name = p->clock_interface; + spa_scnprintf(p->clock_name, sizeof(p->clock_name), + "%s.%s", DEFAULT_CLOCK_PREFIX, name); + } +} + +static int impl_node_set_param(void *object, uint32_t id, uint32_t flags, + const struct spa_pod *param) +{ + struct impl *this = object; + + spa_return_val_if_fail(this != NULL, -EINVAL); + + switch (id) { + case SPA_PARAM_Props: + { + struct props *p = &this->props; + bool notify = false; + char buffer[CLOCK_NAME_MAX]; + int count; + + if (param == NULL) { + return 0; + } + + /* Note that the length passed to SPA_POD_OPT_Stringn() also + * includes room for the null terminator, so the content of the + * buffer variable is always guaranteed to be null terminated. */ + + spa_zero(buffer); + count = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_clockId, SPA_POD_OPT_Stringn(buffer, sizeof(buffer)) + ); + if (count && parse_clock_id(this, buffer)) + { + reset_props_strings(p); + notify = true; + } + + spa_zero(buffer); + count = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_clockDevice, SPA_POD_OPT_Stringn(buffer, sizeof(buffer)) + ); + if (count && parse_clock_device(this, buffer)) + { + reset_props_strings(p); + strncpy(p->clock_device, buffer, sizeof(p->clock_device)); + notify = true; + } + + spa_zero(buffer); + count = spa_pod_parse_object(param, + SPA_TYPE_OBJECT_Props, NULL, + SPA_PROP_clockInterface, SPA_POD_OPT_Stringn(buffer, sizeof(buffer)) + ); + if (count && parse_clock_interface(this, buffer)) + { + reset_props_strings(p); + strncpy(p->clock_interface, buffer, sizeof(p->clock_interface)); + notify = true; + } + + if (notify) + { + ensure_clock_name(this); + spa_log_info(this->log, "%p: setting clock to '%s'", this, p->clock_name); + if (this->started) { + do_stop(this); + do_start(this); + } + emit_node_info(this, true); + } + + break; + } + + default: + return -ENOENT; + break; + } + + return 0; +} + static const struct spa_node_methods impl_node = { SPA_VERSION_NODE_METHODS, .add_listener = impl_node_add_listener, .set_callbacks = impl_node_set_callbacks, + .enum_params = impl_node_enum_params, + .set_param = impl_node_set_param, .set_io = impl_node_set_io, .send_command = impl_node_send_command, .process = impl_node_process, @@ -655,31 +941,6 @@ impl_get_size(const struct spa_handle_factory *factory, return sizeof(struct impl); } -static int get_phc_index(struct spa_system *s, const char *name) { -#ifdef ETHTOOL_GET_TS_INFO - struct ethtool_ts_info info = {0}; - struct ifreq ifr = {0}; - int fd, err; - - info.cmd = ETHTOOL_GET_TS_INFO; - strncpy(ifr.ifr_name, name, IFNAMSIZ - 1); - ifr.ifr_data = (char *) &info; - - fd = socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) - return -errno; - - err = spa_system_ioctl(s, fd, SIOCETHTOOL, &ifr); - close(fd); - if (err < 0) - return -errno; - - return info.phc_index; -#else - return -ENOTSUP; -#endif -} - static int impl_init(const struct spa_handle_factory *factory, struct spa_handle *handle, @@ -727,9 +988,10 @@ impl_init(const struct spa_handle_factory *factory, this->info.max_input_ports = 0; this->info.max_output_ports = 0; this->info.flags = SPA_NODE_FLAG_RT; - this->params[0] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); + this->params[NODE_PropInfo] = SPA_PARAM_INFO(SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ); + this->params[NODE_Props] = SPA_PARAM_INFO(SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE); this->info.params = this->params; - this->info.n_params = 0; + this->info.n_params = N_NODE_PARAMS; reset_props(&this->props); @@ -742,37 +1004,17 @@ impl_init(const struct spa_handle_factory *factory, spa_scnprintf(this->props.clock_name, sizeof(this->props.clock_name), "%s", s); } else if (spa_streq(k, "clock.id") && this->clock_fd < 0) { - this->props.clock_id = clock_name_to_id(s); - if (this->props.clock_id == -1) { - spa_log_warn(this->log, "unknown clock id '%s'", s); - this->props.clock_id = DEFAULT_CLOCK_ID; - } + if (parse_clock_id(this, s)) + reset_props_strings(&this->props); } else if (spa_streq(k, "clock.device")) { - if (this->clock_fd >= 0) { - close(this->clock_fd); - } - this->clock_fd = open(s, O_RDONLY); - - if (this->clock_fd == -1) { - spa_log_warn(this->log, "failed to open clock device '%s': %m", s); - } else { - this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); + if (parse_clock_device(this, s)) { + reset_props_strings(&this->props); + strncpy(this->props.clock_device, s, sizeof(this->props.clock_device)-1); } } else if (spa_streq(k, "clock.interface") && this->clock_fd < 0) { - int phc_index = get_phc_index(this->data_system, s); - if (phc_index < 0) { - spa_log_warn(this->log, "failed to get phc device index for interface '%s': %s", - s, spa_strerror(phc_index)); - } else { - char dev[19]; - spa_scnprintf(dev, sizeof(dev), "/dev/ptp%d", phc_index); - this->clock_fd = open(dev, O_RDONLY); - if (this->clock_fd == -1) { - spa_log_warn(this->log, "failed to open clock device '%s' " - "for interface '%s': %m", dev, s); - } else { - this->props.clock_id = FD_TO_CLOCKID(this->clock_fd); - } + if (parse_clock_interface(this, s)) { + reset_props_strings(&this->props); + strncpy(this->props.clock_interface, s, sizeof(this->props.clock_interface)-1); } } else if (spa_streq(k, "freewheel.wait")) { this->props.freewheel_wait = atoi(s); @@ -785,6 +1027,7 @@ impl_init(const struct spa_handle_factory *factory, "%s.%s", DEFAULT_CLOCK_PREFIX, clock_id_to_name(this->props.clock_id)); } + ensure_clock_name(this); this->tracking = !clock_for_timerfd(this->props.clock_id); this->timer_clockid = this->tracking ? CLOCK_MONOTONIC : this->props.clock_id; From a837dcd40bbfe1a0b5231af6d52fabad43bb3846 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 28 Oct 2025 08:19:17 +0100 Subject: [PATCH 17/18] audioadapter: renegotiate when driver changes The renegotiated format can depend on the clock rate of the new driver. See #4933 --- spa/plugins/audioconvert/audioadapter.c | 1 + spa/plugins/videoconvert/videoadapter.c | 1 + 2 files changed, 2 insertions(+) diff --git a/spa/plugins/audioconvert/audioadapter.c b/spa/plugins/audioconvert/audioadapter.c index 0d0d3e15f..7c4605c8a 100644 --- a/spa/plugins/audioconvert/audioadapter.c +++ b/spa/plugins/audioconvert/audioadapter.c @@ -931,6 +931,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) switch (id) { case SPA_IO_Position: this->io_position = data; + this->recheck_format = true; break; default: break; diff --git a/spa/plugins/videoconvert/videoadapter.c b/spa/plugins/videoconvert/videoadapter.c index 23b55d768..f4346fab6 100644 --- a/spa/plugins/videoconvert/videoadapter.c +++ b/spa/plugins/videoconvert/videoadapter.c @@ -900,6 +900,7 @@ static int impl_node_set_io(void *object, uint32_t id, void *data, size_t size) switch (id) { case SPA_IO_Position: this->io_position = data; + this->recheck_format = true; break; default: break; From a8138300244e0e44652e8e9bc634ee916d12563a Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 28 Oct 2025 08:48:18 +0100 Subject: [PATCH 18/18] po: update Turkish translation --- po/tr.po | 341 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 183 insertions(+), 158 deletions(-) diff --git a/po/tr.po b/po/tr.po index 49eb6eaba..345963c70 100644 --- a/po/tr.po +++ b/po/tr.po @@ -1,46 +1,51 @@ # Turkish translation for PipeWire. -# Copyright (C) 2014-2024 PipeWire's COPYRIGHT HOLDER +# Copyright (C) 2014-2025 PipeWire's COPYRIGHT HOLDER # This file is distributed under the same license as the PipeWire package. # # Necdet Yücel , 2014. # Kaan Özdinçer , 2014. # Muhammet Kara , 2015, 2016, 2017. # Oğuz Ersen , 2021-2022. -# Sabri Ünal , 2024. +# Sabri Ünal , 2024, 2025. # msgid "" msgstr "" "Project-Id-Version: PipeWire master\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-25 03:43+0300\n" -"PO-Revision-Date: 2024-02-25 03:49+0300\n" -"Last-Translator: Sabri Ünal \n" +"Report-Msgid-Bugs-To: https://gitlab.freedesktop.org/pipewire/pipewire/-/" +"issues\n" +"POT-Creation-Date: 2025-10-24 15:37+0000\n" +"PO-Revision-Date: 2025-10-24 20:15+0300\n" +"Last-Translator: Sabri Ünal \n" "Language-Team: Türkçe \n" "Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" -"X-Generator: Poedit 3.4.2\n" +"X-Generator: Poedit 3.8\n" -#: src/daemon/pipewire.c:26 +#: src/daemon/pipewire.c:29 #, c-format msgid "" "%s [options]\n" " -h, --help Show this help\n" +" -v, --verbose Increase verbosity by one level\n" " --version Show version\n" " -c, --config Load config (Default %s)\n" +" -P --properties Set context properties\n" msgstr "" "%s [seçenekler]\n" " -h, --help Bu yardımı göster\n" +" -v, --verbose Ayrıntı düzeyini bir düzey artır\n" " --version Sürümü göster\n" " -c, --config Yapılandırmayı yükle (Öntanımlı %s)\n" +" -P --properties Bağlam özelliklerini ayarla\n" -#: src/daemon/pipewire.desktop.in:4 +#: src/daemon/pipewire.desktop.in:3 msgid "PipeWire Media System" msgstr "PipeWire Ortam Sistemi" -#: src/daemon/pipewire.desktop.in:5 +#: src/daemon/pipewire.desktop.in:4 msgid "Start the PipeWire Media System" msgstr "PipeWire Ortam Sistemini Başlat" @@ -54,26 +59,26 @@ msgstr "%s%s%s tüneli" msgid "Dummy Output" msgstr "Temsili Çıkış" -#: src/modules/module-pulse-tunnel.c:774 +#: src/modules/module-pulse-tunnel.c:760 #, c-format msgid "Tunnel for %s@%s" msgstr "%s@%s için tünel" -#: src/modules/module-zeroconf-discover.c:315 +#: src/modules/module-zeroconf-discover.c:320 msgid "Unknown device" msgstr "Bilinmeyen aygıt" -#: src/modules/module-zeroconf-discover.c:327 +#: src/modules/module-zeroconf-discover.c:332 #, c-format msgid "%s on %s@%s" msgstr "%s, %s@%s" -#: src/modules/module-zeroconf-discover.c:331 +#: src/modules/module-zeroconf-discover.c:336 #, c-format msgid "%s on %s" msgstr "%s, %s" -#: src/tools/pw-cat.c:991 +#: src/tools/pw-cat.c:1096 #, c-format msgid "" "%s [options] [|-]\n" @@ -88,7 +93,7 @@ msgstr "" " -v, --verbose Ayrıntılı işlemleri etkinleştir\n" "\n" -#: src/tools/pw-cat.c:998 +#: src/tools/pw-cat.c:1103 #, c-format msgid "" " -R, --remote Remote daemon name\n" @@ -122,7 +127,7 @@ msgstr "" " -P --properties Düğüm özelliklerini ayarla\n" "\n" -#: src/tools/pw-cat.c:1016 +#: src/tools/pw-cat.c:1121 #, c-format msgid "" " --rate Sample rate (req. for rec) (default " @@ -139,6 +144,10 @@ msgid "" " --volume Stream volume 0-1.0 (default %.3f)\n" " -q --quality Resampler quality (0 - 15) (default " "%d)\n" +" -a, --raw RAW mode\n" +" -M, --force-midi Force midi format, one of \"midi\" " +"or \"ump\", (default ump)\n" +" -n, --sample-count COUNT Stop after COUNT samples\n" "\n" msgstr "" " --rate Örnekleme oranı (kayıt için gerekli) " @@ -156,15 +165,21 @@ msgstr "" "%.3f)\n" " -q --quality Yeniden örnekleyici kalitesi (0 - " "15) (öntanımlı %d)\n" +" -a, --raw HAM kipi\n" +" -M, --force-midi Midi biçimini zorla, ikisinden " +"birisi \"midi\" ya da\"ump\", (öntanımlı ump)\n" +" -n, --sample-count COUNT COUNT örnekleme sonrası dur\n" "\n" -#: src/tools/pw-cat.c:1033 +#: src/tools/pw-cat.c:1141 msgid "" " -p, --playback Playback mode\n" " -r, --record Recording mode\n" " -m, --midi Midi mode\n" " -d, --dsd DSD mode\n" " -o, --encoded Encoded mode\n" +" -s, --sysex SysEx mode\n" +" -c, --midi-clip MIDI clip mode\n" "\n" msgstr "" " -p, --playback Çalma kipi\n" @@ -172,9 +187,11 @@ msgstr "" " -m, --midi Midi kipi\n" " -d, --dsd DSD kipi\n" " -o, --encoded Kodlanmış kip\n" +" -s, --sysex SysEx kipi\n" +" -c, --midi-clip MIDI klip kipi\n" "\n" -#: src/tools/pw-cli.c:2252 +#: src/tools/pw-cli.c:2386 #, c-format msgid "" "%s [options] [command]\n" @@ -193,195 +210,203 @@ msgstr "" " -r, --remote Uzak arka plan programı adı\n" " -m, --monitor Etkinliği izle\n" -#: spa/plugins/alsa/acp/acp.c:327 +#: spa/plugins/alsa/acp/acp.c:361 msgid "Pro Audio" msgstr "Profesyonel Ses" -#: spa/plugins/alsa/acp/acp.c:488 spa/plugins/alsa/acp/alsa-mixer.c:4633 -#: spa/plugins/bluez5/bluez5-device.c:1701 +#: spa/plugins/alsa/acp/acp.c:537 spa/plugins/alsa/acp/alsa-mixer.c:4699 +#: spa/plugins/bluez5/bluez5-device.c:1976 msgid "Off" msgstr "Kapalı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2652 +#: spa/plugins/alsa/acp/acp.c:620 +#, c-format +msgid "%s [ALSA UCM error]" +msgstr "%s [ALSA UCM hatası]" + +#: spa/plugins/alsa/acp/alsa-mixer.c:2721 msgid "Input" msgstr "Giriş" -#: spa/plugins/alsa/acp/alsa-mixer.c:2653 +#: spa/plugins/alsa/acp/alsa-mixer.c:2722 msgid "Docking Station Input" msgstr "Yerleştirme İstasyonu Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2654 +#: spa/plugins/alsa/acp/alsa-mixer.c:2723 msgid "Docking Station Microphone" msgstr "Yerleştirme İstasyonu Mikrofonu" -#: spa/plugins/alsa/acp/alsa-mixer.c:2655 +#: spa/plugins/alsa/acp/alsa-mixer.c:2724 msgid "Docking Station Line In" msgstr "Yerleştirme İstasyonu Hat Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2656 -#: spa/plugins/alsa/acp/alsa-mixer.c:2747 +#: spa/plugins/alsa/acp/alsa-mixer.c:2725 +#: spa/plugins/alsa/acp/alsa-mixer.c:2816 msgid "Line In" msgstr "Hat Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2657 -#: spa/plugins/alsa/acp/alsa-mixer.c:2741 -#: spa/plugins/bluez5/bluez5-device.c:1989 +#: spa/plugins/alsa/acp/alsa-mixer.c:2726 +#: spa/plugins/alsa/acp/alsa-mixer.c:2810 +#: spa/plugins/bluez5/bluez5-device.c:2374 msgid "Microphone" msgstr "Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2658 -#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +#: spa/plugins/alsa/acp/alsa-mixer.c:2727 +#: spa/plugins/alsa/acp/alsa-mixer.c:2811 msgid "Front Microphone" msgstr "Ön Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2659 -#: spa/plugins/alsa/acp/alsa-mixer.c:2743 +#: spa/plugins/alsa/acp/alsa-mixer.c:2728 +#: spa/plugins/alsa/acp/alsa-mixer.c:2812 msgid "Rear Microphone" msgstr "Arka Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2660 +#: spa/plugins/alsa/acp/alsa-mixer.c:2729 msgid "External Microphone" msgstr "Harici Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2661 -#: spa/plugins/alsa/acp/alsa-mixer.c:2745 +#: spa/plugins/alsa/acp/alsa-mixer.c:2730 +#: spa/plugins/alsa/acp/alsa-mixer.c:2814 msgid "Internal Microphone" msgstr "Dahili Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2662 -#: spa/plugins/alsa/acp/alsa-mixer.c:2748 +#: spa/plugins/alsa/acp/alsa-mixer.c:2731 +#: spa/plugins/alsa/acp/alsa-mixer.c:2817 msgid "Radio" msgstr "Radyo" -#: spa/plugins/alsa/acp/alsa-mixer.c:2663 -#: spa/plugins/alsa/acp/alsa-mixer.c:2749 +#: spa/plugins/alsa/acp/alsa-mixer.c:2732 +#: spa/plugins/alsa/acp/alsa-mixer.c:2818 msgid "Video" msgstr "Video" -#: spa/plugins/alsa/acp/alsa-mixer.c:2664 +#: spa/plugins/alsa/acp/alsa-mixer.c:2733 msgid "Automatic Gain Control" msgstr "Otomatik Kazanç Denetimi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2665 +#: spa/plugins/alsa/acp/alsa-mixer.c:2734 msgid "No Automatic Gain Control" msgstr "Otomatik Kazanç Denetimi Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2666 +#: spa/plugins/alsa/acp/alsa-mixer.c:2735 msgid "Boost" msgstr "Artır" -#: spa/plugins/alsa/acp/alsa-mixer.c:2667 +#: spa/plugins/alsa/acp/alsa-mixer.c:2736 msgid "No Boost" msgstr "Artırma Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2668 +#: spa/plugins/alsa/acp/alsa-mixer.c:2737 msgid "Amplifier" msgstr "Yükseltici" -#: spa/plugins/alsa/acp/alsa-mixer.c:2669 +#: spa/plugins/alsa/acp/alsa-mixer.c:2738 msgid "No Amplifier" msgstr "Yükseltici Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2670 +#: spa/plugins/alsa/acp/alsa-mixer.c:2739 msgid "Bass Boost" msgstr "Bas Artır" -#: spa/plugins/alsa/acp/alsa-mixer.c:2671 +#: spa/plugins/alsa/acp/alsa-mixer.c:2740 msgid "No Bass Boost" msgstr "Bas Artırma Yok" -#: spa/plugins/alsa/acp/alsa-mixer.c:2672 -#: spa/plugins/bluez5/bluez5-device.c:1995 +#: spa/plugins/alsa/acp/alsa-mixer.c:2741 +#: spa/plugins/bluez5/bluez5-device.c:2380 msgid "Speaker" msgstr "Hoparlör" -#: spa/plugins/alsa/acp/alsa-mixer.c:2673 -#: spa/plugins/alsa/acp/alsa-mixer.c:2751 +#. Don't call it "headset", the HF one has the mic +#: spa/plugins/alsa/acp/alsa-mixer.c:2742 +#: spa/plugins/alsa/acp/alsa-mixer.c:2820 +#: spa/plugins/bluez5/bluez5-device.c:2386 +#: spa/plugins/bluez5/bluez5-device.c:2453 msgid "Headphones" msgstr "Kulaklık" -#: spa/plugins/alsa/acp/alsa-mixer.c:2740 +#: spa/plugins/alsa/acp/alsa-mixer.c:2809 msgid "Analog Input" msgstr "Analog Giriş" -#: spa/plugins/alsa/acp/alsa-mixer.c:2744 +#: spa/plugins/alsa/acp/alsa-mixer.c:2813 msgid "Dock Microphone" msgstr "Yapışık Mikrofon" -#: spa/plugins/alsa/acp/alsa-mixer.c:2746 +#: spa/plugins/alsa/acp/alsa-mixer.c:2815 msgid "Headset Microphone" msgstr "Mikrofonlu Kulaklık" -#: spa/plugins/alsa/acp/alsa-mixer.c:2750 +#: spa/plugins/alsa/acp/alsa-mixer.c:2819 msgid "Analog Output" msgstr "Analog Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2752 +#: spa/plugins/alsa/acp/alsa-mixer.c:2821 msgid "Headphones 2" msgstr "Kulaklık 2" -#: spa/plugins/alsa/acp/alsa-mixer.c:2753 +#: spa/plugins/alsa/acp/alsa-mixer.c:2822 msgid "Headphones Mono Output" msgstr "Kulaklık Tek Kanallı Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2754 +#: spa/plugins/alsa/acp/alsa-mixer.c:2823 msgid "Line Out" msgstr "Hat Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2755 +#: spa/plugins/alsa/acp/alsa-mixer.c:2824 msgid "Analog Mono Output" msgstr "Analog Tek Kanallı Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2756 +#: spa/plugins/alsa/acp/alsa-mixer.c:2825 msgid "Speakers" msgstr "Hoparlörler" -#: spa/plugins/alsa/acp/alsa-mixer.c:2757 +#: spa/plugins/alsa/acp/alsa-mixer.c:2826 msgid "HDMI / DisplayPort" msgstr "HDMI / DisplayPort" -#: spa/plugins/alsa/acp/alsa-mixer.c:2758 +#: spa/plugins/alsa/acp/alsa-mixer.c:2827 msgid "Digital Output (S/PDIF)" msgstr "Sayısal Çıkış (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2759 +#: spa/plugins/alsa/acp/alsa-mixer.c:2828 msgid "Digital Input (S/PDIF)" msgstr "Sayısal Giriş (S/PDIF)" -#: spa/plugins/alsa/acp/alsa-mixer.c:2760 +#: spa/plugins/alsa/acp/alsa-mixer.c:2829 msgid "Multichannel Input" msgstr "Çok Kanallı Giriş" -#: spa/plugins/alsa/acp/alsa-mixer.c:2761 +#: spa/plugins/alsa/acp/alsa-mixer.c:2830 msgid "Multichannel Output" msgstr "Çok Kanallı Çıkış" -#: spa/plugins/alsa/acp/alsa-mixer.c:2762 +#: spa/plugins/alsa/acp/alsa-mixer.c:2831 msgid "Game Output" msgstr "Oyun Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2763 -#: spa/plugins/alsa/acp/alsa-mixer.c:2764 +#: spa/plugins/alsa/acp/alsa-mixer.c:2832 +#: spa/plugins/alsa/acp/alsa-mixer.c:2833 msgid "Chat Output" msgstr "Sohbet Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:2765 +#: spa/plugins/alsa/acp/alsa-mixer.c:2834 msgid "Chat Input" msgstr "Sohbet Girişi" -#: spa/plugins/alsa/acp/alsa-mixer.c:2766 +#: spa/plugins/alsa/acp/alsa-mixer.c:2835 msgid "Virtual Surround 7.1" msgstr "Sanal Çevresel Ses 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4456 +#: spa/plugins/alsa/acp/alsa-mixer.c:4522 msgid "Analog Mono" msgstr "Analog Tek Kanallı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4457 +#: spa/plugins/alsa/acp/alsa-mixer.c:4523 msgid "Analog Mono (Left)" msgstr "Analog Tek Kanallı (Sol)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4458 +#: spa/plugins/alsa/acp/alsa-mixer.c:4524 msgid "Analog Mono (Right)" msgstr "Analog Tek Kanallı (Sağ)" @@ -390,147 +415,147 @@ msgstr "Analog Tek Kanallı (Sağ)" #. * here would lead to the source name to become "Analog Stereo Input #. * Input". The same logic applies to analog-stereo-output, #. * multichannel-input and multichannel-output. -#: spa/plugins/alsa/acp/alsa-mixer.c:4459 -#: spa/plugins/alsa/acp/alsa-mixer.c:4467 -#: spa/plugins/alsa/acp/alsa-mixer.c:4468 +#: spa/plugins/alsa/acp/alsa-mixer.c:4525 +#: spa/plugins/alsa/acp/alsa-mixer.c:4533 +#: spa/plugins/alsa/acp/alsa-mixer.c:4534 msgid "Analog Stereo" msgstr "Analog Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4460 +#: spa/plugins/alsa/acp/alsa-mixer.c:4526 msgid "Mono" msgstr "Tek Kanallı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4461 +#: spa/plugins/alsa/acp/alsa-mixer.c:4527 msgid "Stereo" msgstr "Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4469 -#: spa/plugins/alsa/acp/alsa-mixer.c:4627 -#: spa/plugins/bluez5/bluez5-device.c:1977 +#: spa/plugins/alsa/acp/alsa-mixer.c:4535 +#: spa/plugins/alsa/acp/alsa-mixer.c:4693 +#: spa/plugins/bluez5/bluez5-device.c:2362 msgid "Headset" msgstr "Kulaklık" -#: spa/plugins/alsa/acp/alsa-mixer.c:4470 -#: spa/plugins/alsa/acp/alsa-mixer.c:4628 +#: spa/plugins/alsa/acp/alsa-mixer.c:4536 +#: spa/plugins/alsa/acp/alsa-mixer.c:4694 msgid "Speakerphone" msgstr "Hoparlör" -#: spa/plugins/alsa/acp/alsa-mixer.c:4471 -#: spa/plugins/alsa/acp/alsa-mixer.c:4472 +#: spa/plugins/alsa/acp/alsa-mixer.c:4537 +#: spa/plugins/alsa/acp/alsa-mixer.c:4538 msgid "Multichannel" msgstr "Çok kanallı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4473 +#: spa/plugins/alsa/acp/alsa-mixer.c:4539 msgid "Analog Surround 2.1" msgstr "Analog Çevresel Ses 2.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4474 +#: spa/plugins/alsa/acp/alsa-mixer.c:4540 msgid "Analog Surround 3.0" msgstr "Analog Çevresel Ses 3.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4475 +#: spa/plugins/alsa/acp/alsa-mixer.c:4541 msgid "Analog Surround 3.1" msgstr "Analog Çevresel Ses 3.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4476 +#: spa/plugins/alsa/acp/alsa-mixer.c:4542 msgid "Analog Surround 4.0" msgstr "Analog Çevresel Ses 4.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4477 +#: spa/plugins/alsa/acp/alsa-mixer.c:4543 msgid "Analog Surround 4.1" msgstr "Analog Çevresel Ses 4.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4478 +#: spa/plugins/alsa/acp/alsa-mixer.c:4544 msgid "Analog Surround 5.0" msgstr "Analog Çevresel Ses 5.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4479 +#: spa/plugins/alsa/acp/alsa-mixer.c:4545 msgid "Analog Surround 5.1" msgstr "Analog Çevresel Ses 5.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4480 +#: spa/plugins/alsa/acp/alsa-mixer.c:4546 msgid "Analog Surround 6.0" msgstr "Analog Çevresel Ses 6.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4481 +#: spa/plugins/alsa/acp/alsa-mixer.c:4547 msgid "Analog Surround 6.1" msgstr "Analog Çevresel Ses 6.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4482 +#: spa/plugins/alsa/acp/alsa-mixer.c:4548 msgid "Analog Surround 7.0" msgstr "Analog Çevresel Ses 7.0" -#: spa/plugins/alsa/acp/alsa-mixer.c:4483 +#: spa/plugins/alsa/acp/alsa-mixer.c:4549 msgid "Analog Surround 7.1" msgstr "Analog Çevresel Ses 7.1" -#: spa/plugins/alsa/acp/alsa-mixer.c:4484 +#: spa/plugins/alsa/acp/alsa-mixer.c:4550 msgid "Digital Stereo (IEC958)" msgstr "Sayısal Stereo (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4485 +#: spa/plugins/alsa/acp/alsa-mixer.c:4551 msgid "Digital Surround 4.0 (IEC958/AC3)" msgstr "Sayısal Çevresel Ses 4.0 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4486 +#: spa/plugins/alsa/acp/alsa-mixer.c:4552 msgid "Digital Surround 5.1 (IEC958/AC3)" msgstr "Sayısal Çevresel Ses 5.1 (IEC958/AC3)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4487 +#: spa/plugins/alsa/acp/alsa-mixer.c:4553 msgid "Digital Surround 5.1 (IEC958/DTS)" msgstr "Sayısal Çevresel Ses 5.1 (IEC958/DTS)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4488 +#: spa/plugins/alsa/acp/alsa-mixer.c:4554 msgid "Digital Stereo (HDMI)" msgstr "Sayısal Stereo (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4489 +#: spa/plugins/alsa/acp/alsa-mixer.c:4555 msgid "Digital Surround 5.1 (HDMI)" msgstr "Sayısal Çevresel Ses 5.1 (HDMI)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4490 +#: spa/plugins/alsa/acp/alsa-mixer.c:4556 msgid "Chat" msgstr "Sohbet" -#: spa/plugins/alsa/acp/alsa-mixer.c:4491 +#: spa/plugins/alsa/acp/alsa-mixer.c:4557 msgid "Game" msgstr "Oyun" -#: spa/plugins/alsa/acp/alsa-mixer.c:4625 +#: spa/plugins/alsa/acp/alsa-mixer.c:4691 msgid "Analog Mono Duplex" msgstr "Analog Tek Kanallı İkili" -#: spa/plugins/alsa/acp/alsa-mixer.c:4626 +#: spa/plugins/alsa/acp/alsa-mixer.c:4692 msgid "Analog Stereo Duplex" msgstr "Analog İkili Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4629 +#: spa/plugins/alsa/acp/alsa-mixer.c:4695 msgid "Digital Stereo Duplex (IEC958)" msgstr "Sayısal İkili Stereo (IEC958)" -#: spa/plugins/alsa/acp/alsa-mixer.c:4630 +#: spa/plugins/alsa/acp/alsa-mixer.c:4696 msgid "Multichannel Duplex" msgstr "Çok Kanallı İkili" -#: spa/plugins/alsa/acp/alsa-mixer.c:4631 +#: spa/plugins/alsa/acp/alsa-mixer.c:4697 msgid "Stereo Duplex" msgstr "İkili Stereo" -#: spa/plugins/alsa/acp/alsa-mixer.c:4632 +#: spa/plugins/alsa/acp/alsa-mixer.c:4698 msgid "Mono Chat + 7.1 Surround" msgstr "Tek Kanallı Sohbet + 7.1 Çevresel Ses" -#: spa/plugins/alsa/acp/alsa-mixer.c:4733 +#: spa/plugins/alsa/acp/alsa-mixer.c:4799 #, c-format msgid "%s Output" msgstr "%s Çıkışı" -#: spa/plugins/alsa/acp/alsa-mixer.c:4741 +#: spa/plugins/alsa/acp/alsa-mixer.c:4807 #, c-format msgid "%s Input" msgstr "%s Girişi" -#: spa/plugins/alsa/acp/alsa-util.c:1220 spa/plugins/alsa/acp/alsa-util.c:1314 +#: spa/plugins/alsa/acp/alsa-util.c:1233 spa/plugins/alsa/acp/alsa-util.c:1327 #, c-format msgid "" "snd_pcm_avail() returned a value that is exceptionally large: %lu byte (%lu " @@ -547,16 +572,16 @@ msgstr[0] "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/alsa-util.c:1286 +#: spa/plugins/alsa/acp/alsa-util.c:1299 #, c-format msgid "" -"snd_pcm_delay() returned a value that is exceptionally large: %li byte (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li byte " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgid_plural "" -"snd_pcm_delay() returned a value that is exceptionally large: %li bytes (%s" -"%lu ms).\n" +"snd_pcm_delay() returned a value that is exceptionally large: %li bytes " +"(%s%lu ms).\n" "Most likely this is a bug in the ALSA driver '%s'. Please report this issue " "to the ALSA developers." msgstr[0] "" @@ -564,7 +589,7 @@ msgstr[0] "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/alsa-util.c:1333 +#: spa/plugins/alsa/acp/alsa-util.c:1346 #, c-format msgid "" "snd_pcm_avail_delay() returned strange values: delay %lu is less than avail " @@ -577,7 +602,7 @@ msgstr "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/alsa-util.c:1376 +#: spa/plugins/alsa/acp/alsa-util.c:1389 #, c-format msgid "" "snd_pcm_mmap_begin() returned a value that is exceptionally large: %lu byte " @@ -595,112 +620,112 @@ msgstr[0] "" "Büyük ihtimalle bu bir ALSA sürücüsü '%s' hatasıdır. Lütfen bu sorunu ALSA " "geliştiricilerine bildirin." -#: spa/plugins/alsa/acp/channelmap.h:457 +#: spa/plugins/alsa/acp/channelmap.h:460 msgid "(invalid)" msgstr "(geçersiz)" -#: spa/plugins/alsa/acp/compat.c:193 +#: spa/plugins/alsa/acp/compat.c:194 msgid "Built-in Audio" msgstr "Dahili Ses" -#: spa/plugins/alsa/acp/compat.c:198 +#: spa/plugins/alsa/acp/compat.c:199 msgid "Modem" msgstr "Modem" -#: spa/plugins/bluez5/bluez5-device.c:1712 +#: spa/plugins/bluez5/bluez5-device.c:1987 msgid "Audio Gateway (A2DP Source & HSP/HFP AG)" msgstr "Ses Geçidi (A2DP Kaynak & HSP/HFP AG)" -#: spa/plugins/bluez5/bluez5-device.c:1760 +#: spa/plugins/bluez5/bluez5-device.c:2016 +msgid "Audio Streaming for Hearing Aids (ASHA Sink)" +msgstr "İşitme Aygıtları İçin Ses Akışı (ASHA Alıcı)" + +#: spa/plugins/bluez5/bluez5-device.c:2059 #, c-format msgid "High Fidelity Playback (A2DP Sink, codec %s)" msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1763 +#: spa/plugins/bluez5/bluez5-device.c:2062 #, c-format msgid "High Fidelity Duplex (A2DP Source/Sink, codec %s)" msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1771 +#: spa/plugins/bluez5/bluez5-device.c:2070 msgid "High Fidelity Playback (A2DP Sink)" msgstr "Yüksek Kaliteli Çalma (A2DP Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1773 +#: spa/plugins/bluez5/bluez5-device.c:2072 msgid "High Fidelity Duplex (A2DP Source/Sink)" msgstr "Yüksek Kaliteli İkili (A2DP Kaynak/Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1823 +#: spa/plugins/bluez5/bluez5-device.c:2146 #, c-format msgid "High Fidelity Playback (BAP Sink, codec %s)" msgstr "Yüksek Kaliteli Çalma (BAP Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1828 +#: spa/plugins/bluez5/bluez5-device.c:2151 #, c-format msgid "High Fidelity Input (BAP Source, codec %s)" msgstr "Yüksek Kaliteli Giriş (BAP Kaynak, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1832 +#: spa/plugins/bluez5/bluez5-device.c:2155 #, c-format msgid "High Fidelity Duplex (BAP Source/Sink, codec %s)" msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1841 +#: spa/plugins/bluez5/bluez5-device.c:2164 msgid "High Fidelity Playback (BAP Sink)" msgstr "Yüksek Kaliteli Çalma (BAP Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1845 +#: spa/plugins/bluez5/bluez5-device.c:2168 msgid "High Fidelity Input (BAP Source)" msgstr "Yüksek Kaliteli Giriş (BAP Kaynak)" -#: spa/plugins/bluez5/bluez5-device.c:1848 +#: spa/plugins/bluez5/bluez5-device.c:2171 msgid "High Fidelity Duplex (BAP Source/Sink)" msgstr "Yüksek Kaliteli İkili (BAP Kaynak/Alıcı)" -#: spa/plugins/bluez5/bluez5-device.c:1897 +#: spa/plugins/bluez5/bluez5-device.c:2211 #, c-format msgid "Headset Head Unit (HSP/HFP, codec %s)" msgstr "Kulaklık Ana Birimi (HSP/HFP, çözücü %s)" -#: spa/plugins/bluez5/bluez5-device.c:1978 -#: spa/plugins/bluez5/bluez5-device.c:1983 -#: spa/plugins/bluez5/bluez5-device.c:1990 -#: spa/plugins/bluez5/bluez5-device.c:1996 -#: spa/plugins/bluez5/bluez5-device.c:2002 -#: spa/plugins/bluez5/bluez5-device.c:2008 -#: spa/plugins/bluez5/bluez5-device.c:2014 -#: spa/plugins/bluez5/bluez5-device.c:2020 -#: spa/plugins/bluez5/bluez5-device.c:2026 +#: spa/plugins/bluez5/bluez5-device.c:2363 +#: spa/plugins/bluez5/bluez5-device.c:2368 +#: spa/plugins/bluez5/bluez5-device.c:2375 +#: spa/plugins/bluez5/bluez5-device.c:2381 +#: spa/plugins/bluez5/bluez5-device.c:2387 +#: spa/plugins/bluez5/bluez5-device.c:2393 +#: spa/plugins/bluez5/bluez5-device.c:2399 +#: spa/plugins/bluez5/bluez5-device.c:2405 +#: spa/plugins/bluez5/bluez5-device.c:2411 msgid "Handsfree" msgstr "Ahizesiz" -#: spa/plugins/bluez5/bluez5-device.c:1984 +#: spa/plugins/bluez5/bluez5-device.c:2369 msgid "Handsfree (HFP)" msgstr "Ahizesiz (HFP)" -#: spa/plugins/bluez5/bluez5-device.c:2001 -msgid "Headphone" -msgstr "Kulaklık" - -#: spa/plugins/bluez5/bluez5-device.c:2007 +#: spa/plugins/bluez5/bluez5-device.c:2392 msgid "Portable" msgstr "Taşınabilir" -#: spa/plugins/bluez5/bluez5-device.c:2013 +#: spa/plugins/bluez5/bluez5-device.c:2398 msgid "Car" msgstr "Araba" -#: spa/plugins/bluez5/bluez5-device.c:2019 +#: spa/plugins/bluez5/bluez5-device.c:2404 msgid "HiFi" msgstr "Yüksek Kalite" -#: spa/plugins/bluez5/bluez5-device.c:2025 +#: spa/plugins/bluez5/bluez5-device.c:2410 msgid "Phone" msgstr "Telefon" -#: spa/plugins/bluez5/bluez5-device.c:2032 +#: spa/plugins/bluez5/bluez5-device.c:2417 msgid "Bluetooth" msgstr "Bluetooth" -#: spa/plugins/bluez5/bluez5-device.c:2033 +#: spa/plugins/bluez5/bluez5-device.c:2418 msgid "Bluetooth (HFP)" msgstr "Bluetooth (HFP)"