Implementation_of_replace_port_to_support_conf_async_port - #4985
Implementation_of_replace_port_to_support_conf_async_port#4985PesalaDeSilva wants to merge 4 commits into
Conversation
Signed-off-by: Pesala Silva <pesala.silva@ifs.com>
There was a problem hiding this comment.
Pull request overview
Adds an early "placeholder" conference-slot reservation for PJSUA calls so callers can hold a stable conf_slot ID while media setup is still in progress, plus a new pjmedia_conf_replace_port API on the conference bridge that swaps the placeholder for the real stream port via an op queue, with deferred destruction of the old port.
Changes:
- New pjsua API
pjsua_call_preallocate_conf_port()and inline placeholder allocation inpjsua_aud_channel_updatewith areplace_port-then-fallback-to-add_portstrategy. - New
pjmedia_conf_replace_port(queued) +pjmedia_conf_replace_port_impl(worker) with anop_list/op_mutex/destroy_listmechanism drained from the masterget_frametick. - Hardening of
get_frame/put_frameagainst NULL master/port and a renamed mutex string.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| pjmedia/include/pjmedia/conference.h | Adds op-type enum, internal conf_op / port_destroy_item types, static-function declarations, and pjmedia_conf_replace_port[_impl] to the public header. |
| pjmedia/src/pjmedia/conference.c | Implements queued replace_port, deferred port destroys, op-queue drain in get_frame, NULL guards in get_frame/put_frame; introduces an op_mutex field that is never created. |
| pjsip/include/pjsua-lib/pjsua.h | Declares the new pjsua_call_preallocate_conf_port API. |
| pjsip/src/pjsua-lib/pjsua_aud.c | Implements pre-allocation API and uses replace_port from pjsua_aud_channel_update, with placeholder also auto-allocated when no caller opted in. |
Comments suppressed due to low confidence (7)
pjmedia/src/pjmedia/conference.c:881
conf->op_mutexis declared and used (locked/unlocked inpjmedia_conf_replace_port, inget_frameto drain ops, and destroyed inpjmedia_conf_destroy), but it is never created withpj_mutex_create_recursive()/pj_mutex_create_simple()anywhere inpjmedia_conf_create(). With the field left as NULL fromPJ_POOL_ZALLOC_T, the first call topj_mutex_lock(conf->op_mutex)will dereference a NULL pointer and crash. Anop_mutexcreation step must be added during conference creation (and on failure,pjmedia_conf_destroymust clean up the rest correctly).
conf->op_list = PJ_POOL_ZALLOC_T(pool, struct conf_op);
pj_list_init(conf->op_list);
conf->op_pending = PJ_FALSE;
//conf->destroy_list = PJ_POOL_ZALLOC_T(pool, struct port_destroy_item);
pj_list_init(conf->destroy_list);
pjmedia/include/pjmedia/conference.h:361
- These three functions are declared
staticin a public header.staticdeclarations in a header give each translation unit that includes the header its own (file-local) declaration but no definition, producing "used but never defined" / unused-static-function warnings (which become errors under-Wall -Werrorused by this project) and linker problems. These helpers are private toconference.cand should not be declared in the public header at all — move the forward declarations intoconference.c. Additionally,conf_handle_op_is declared twice on lines 358 and 361.
static void conf_handle_op_(pjmedia_conf* conf, struct conf_op* op);
static void conf_defer_port_destroy_(pjmedia_conf* conf, pjmedia_port* p);
static void conf_run_deferred_destroys_(pjmedia_conf* conf);
static void conf_handle_op_(pjmedia_conf* conf, struct conf_op* op);
pjmedia/include/pjmedia/conference.h:580
pjmedia_conf_replace_portis declared withPJ_DEFin a public header.PJ_DEFis intended for the definition in the .c file (it expands to the export/visibility attributes for the function body); using it on a declaration in a header will produce duplicate-symbol / redefinition issues for every translation unit that includes this header. UsePJ_DECL(pj_status_t)here, matching the declaration ofpjmedia_conf_replace_port_implimmediately above and the convention used elsewhere in this header (e.g.pjmedia_conf_add_port).
PJ_DEF(pj_status_t) pjmedia_conf_replace_port(pjmedia_conf *conf,
pj_pool_t *pool,
pjmedia_port *strm_port,
unsigned slot);
pjmedia/include/pjmedia/conference.h:575
pjmedia_conf_replace_port_implshould not be part of the public API — it is an internal helper executed from the conference op queue. Exposing it viaPJ_DECLinconference.hlets external callers invoke it directly on arbitrary threads, bypassing the queue/serialization that the PR is specifically introducing. Make itstaticinsideconference.cand remove this declaration.
PJ_DECL(pj_status_t) pjmedia_conf_replace_port_impl(pjmedia_conf *conf,
pj_pool_t *pool,
pjmedia_port *strm_port,
unsigned slot);
pjmedia/src/pjmedia/conference.c:846
- The mutex name passed to
pj_mutex_create_recursiveis being changed from "conf" to "conf_op", but this is still the mainconf->mutex(the new op-queue mutex isconf->op_mutex, a separate field that is never created in this PR — see the other comment). Renaming the existing mutex here is misleading for diagnostics and unrelated to the rest of the change. Please revert this rename and instead use "conf_op" as the name of the newly-introducedconf->op_mutex.
status = pj_mutex_create_recursive(pool, "conf_op", &conf->mutex);
pjsip/src/pjsua-lib/pjsua_aud.c:917
pjmedia_conf_replace_portis asynchronous — it queues the op and returnsPJ_SUCCESSbefore the swap actually executes on the conference tick. That means this fallback ("replace_port failed, falling back to add_port") can only trigger on early validation failures (bad slot, allocation failure) and will NOT catch any failure insidepjmedia_conf_replace_port_impl(resample create, buffer alloc, channel mismatch, etc.). If the deferred impl later fails the slot is silently left in a broken state andcall_med->strm.a.conf_slotstill points at the placeholder. Consider passing a result callback viaop->cbso the call-media layer can recover (e.g. remove the broken slot and add a fresh one) if the async replace fails.
if (call_med->strm.a.conf_slot != PJSUA_INVALID_ID) {
/* Slot pre-allocated with placeholder; replace it with real stream */
status = pjmedia_conf_replace_port(pjsua_var.mconf,
call->inv->pool,
call_med->strm.a.media_port,
(unsigned)call_med->strm.a.conf_slot);
if (status != PJ_SUCCESS) {
PJ_LOG(2, (THIS_FILE, "replace_port failed, falling back to add_port: %d", status));
pjsua_conf_remove_port(call_med->strm.a.conf_slot);
call_med->strm.a.conf_slot = PJSUA_INVALID_ID;
/* Fall through to add_port */
}
}
pjsip/src/pjsua-lib/pjsua_aud.c:815
- This block unconditionally pre-allocates a null placeholder port whenever
conf_slot == PJSUA_INVALID_ID, i.e. for every call that did not explicitly callpjsua_call_preallocate_conf_port(). That defeats the original control flow (which only added the real stream port to the conference once) and now creates a null port + adds a slot + immediately attempts to replace it on every audio channel update. This is wasted work and an extrapjmedia_conf_add_port/replace_portpair for callers that never wanted the early-slot feature. The placeholder should only be created when the caller has actually opted in (e.g. only keep thereplace_portpath whenconf_slotwas already pre-allocated, and fall through to plainadd_portotherwise), as the surrounding comment inpjsua_call_preallocate_conf_portalready implies.
/* Pre-allocate conference slot with null port for PJSIP 2.15 async conf.
* replace_port will swap it with the real stream later, keeping the same
* slot so switch connections (e.g. 2->4) route real audio.
*/
if (call_med->strm.a.conf_slot == PJSUA_INVALID_ID) {
pjmedia_port* null_port = NULL;
unsigned null_slot;
pj_status_t pre_status;
pj_str_t null_name = pj_str("call-null-placeholder");
unsigned clock_rate = si->fmt.clock_rate;
unsigned channel_count = si->fmt.channel_cnt;
unsigned samples_per_frame = PJMEDIA_SPF(clock_rate, 20000, channel_count);
pre_status = pjmedia_null_port_create(
call->inv->pool,
clock_rate,
channel_count,
samples_per_frame,
16,
&null_port);
if (pre_status == PJ_SUCCESS) {
pre_status = pjmedia_conf_add_port(pjsua_var.mconf,
call->inv->pool,
null_port,
&null_name,
&null_slot);
if (pre_status == PJ_SUCCESS) {
call_med->strm.a.conf_slot = (int)null_slot;
}
else {
pjmedia_port_destroy(null_port);
}
}
}
| return status; | ||
| } | ||
|
|
||
| #endif |
| typedef enum pjmedia_conf_op_type | ||
| { | ||
| PJMEDIA_CONF_OP_NONE = 0, | ||
| PJMEDIA_CONF_OP_ADD_PORT, | ||
| PJMEDIA_CONF_OP_REMOVE_PORT, | ||
| PJMEDIA_CONF_OP_REPLACE_PORT | ||
| } pjmedia_conf_op_type; | ||
|
|
||
| typedef void (*pjmedia_conf_op_result_cb)(pj_status_t status, | ||
| unsigned slot, | ||
| void* user_data); | ||
|
|
||
|
|
||
| //the structure to queue and process operations on its ports without blocking the main thread. | ||
| typedef struct conf_op { | ||
| PJ_DECL_LIST_MEMBER(struct conf_op); // to embeded in linked list | ||
| pjmedia_conf_op_type type; | ||
|
|
||
| unsigned slot; | ||
|
|
||
| /* for REPLACE */ | ||
| pjmedia_port* new_port; | ||
| pj_pool_t* new_port_pool; | ||
| pjmedia_conf_op_result_cb cb; | ||
| void* user_data; | ||
| pj_status_t rc; | ||
| } conf_op; | ||
|
|
||
| struct port_destroy_item { | ||
| PJ_DECL_LIST_MEMBER(struct port_destroy_item); | ||
| pjmedia_port* port; | ||
| }; | ||
|
|
||
|
|
||
| static void conf_handle_op_(pjmedia_conf* conf, struct conf_op* op); | ||
| static void conf_defer_port_destroy_(pjmedia_conf* conf, pjmedia_port* p); | ||
| static void conf_run_deferred_destroys_(pjmedia_conf* conf); | ||
| static void conf_handle_op_(pjmedia_conf* conf, struct conf_op* op); | ||
|
|
||
|
|
| conf_port->tx_buf_cap = conf_port->rx_buf_cap; | ||
| conf_port->tx_buf_count = 0; | ||
| conf_port->tx_buf = (pj_int16_t*) | ||
| pj_pool_alloc(pool, conf_port->tx_buf_cap * |
| pj_thread_t* thread = pj_thread_this(); | ||
| const char* thread_name = thread ? pj_thread_get_name(thread) : "unknown"; | ||
|
|
||
| PJ_LOG(4, (THIS_FILE, ">>> [%s] pjmedia_conf_replace_port START: slot=%d, port=%p", thread_name, slot, strm_port)); | ||
|
|
||
| PJ_ASSERT_RETURN(conf && pool && strm_port && slot < conf->max_ports, PJ_EINVAL); | ||
|
|
||
| if (!conf->ports[slot]) | ||
| return PJ_EINVAL; | ||
|
|
||
| op = PJ_POOL_ZALLOC_T(conf->pool, struct conf_op); | ||
| if (!op) { | ||
| return PJ_ENOMEM; | ||
| } | ||
|
|
||
| op->type = PJMEDIA_CONF_OP_REPLACE_PORT; | ||
| op->slot = slot; | ||
| op->new_port = strm_port; | ||
| op->new_port_pool = pool; | ||
|
|
||
| PJ_LOG(5, (THIS_FILE, " [%s] Attempting to lock op_mutex...", thread_name)); | ||
| pj_mutex_lock(conf->op_mutex); | ||
| PJ_LOG(5, (THIS_FILE, " [%s] op_mutex LOCKED", thread_name)); | ||
|
|
||
| pj_list_push_back(&conf->op_list, op); | ||
| conf->op_pending = PJ_TRUE; | ||
|
|
||
| pj_mutex_unlock(conf->op_mutex); | ||
| PJ_LOG(5, (THIS_FILE, " [%s] op_mutex UNLOCKED", thread_name)); |
|
|
||
|
|
| status = pjmedia_conf_add_port(pjsua_var.mconf, | ||
| call->inv ? call->inv->pool : pjsua_var.pool, | ||
| null_port, &null_name, &null_slot); | ||
| if (status != PJ_SUCCESS) { | ||
| pjmedia_port_destroy(null_port); | ||
| goto on_return; | ||
| } |
| /* | ||
| * Pre-allocate a null-port conference slot for a call that has no active | ||
| * media yet (PJSIP 2.15 defers conf_slot assignment until on_media_update). | ||
| * This allows SWI conference connections to be set up while the INVITE is | ||
| * still in progress, matching PJSIP 2.6 behaviour. |
| pj_mutex_t* op_mutex; /* protects op_list */ | ||
| pj_bool_t op_pending; /* flag to signal ops in queue */ | ||
|
|
||
| struct conf_op* op_list; | ||
|
|
||
| struct port_destroy_item* destroy_list; |
|
|
||
| //the structure to queue and process operations on its ports without blocking the main thread. | ||
| typedef struct conf_op { | ||
| PJ_DECL_LIST_MEMBER(struct conf_op); // to embeded in linked list |
| } | ||
| } | ||
|
|
||
| /* Initialize group lock for the port before detatching the old port */ |
…ala Silva <pesala.silva@ifs.com>Modifications_for_the_PR_Comments
|
@PesalaDeSilva , we have replied via email for discussions and feedback regarding this PR, so please check it as well |
…ions_for_the_PR_Comments
|
Hi @sauwming, Bug fixes:
Design fix:
Code quality:
Style:
Noted / intentionally deferred:
Please let me know if anything else needs to be addressed. I wanted to confirm — I believe I have not missed any email regarding this PR. If there was a specific email with additional feedback or discussion points that I may have overlooked, could you please resend it or summarize the key points here on the PR? I want to make sure I have addressed everything correctly. |
|
Here is @nanangizz's feedback sent via email dated 26 May: We understand the feature is important for your integration, and we
So yes, there are still significant missing gaps. |
|
Hi @nanangizz, thank you very much for the detailed feedback and for taking the time to review this PR. We fully understand and agree with the concerns raised — particularly around the async-op mechanism design and the null-port placeholder causing unnecessary object recreation on renegotiation. These are valid architectural issues that we were not fully aware of when implementing the feature. We are very glad to hear that the PJSIP team is considering implementing this natively. The outlined approach sounds much cleaner and more robust than our current implementation: One question, is there an estimated timeline for when this implementation might be available? Thank you again for considering this feature. We look forward to seeing the official implementation. |
|
@PesalaDeSilva We’ve started working on this. The usual steps are investigation, design, implementation, and review. We expect it to be available within a week or two. |
…iation Add an option to keep a call's conference bridge slot, its connections, and its per-slot settings (enable/disable/mute, level adjustment) across a media re-negotiation that recreates the audio stream, instead of removing the slot and adding a fresh one (which loses the slot id and all its state). Addresses the need behind #4583 / #4985. pjmedia: - New pjmedia_conf_detach_port(): clears conf_port->port only, keeping the slot, its connections and its tx/rx settings; moves slot-pool ownership off the old port's group lock so the caller can destroy the old port safely. - New pjmedia_conf_replace_port(): re-attaches a new port to a detached slot, rebuilding resamplers/buffers only when the format actually changed (an unchanged format is a pure pointer swap); preserves tx/rx settings; rejects a non-audio port and keeps the sample-rate/ptime compatibility check. - Both run via the existing async op queue (handle_op_queue), not a new one, and guard the read/mix path against a slot whose port is NULL (detached). - Implemented for both mixing backends: serial (conference.c) and parallel (conf_thread.c). The switchboard backend (conf_switch.c), which has no async op queue and does not mix/resample, returns PJ_ENOTSUP. pjsua: - pjsua_acc_config.preserve_conf_slot (default PJ_FALSE). - channel_update hooks the existing is_media_changed() gate: on changed media that stays active audio, detach+replace; media removed, deactivated, changed type, error, or hangup still uses remove (RFC 3264/6141 preserved). - Graceful fallback to the classic remove/add if detach/replace is unsupported (switchboard backend) or fails. pjsua2: - AccountMediaConfig.preserveConfSlot (default false), bridged to/from pjsua_acc_config and (de)serialized in read/writeObject. Test: pjmedia-test conf_test verifies connection, rx level, and tx mute all survive a detach/replace and audio flows on the new port; validated under both the serial and parallel backends. Follow-ups: switchboard-backend support if needed; a pjsua-level re-INVITE/hold regression; video conference. Co-Authored-By Claude Code
|
@PesalaDeSilva Please check+test #5125, it is the initial implementation. Let us know if it aligns with your needs, and feel free to join the review. |
…iation Add an option to keep a call's conference bridge slot, its connections, and its per-slot settings (enable/disable/mute, level adjustment) across a media re-negotiation that recreates the audio stream, instead of removing the slot and adding a fresh one (which loses the slot id and all its state). Addresses the need behind #4583 / #4985. pjmedia: - New pjmedia_conf_detach_port(): clears conf_port->port only, keeping the slot, its connections and its tx/rx settings; moves slot-pool ownership off the old port's group lock so the caller can destroy the old port safely. - New pjmedia_conf_replace_port(): re-attaches a new port to a detached slot, rebuilding resamplers/buffers only when the format actually changed (an unchanged format is a pure pointer swap); preserves tx/rx settings; rejects a non-audio port and keeps the sample-rate/ptime compatibility check. - Both run via the existing async op queue (handle_op_queue), not a new one, and guard the read/mix path against a slot whose port is NULL (detached). - Implemented for both mixing backends: serial (conference.c) and parallel (conf_thread.c). The switchboard backend (conf_switch.c), which has no async op queue and does not mix/resample, returns PJ_ENOTSUP. pjsua: - pjsua_acc_config.preserve_conf_slot (default PJ_FALSE). - channel_update hooks the existing is_media_changed() gate: on changed media that stays active audio, detach+replace; media removed, deactivated, changed type, error, or hangup still uses remove (RFC 3264/6141 preserved). - Graceful fallback to the classic remove/add if detach/replace is unsupported (switchboard backend) or fails. pjsua2: - AccountMediaConfig.preserveConfSlot (default false), bridged to/from pjsua_acc_config and (de)serialized in read/writeObject. Test: pjmedia-test conf_test verifies connection, rx level, and tx mute all survive a detach/replace and audio flows on the new port; validated under both the serial and parallel backends. Follow-ups: switchboard-backend support if needed; a pjsua-level re-INVITE/hold regression; video conference. Co-Authored-By Claude Code
…iation Add an option to keep a call's conference bridge slot, its connections, and its per-slot settings (enable/disable/mute, level adjustment) across a media re-negotiation that recreates the audio stream, instead of removing the slot and adding a fresh one (which loses the slot id and all its state). Addresses the need behind #4583 / #4985. pjmedia: - New pjmedia_conf_detach_port(): clears conf_port->port only, keeping the slot, its connections and its tx/rx settings; moves slot-pool ownership off the old port's group lock so the caller can destroy the old port safely. - New pjmedia_conf_replace_port(): re-attaches a new port to a detached slot, rebuilding resamplers/buffers only when the format actually changed (an unchanged format is a pure pointer swap); preserves tx/rx settings; rejects a non-audio port and keeps the sample-rate/ptime compatibility check. - Both run via the existing async op queue (handle_op_queue), not a new one, and guard the read/mix path against a slot whose port is NULL (detached). - Implemented for both mixing backends: serial (conference.c) and parallel (conf_thread.c). The switchboard backend (conf_switch.c), which has no async op queue and does not mix/resample, returns PJ_ENOTSUP. pjsua: - pjsua_acc_config.preserve_conf_slot (default PJ_FALSE). - channel_update hooks the existing is_media_changed() gate: on changed media that stays active audio, detach+replace; media removed, deactivated, changed type, error, or hangup still uses remove (RFC 3264/6141 preserved). - Graceful fallback to the classic remove/add if detach/replace is unsupported (switchboard backend) or fails. pjsua2: - AccountMediaConfig.preserveConfSlot (default false), bridged to/from pjsua_acc_config and (de)serialized in read/writeObject. Test: pjmedia-test conf_test verifies connection, rx level, and tx mute all survive a detach/replace and audio flows on the new port; validated under both the serial and parallel backends. Follow-ups: switchboard-backend support if needed; a pjsua-level re-INVITE/hold regression; video conference. Co-Authored-By Claude Code
…iation Add an option to keep a call's conference bridge slot, its connections, and its per-slot settings (enable/disable/mute, level adjustment) across a media re-negotiation that recreates the audio stream, instead of removing the slot and adding a fresh one (which loses the slot id and all its state). Addresses the need behind #4583 / #4985. pjmedia: - New pjmedia_conf_detach_port(): clears conf_port->port only, keeping the slot, its connections and its tx/rx settings; moves slot-pool ownership off the old port's group lock so the caller can destroy the old port safely. - New pjmedia_conf_replace_port(): re-attaches a new port to a detached slot, rebuilding resamplers/buffers only when the format actually changed (an unchanged format is a pure pointer swap); preserves tx/rx settings; rejects a non-audio port and keeps the sample-rate/ptime compatibility check. - Both run via the existing async op queue (handle_op_queue), not a new one, and guard the read/mix path against a slot whose port is NULL (detached). - Implemented for both mixing backends: serial (conference.c) and parallel (conf_thread.c). The switchboard backend (conf_switch.c), which has no async op queue and does not mix/resample, returns PJ_ENOTSUP. pjsua: - pjsua_acc_config.preserve_conf_slot (default PJ_FALSE). - channel_update hooks the existing is_media_changed() gate: on changed media that stays active audio, detach+replace; media removed, deactivated, changed type, error, or hangup still uses remove (RFC 3264/6141 preserved). - Graceful fallback to the classic remove/add if detach/replace is unsupported (switchboard backend) or fails. pjsua2: - AccountMediaConfig.preserveConfSlot (default false), bridged to/from pjsua_acc_config and (de)serialized in read/writeObject. Test: pjmedia-test conf_test verifies connection, rx level, and tx mute all survive a detach/replace and audio flows on the new port; validated under both the serial and parallel backends. Follow-ups: switchboard-backend support if needed; a pjsua-level re-INVITE/hold regression; video conference. Co-Authored-By Claude Code
Signed-off-by: Pesala Silva pesala.silva@ifs.com
Summary
Add optional early conference slot reservation for PJSUA calls (null-port
placeholder) and implement
pjmedia_conf_replace_portto swap that slot tothe real stream after SDP/stream setup, without performing the heavy work on
arbitrary threads. Queued operations run on the conference media tick; old
port destruction is deferred for safety.
Motivation
Some integrations need a stable
conf_slotID while the call is still beingset up (e.g. before media is fully active). PJSIP 2.15-era behaviour can defer
slot assignment until channel update; this patch preserves an early slot and
replaces the port in place when audio is ready.
Changes (high level)
pjmedia_conf_replace_port+ implementation, op queue, deferreddestroy list; small hardening in conf get/put frame paths.
pjsua_call_preallocate_conf_port();pjsua_aud_channel_updateuses replace when a placeholder slot exists, with add/remove fallback.