-
Notifications
You must be signed in to change notification settings - Fork 929
pjsua: Add audio play/rec commands to CLI framework #4723
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds CLI commands for dynamic audio playback and recording control during calls. The new commands (play_start, play_stop, rec_start, rec_stop) are available via the CLI framework (enabled with --use-cli) and support queuing operations before calls are established, with automatic activation when calls become active.
Key Changes
- New state management fields for dynamic playback/recording with one-shot semantics (attaches to a single call)
- CLI command handlers with queuing support that activates automatically when calls establish
- Automatic cleanup of playback/recording resources when the associated call ends
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| pjsip-apps/src/pjsua/pjsua_app_common.h | Adds state tracking fields for dynamic player and recorder (IDs, ports, call association, active flags, filenames) |
| pjsip-apps/src/pjsua/pjsua_app_config.c | Initializes new dynamic playback/recording state variables to invalid/inactive states |
| pjsip-apps/src/pjsua/pjsua_app_cli.c | Implements CLI command handlers and helper functions (start/stop for playback/recording), adds XML command definitions for the audio command group |
| pjsip-apps/src/pjsua/pjsua_app.c | Adds cleanup logic in call disconnect handler and queued activation logic in audio state callback |
| pj_ansi_strncpy(app_config.dyn_play_filename, filename, PJ_MAXPATH); | ||
| app_config.dyn_play_filename[PJ_MAXPATH-1] = '\0'; | ||
| app_config.dyn_player_active = PJ_TRUE; |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filename should be validated before being copied. There's no check to ensure the filename is not empty or NULL, which could lead to issues. Consider validating that the filename is not empty and is a valid file path before storing it and setting the active flag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The filename is already validated at the caller:
- argc < 2 check ensures filename argument is provided (not empty, not NULL)
- slen >= PJ_MAXPATH check ensures it's not too long
- pjsua_player_create() will also return error for invalid/empty filenames
The project doesn't validate file paths upfront - this is consistent with how --auto-play and other file operations work. The file is validated when pjsua_player_create() tries to open it via pjmedia_wav_player_port_create(). If the file doesn't exist or is invalid, an error is returned and logged ("Unable to open file for playback")
| /* Check if playback already active */ | ||
| if (app_config.dyn_player_active) { | ||
| PJ_LOG(3, (THIS_FILE, "Playback already active, stopping first...")); | ||
| stop_playback(); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When stop_playback fails on line 1399, the error status is not checked. If stop_playback returns an error, the function continues and may leave resources in an inconsistent state. Consider checking the return status and handling the error appropriately.
| stop_playback(); | |
| status = stop_playback(); | |
| if (status != PJ_SUCCESS) { | |
| PJ_PERROR(1, (THIS_FILE, status, | |
| "Failed to stop existing playback")); | |
| return status; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stop_playback() error is already logged internally (line 1521), and the state is always cleaned up regardless of the result. pjsua_player_destroy() can only fail on assertions (invalid ID or player already destroyed)
Returning early here would prevent the user from starting a new playback, I think showing the warning (already done) and continuing is the ok behavior. But if you think, it should fail, I can use suggested error processing.
| /* Check if recording already active */ | ||
| if (app_config.dyn_rec_active) { | ||
| PJ_LOG(3, (THIS_FILE, "Recording already active, stopping first...")); | ||
| stop_recording(); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When stop_recording fails, the error status is not checked, similar to the issue in start_playback. The function continues and may leave resources in an inconsistent state. Consider checking the return status and handling the error appropriately.
| stop_recording(); | |
| status = stop_recording(); | |
| if (status != PJ_SUCCESS) { | |
| PJ_PERROR(1,(THIS_FILE, status, | |
| "Error stopping current recording before starting " | |
| "dynamic recording")); | |
| return status; | |
| } |
| PJ_LOG(3, (THIS_FILE, "Dynamic recording auto-started")); | ||
| } else { | ||
| PJ_LOG(2, (THIS_FILE, "Failed to auto-start recording: %d", status)); | ||
| app_config.dyn_rec_active = PJ_FALSE; |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When auto-start of recording fails and dyn_rec_active is set to FALSE (line 500), the dyn_rec_filename is not cleared. This creates an inconsistent state. The filename should be cleared along with the active flag to maintain consistency.
| app_config.dyn_rec_active = PJ_FALSE; | |
| app_config.dyn_rec_active = PJ_FALSE; | |
| app_config.dyn_rec_filename[0] = '\0'; |
| /* Dynamic recording - start if queued */ | ||
| if (app_config.dyn_rec_active && | ||
| app_config.dyn_rec_id == PJSUA_INVALID_ID && | ||
| app_config.dyn_rec_filename[0] != '\0') | ||
| { | ||
| pj_status_t status; | ||
| pjsua_recorder_id rec_id; | ||
| pjsua_conf_port_id rec_port; | ||
| pj_str_t rec_file; | ||
|
|
||
| /* Create recorder */ | ||
| rec_file = pj_str(app_config.dyn_rec_filename); | ||
| status = pjsua_recorder_create(&rec_file, 0, NULL, 0, 0, &rec_id); | ||
| if (status == PJ_SUCCESS) { | ||
| rec_port = pjsua_recorder_get_conf_port(rec_id); | ||
|
|
||
| /* Connect call to recorder */ | ||
| pjsua_conf_connect(call_conf_slot, rec_port); | ||
|
|
||
| /* Connect microphone to recorder */ | ||
| pjsua_conf_connect(0, rec_port); | ||
|
|
||
| /* Save state */ | ||
| app_config.dyn_rec_id = rec_id; | ||
| app_config.dyn_rec_port = rec_port; | ||
| app_config.dyn_rec_call = ci->id; | ||
|
|
||
| PJ_LOG(3, (THIS_FILE, "Dynamic recording auto-started")); | ||
| } else { | ||
| PJ_LOG(2, (THIS_FILE, "Failed to auto-start recording: %d", status)); | ||
| app_config.dyn_rec_active = PJ_FALSE; | ||
| } |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code for creating and connecting the recorder is duplicated from start_recording in pjsua_app_cli.c (lines 1608-1634). Consider extracting this logic into a shared helper function to reduce code duplication and improve maintainability.
| pj_ansi_strncpy(app_config.dyn_rec_filename, filename, PJ_MAXPATH); | ||
| app_config.dyn_rec_filename[PJ_MAXPATH-1] = '\0'; | ||
| app_config.dyn_rec_active = PJ_TRUE; |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The same validation and consistency issue exists here as in start_playback. The filename should be validated before being copied, and there's no check to ensure it's not empty or NULL.
| /* Get call info */ | ||
| status = pjsua_call_get_info(call_id, &call_info); | ||
| if (status != PJ_SUCCESS) { | ||
| PJ_LOG(2, (THIS_FILE, "Failed to get call info")); |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After get_call_info fails and returns an error status, the function leaves dyn_rec_active set to TRUE and dyn_rec_filename populated, creating an inconsistent state. The function should reset these fields before returning an error to maintain consistency.
| PJ_LOG(2, (THIS_FILE, "Failed to get call info")); | |
| PJ_LOG(2, (THIS_FILE, "Failed to get call info")); | |
| /* Reset dynamic recording state on failure to maintain consistency */ | |
| app_config.dyn_rec_active = PJ_FALSE; | |
| app_config.dyn_rec_filename[0] = '\0'; |
| connect_sound = PJ_FALSE; | ||
| } else { | ||
| PJ_LOG(2, (THIS_FILE, "Failed to auto-start playback: %d", status)); | ||
| app_config.dyn_player_active = PJ_FALSE; |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When auto-start of playback fails and dyn_player_active is set to FALSE (line 550), the dyn_play_filename is not cleared. This creates an inconsistent state similar to the recording case. The filename should be cleared along with the active flag to maintain consistency.
| app_config.dyn_player_active = PJ_FALSE; | |
| app_config.dyn_player_active = PJ_FALSE; | |
| app_config.dyn_play_filename[0] = '\0'; |
| /* Dynamic playback - start if queued */ | ||
| if (app_config.dyn_player_active && | ||
| app_config.dyn_player_id == PJSUA_INVALID_ID && | ||
| app_config.dyn_play_filename[0] != '\0') | ||
| { | ||
| pj_status_t status; | ||
| pjsua_player_id player_id; | ||
| pjsua_conf_port_id player_port; | ||
| pj_str_t play_file; | ||
|
|
||
| /* Create player */ | ||
| play_file = pj_str(app_config.dyn_play_filename); | ||
| status = pjsua_player_create(&play_file, 0, &player_id); | ||
| if (status == PJ_SUCCESS) { | ||
| player_port = pjsua_player_get_conf_port(player_id); | ||
|
|
||
| /* Connect player to call */ | ||
| pjsua_conf_connect(player_port, call_conf_slot); | ||
|
|
||
| /* Disconnect microphone (like auto-play) */ | ||
| pjsua_conf_disconnect(0, call_conf_slot); | ||
|
|
||
| /* Save state */ | ||
| app_config.dyn_player_id = player_id; | ||
| app_config.dyn_player_port = player_port; | ||
| app_config.dyn_player_call = ci->id; | ||
|
|
||
| PJ_LOG(3, (THIS_FILE, "Dynamic playback auto-started")); | ||
| connect_sound = PJ_FALSE; | ||
| } else { | ||
| PJ_LOG(2, (THIS_FILE, "Failed to auto-start playback: %d", status)); | ||
| app_config.dyn_player_active = PJ_FALSE; | ||
| } |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code for creating and connecting the player is duplicated from start_playback in pjsua_app_cli.c (lines 1446-1468). Consider extracting this logic into a shared helper function to reduce code duplication and improve maintainability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pjsua_app.c (callback context) already has call_conf_slot and ci->id available from callback, has no intermediate error checking for conf_connect
pjsua_app_cli.c (CLI command) must find and validate call/media slots itself, error handling with cleanup at each step, returns status to caller
A shared helper would require passing many parameters (call_id, call_conf_slot, filename, etc.), handling different error processing.
Don't you think that it will add more complexity?
| /* Connect microphone to recorder */ | ||
| status = pjsua_conf_connect(0, rec_port); | ||
| if (status != PJ_SUCCESS) { | ||
| PJ_LOG(2, (THIS_FILE, "Failed to connect mic to recorder: %d", status)); | ||
| /* Continue anyway, we at least have call audio */ | ||
| } | ||
|
|
||
| /* Save state */ | ||
| app_config.dyn_rec_id = rec_id; | ||
| app_config.dyn_rec_port = rec_port; | ||
| app_config.dyn_rec_call = call_id; | ||
|
|
||
| PJ_LOG(3, (THIS_FILE, "Recording started: %s", filename)); | ||
| return PJ_SUCCESS; |
Copilot
AI
Dec 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When connecting the microphone to the recorder fails on line 1630, the function continues and saves the recorder state including rec_port and rec_id. However, only the call audio is connected, not the microphone. Later, in stop_recording (line 1683), the code unconditionally disconnects the microphone from rec_port, which may fail or be unnecessary since it was never successfully connected. Consider tracking whether the microphone was successfully connected to avoid unnecessary disconnect operations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disconnect of a never-connected port returns PJ_EINVAL but is harmless - no crash or resource leak. However, if you prefer the explicit tracking, I can add some flag.
|
Just an idea. As the new command only affects a specific call (i.e: current or next call), perhaps the command should start with
This will improve the feature to be able to accurately target a specific call. Also, in the future it may be improved further by allowing WAV file playback in multiple calls concurrently ( |
|
I like the idea with specifying |
I think we should leave the mic and any other connections as they are? |
Summary
Add commands to dynamically start/stop audio file playback and recording during calls. These commands are available in the CLI framework (enabled with
--use-clioption) via telnet front-end.These commands complement the existing
--auto-playand--auto-recstartup options.Note: These commands are not added to the legacy console menu.
New Commands
Under the
audiocommand group:audio play_start <filename>- Start playing a WAV file to the current callaudio play_stop- Stop playback and reconnect microphoneaudio rec_start <filename>- Start recording the call to a fileaudio rec_stop- Stop recordingFeatures
--auto-play)--auto-play/--auto-recstartup options are active (returns error)Behavior with Multiple Calls
These commands use one-shot semantics, unlike
--auto-play/--auto-recwhich persist across all calls: