]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
ARI: The bridges play and record APIs now handle sample rates > 8K correctly.
authorGeorge Joseph <gjoseph@sangoma.com>
Thu, 25 Sep 2025 17:48:01 +0000 (11:48 -0600)
committerGeorge Joseph <gjoseph@sangoma.com>
Tue, 30 Sep 2025 13:59:27 +0000 (13:59 +0000)
The bridge play and record APIs were forcing the Announcer/Recorder channel
to slin8 which meant that if you played or recorded audio with a sample
rate > 8K, it was downsampled to 8K limiting the bandwidth.

* The /bridges/play REST APIs have a new "announcer_format" parameter that
  allows the caller to explicitly set the format on the "Announcer" channel
  through which the audio is played into the bridge.  If not specified, the
  default depends on how many channels are currently in the bridge.  If
  a single channel is in the bridge, then the Announcer channel's format
  will be set to the same as that channel's.  If multiple channels are in the
  bridge, the channels will be scanned to find the one with the highest
  sample rate and the Announcer channel's format will be set to the slin
  format that has an equal to or greater than sample rate.

* The /bridges/record REST API has a new "recorder_format" parameter that
  allows the caller to explicitly set the format on the "Recorder" channel
  from which audio is retrieved to write to the file.  If not specified,
  the Recorder channel's format will be set to the format that was requested
  to save the audio in.

Resolves: #1479

DeveloperNote: The ARI /bridges/play and /bridges/record REST APIs have new
parameters that allow the caller to specify the format to be used on the
"Announcer" and "Recorder" channels respecitvely.

res/ari/resource_bridges.c
res/ari/resource_bridges.h
res/res_ari_bridges.c
rest-api/api-docs/bridges.json

index fafe03aa582b83454d48e68cc8f4515ab60547d0..6d77aa5efd57837fc070c0f10a6382b0aaf3d7b7 100644 (file)
@@ -311,7 +311,8 @@ static void *bridge_channel_control_thread(void *data)
        return NULL;
 }
 
-static struct ast_channel *prepare_bridge_media_channel(const char *type)
+static struct ast_channel *prepare_bridge_media_channel(const char *type,
+       struct ast_format *channel_format)
 {
        RAII_VAR(struct ast_format_cap *, cap, NULL, ao2_cleanup);
        struct ast_channel *chan;
@@ -321,7 +322,8 @@ static struct ast_channel *prepare_bridge_media_channel(const char *type)
                return NULL;
        }
 
