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. |
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.