-       ast_format_cap_append(cap, ast_format_slin, 0);
+       /* This bumps the format's refcount */
+       ast_format_cap_append(cap, channel_format, 0);
 
        chan = ast_request(type, cap, NULL, NULL, "ARI", NULL);
        if (!chan) {
@@ -407,6 +409,7 @@ static int ari_bridges_play_helper(const char **args_media,
 
 static void ari_bridges_play_new(const char **args_media,
        size_t args_media_count,
+       const char *args_format,
        const char *args_lang,
        int args_offset_ms,
        int args_skipms,
@@ -424,14 +427,64 @@ static void ari_bridges_play_new(const char **args_media,
        struct stasis_topic *bridge_topic;
        struct bridge_channel_control_thread_data *thread_data;
        pthread_t threadid;
+       struct ast_format *channel_format = NULL;
 
        struct ast_frame prog = {
                .frametype = AST_FRAME_CONTROL,
                .subclass.integer = AST_CONTROL_PROGRESS,
        };
 
+       /*
+        * Determine the format for the playback channel.
+        * If a format was specified, use that if it's valid.
+        * Otherwise, if the bridge is empty, use slin.
+        * If the bridge has one channel, use that channel's raw write format.
+        * If the bridge has multiple channels, use the slin format that
+        * will handle the highest sample rate of the raw write format of all the channels.
+        */
+       if (!ast_strlen_zero(args_format)) {
+               channel_format = ast_format_cache_get(args_format);
+               if (!channel_format) {
+                       ast_ari_response_error(
+                               response, 422, "Unprocessable Entity",
+                               "specified announcer_format is unknown on this system");
+                       return;
+               }
+               /*
+                * ast_format_cache_get() bumps the refcount but the other calls
+                * to retrieve formats don't so we'll drop this reference.
+                * It'll be bumped again in the prepare_bridge_media_channel() call below.
+                */
+               ao2_ref(channel_format, -1);
+       } else {
+               ast_bridge_lock(bridge);
+               if (bridge->num_channels == 0) {
+                       channel_format = ast_format_slin;
+               } else if (bridge->num_channels == 1) {
+                       struct ast_bridge_channel *bc = NULL;
+                       bc = AST_LIST_FIRST(&bridge->channels);
+                       if (bc) {
+                               channel_format = ast_channel_rawwriteformat(bc->chan);
+                       }
+               } else {
+                       struct ast_bridge_channel *bc = NULL;
+                       unsigned int max_sample_rate = 0;
+                       AST_LIST_TRAVERSE(&bridge->channels, bc, entry) {
+                               struct ast_format *fmt = ast_channel_rawwriteformat(bc->chan);
+                               max_sample_rate = MAX(ast_format_get_sample_rate(fmt), max_sample_rate);
+                       }
+                       channel_format = ast_format_cache_get_slin_by_rate(max_sample_rate);
+               }
+               ast_bridge_unlock(bridge);
+       }
+
+       if (!channel_format) {
+               channel_format = ast_format_slin;
+       }
 
-       if (!(play_channel = prepare_bridge_media_channel("Announcer"))) {
+       play_channel = prepare_bridge_media_channel("Announcer", channel_format);
+       ao2_cleanup(channel_format);
+       if (!play_channel) {
                ast_ari_response_error(
                        response, 500, "Internal Error", "Could not create playback channel");
                return;
@@ -578,6 +631,7 @@ static void ari_bridges_handle_play(
        const char *args_bridge_id,
        const char **args_media,
        size_t args_media_count,
+       const char *args_format,
        const char *args_lang,
        int args_offset_ms,
        int args_skipms,
@@ -608,7 +662,7 @@ static void ari_bridges_handle_play(
                return;
        }
 
-       ari_bridges_play_new(args_media, args_media_count, args_lang, args_offset_ms,
+       ari_bridges_play_new(args_media, args_media_count, args_format, args_lang, args_offset_ms,
                args_skipms, args_playback_id, response, bridge);
 }
 
@@ -620,6 +674,7 @@ void ast_ari_bridges_play(struct ast_variable *headers,
        ari_bridges_handle_play(args->bridge_id,
        args->media,
        args->media_count,
+       args->announcer_format,
        args->lang,
        args->offsetms,
        args->skipms,
@@ -634,6 +689,7 @@ void ast_ari_bridges_play_with_id(struct ast_variable *headers,
        ari_bridges_handle_play(args->bridge_id,
        args->media,
        args->media_count,
+       args->announcer_format,
        args->lang,
        args->offsetms,
        args->skipms,
@@ -660,6 +716,8 @@ void ast_ari_bridges_record(struct ast_variable *headers,
        size_t uri_name_maxlen;
        struct bridge_channel_control_thread_data *thread_data;
        pthread_t threadid;
+       struct ast_format *file_format = NULL;
+       struct ast_format *channel_format = NULL;
 
        ast_assert(response != NULL);
 
@@ -667,7 +725,34 @@ void ast_ari_bridges_record(struct ast_variable *headers,
                return;
        }
 
-       if (!(record_channel = prepare_bridge_media_channel("Recorder"))) {
+       file_format = ast_get_format_for_file_ext(args->format);
+       if (!file_format) {
+               ast_ari_response_error(
+                       response, 422, "Unprocessable Entity",
+                       "specified format is unknown on this system");
+               return;
+       }
+
+       if (!ast_strlen_zero(args->recorder_format)) {
+               channel_format = ast_format_cache_get(args->recorder_format);
+               if (!channel_format) {
+                       ast_ari_response_error(
+                               response, 422, "Unprocessable Entity",
+                               "specified recorder_format is unknown on this system");
+                       return;
+               }
+               /*
+                * ast_format_cache_get() bumps the refcount but the other calls
+                * to retrieve formats don't so we'll drop this reference.
+                * It'll be bumped again in the prepare_bridge_media_channel() call below.
+                */
+               ao2_ref(channel_format, -1);
+
+       } else {
+               channel_format = file_format;
+       }
+
+       if (!(record_channel = prepare_bridge_media_channel("Recorder", channel_format))) {
                ast_ari_response_error(
                        response, 500, "Internal Server Error", "Failed to create recording channel");
                return;
@@ -728,13 +813,6 @@ void ast_ari_bridges_record(struct ast_variable *headers,
                return;
        }
 
-       if (!ast_get_format_for_file_ext(options->format)) {
-               ast_ari_response_error(
-                       response, 422, "Unprocessable Entity",
-                       "specified format is unknown on this system");
-               return;
-       }
-
        recording = stasis_app_control_record(control, options);
        if (recording == NULL) {
                switch(errno) {
index d494d89008597387804a1c8c0fa1f94fba45aa3d..068ac28d60a518c600d13ec201c55d13bb55b57b 100644 (file)
@@ -285,6 +285,8 @@ struct ast_ari_bridges_play_args {
        size_t media_count;
        /*! Parsing context for media. */
        char *media_parse;
+       /*! Format of the 'Anouncer' channel attached to the bridge. Defaults to the format of the channel in the bridge with the highest sampe rate. */
+       const char *announcer_format;
        /*! For sounds, selects language for sound. */
        const char *lang;
        /*! Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified. */
@@ -327,6 +329,8 @@ struct ast_ari_bridges_play_with_id_args {
        size_t media_count;
        /*! Parsing context for media. */
        char *media_parse;
+       /*! Format of the 'Anouncer' channel attached to the bridge. Defaults to the format of the channel in the bridge with the highest sampe rate. */
+       const char *announcer_format;
        /*! For sounds, selects language for sound. */
        const char *lang;
        /*! Number of milliseconds to skip before playing. Only applies to the first URI if multiple media URIs are specified. */
@@ -363,6 +367,8 @@ struct ast_ari_bridges_record_args {
        const char *name;
        /*! Format to encode audio in */
        const char *format;
+       /*! Format of the 'Recorder' channel attached to the bridge. Defaults to the same format as the 'format' parameter. */
+       const char *recorder_format;
        /*! Maximum duration of the recording, in seconds. 0 for no limit. */
        int max_duration_seconds;
        /*! Maximum duration of silence, in seconds. 0 for no limit. */
index ba649a6ded2a4f9bc4344ee85b4e1b97599310fa..465f6e947a35f3c02370cf4e00f5c09a51bf69fb 100644 (file)
@@ -1044,6 +1044,10 @@ int ast_ari_bridges_play_parse_body(
                        args->media[0] = ast_json_string_get(field);
                }
        }
+       field = ast_json_object_get(body, "announcer_format");
+       if (field) {
+               args->announcer_format = ast_json_string_get(field);
+       }
        field = ast_json_object_get(body, "lang");
        if (field) {
                args->lang = ast_json_string_get(field);
@@ -1128,6 +1132,9 @@ static void ast_ari_bridges_play_cb(
                                args.media[j] = (vals[j]);
                        }
                } else
+               if (strcmp(i->name, "announcer_format") == 0) {
+                       args.announcer_format = (i->value);
+               } else
                if (strcmp(i->name, "lang") == 0) {
                        args.lang = (i->value);
                } else
@@ -1164,6 +1171,7 @@ static void ast_ari_bridges_play_cb(
        case 501: /* Not Implemented */
        case 404: /* Bridge not found */
        case 409: /* Bridge not in a Stasis application */
+       case 422: /* The format specified is unknown on this system */
                is_valid = 1;
                break;
        default:
@@ -1223,6 +1231,10 @@ int ast_ari_bridges_play_with_id_parse_body(
                        args->media[0] = ast_json_string_get(field);
                }
        }
+       field = ast_json_object_get(body, "announcer_format");
+       if (field) {
+               args->announcer_format = ast_json_string_get(field);
+       }
        field = ast_json_object_get(body, "lang");
        if (field) {
                args->lang = ast_json_string_get(field);
@@ -1303,6 +1315,9 @@ static void ast_ari_bridges_play_with_id_cb(
                                args.media[j] = (vals[j]);
                        }
                } else
+               if (strcmp(i->name, "announcer_format") == 0) {
+                       args.announcer_format = (i->value);
+               } else
                if (strcmp(i->name, "lang") == 0) {
                        args.lang = (i->value);
                } else
@@ -1339,6 +1354,7 @@ static void ast_ari_bridges_play_with_id_cb(
        case 501: /* Not Implemented */
        case 404: /* Bridge not found */
        case 409: /* Bridge not in a Stasis application */
+       case 422: /* The format specified is unknown on this system */
                is_valid = 1;
                break;
        default:
@@ -1377,6 +1393,10 @@ int ast_ari_bridges_record_parse_body(
        if (field) {
                args->format = ast_json_string_get(field);
        }
+       field = ast_json_object_get(body, "recorder_format");
+       if (field) {
+               args->recorder_format = ast_json_string_get(field);
+       }
        field = ast_json_object_get(body, "maxDurationSeconds");
        if (field) {
                args->max_duration_seconds = ast_json_integer_get(field);
@@ -1428,6 +1448,9 @@ static void ast_ari_bridges_record_cb(
                if (strcmp(i->name, "format") == 0) {
                        args.format = (i->value);
                } else
+               if (strcmp(i->name, "recorder_format") == 0) {
+                       args.recorder_format = (i->value);
+               } else
                if (strcmp(i->name, "maxDurationSeconds") == 0) {
                        args.max_duration_seconds = atoi(i->value);
                } else
index 4cd15b979444f37ddc14c552ec5092b198fb8d2f..ae5ee56fec317dc7fd040f02166663a865ffa09f 100644 (file)
                                                        "allowMultiple": true,
                                                        "dataType": "string"
                                                },
+                                               {
+                                                       "name": "announcer_format",
+                                                       "description": "Format of the 'Anouncer' channel attached to the bridge. Defaults to the format of the channel in the bridge with the highest sampe rate.",
+                                                       "paramType": "query",
+                                                       "required": false,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               },
                                                {
                                                        "name": "lang",
                                                        "description": "For sounds, selects language for sound.",
                                                                "valueType": "RANGE",
                                                                "min": 0
                                                        }
-
                                                },
                                                {
                                                        "name": "skipms",
                                                {
                                                        "code": 409,
                                                        "reason": "Bridge not in a Stasis application"
+                                               },
+                                               {
+                                                       "code": 422,
+                                                       "reason": "The format specified is unknown on this system"
                                                }
                                        ]
                                }
                                                        "allowMultiple": true,
                                                        "dataType": "string"
                                                },
+                                               {
+                                                       "name": "announcer_format",
+                                                       "description": "Format of the 'Anouncer' channel attached to the bridge. Defaults to the format of the channel in the bridge with the highest sampe rate.",
+                                                       "paramType": "query",
+                                                       "required": false,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               },
                                                {
                                                        "name": "lang",
                                                        "description": "For sounds, selects language for sound.",
                                                {
                                                        "code": 409,
                                                        "reason": "Bridge not in a Stasis application"
+                                               },
+                                               {
+                                                       "code": 422,
+                                                       "reason": "The format specified is unknown on this system"
                                                }
                                        ]
 
                                                        "allowMultiple": false,
                                                        "dataType": "string"
                                                },
+                                               {
+                                                       "name": "recorder_format",
+                                                       "description": "Format of the 'Recorder' channel attached to the bridge. Defaults to the same format as the 'format' parameter.",
+                                                       "paramType": "query",
+                                                       "required": false,
+                                                       "allowMultiple": false,
+                                                       "dataType": "string"
+                                               },
                                                {
                                                        "name": "maxDurationSeconds",
                                                        "description": "Maximum duration of the recording, in seconds. 0 for no limit.",