Asterisk skips local channel bridges - how to prevent?

I have a setup where an application (run using ARI) connects to external UAs using a set of local channels and bridges - each application “dial” looks like this:

  1. Set up local channel.
  2. Set up a mixing bridge.
  3. Put local channel in mixing bridge.
  4. Create a new channel and originate to the destination.
  5. Put outgoing channel in mixing bridge.

Then I can do various things to the local channel - play audio, capture DTMF, etc. One of the main reason for the mixing bridge is that I often want to record this session and bridge recording offers recording of all sides - unlike channel recording.

This works well, until I want to put the local channel into another mixing bridge to create a conference (these kinds of use cases are ad-hoc - I first setup the local+bridge+originate then I do other stuff and at some point I might decide to conference this setup with other setups of a similar nature.

Whenever I put the local channel into such a conference mixing bridge, Asterisk appears to move the originating channel into the conference bridge, leaving the original mixing bridge. If I have a recording running on the original mixing bridge, it gets everything up until when I join the local channel to the conference bridge, and then nothing.

This is how it looks in the Asterisk console:

    -- Channel Local/application-outbound@default-00000002;1 joined 'simple_bridge' stasis-bridge <1fdbcbc0-2336-4322-a25c-0e54c1304778>
  == Using SIP RTP CoS mark 5
    -- Channel SIP/remote-entity-00000001 joined 'simple_bridge' stasis-bridge <1fdbcbc0-2336-4322-a25c-0e54c1304778>
    -- Channel Recorder/ARI-00000003;2 joined 'simple_bridge' stasis-bridge <1fdbcbc0-2336-4322-a25c-0e54c1304778>
    -- x=0, open writing:  /var/spool/asterisk/recording/5c07c7d9-ef5b-45d1-8a44-df9619b5a5d9 format: sln, 0x7f144c003780
    -- Channel SIP/remote-entity-00000001 left 'softmix' stasis-bridge <1fdbcbc0-2336-4322-a25c-0e54c1304778>
    -- Channel SIP/remote-entity-00000001 joined 'simple_bridge' stasis-bridge <1_myconf-call-1-94200919-d2aa-4091-8400-c9f9fae70ffa>

Is this the expected behavior? I read about Local Channel Modifiers and tried to add the “No Release” modifier but that doesn’t seem to do anything to change this behavior.

Any idea what I should do?

Your description isn’t detailed enough with channel names (remember: Local channels are actually comprised of 2 channels, so you have to be specific about which one you are referring to) to be able to answer with certainty but in general you can’t put a single channel in multiple bridges at the same time. If you are adding a channel that is in one bridge (be it a Local channel or another channel) to another bridge, then it moves.

I think I also found an issue in my code regarding what gets moved to the conference bridge, but can you please explain more about the dual-channel nature of local channels? I was assuming that I can use a local channel to connect two bridges - is that possible or not?

You can, but when you originate a Local channel it creates two channels. A “;1” and a “;2”. They are independently controlled but linked. If media goes into “;1” then it pops out of “;2” and vice versa.

This is because a single channel can’t be in two places at once, so Local channels create two.

To connect two bridges you would have the “;1” Local channel in one bridge, and the “;2” Local channel in another.

Thank you - my code was definitely not correct. But when I try to create a local channel with otherChannelId, I cannot operate on the second channel as it isn’t in a stasis application (the first local channel is in stasis as I’m setting app correctly) - so I can’t put the “;2” channel into my bridge - I get " Channel not in Stasis application" error.

Am I missing something? How can I force the other channel into stasis?

You have to direct it to a location in the dialplan that will then enter your ARI application so you have control.

I tried to do that, but the “other channel” doesn’t seem to go into dial plan:

My extensions.conf has this:

[application-outbound]
exten =>  h,1,Noop(**** myapp hangup for CLID: ${CALLERID(num)} ****)
 same =>    n,Goto(myapp-termination,${EXTEN},1)

exten => _.,1,Noop(**** Activating myapp for CLID: ${CALLERID(num)} ****)
 same =>    n,Stasis(myapp)
 same =>    n,Hangup()

I start my local channel with the endpoint Local/application-outbound@default, and wait for StasisStart event on the “other channel” id - but it never happens. The neither the verbose log nor the debug log shows any dialplan events happening - it makes sense for the ;1 channel as I specified my stasis app as one of the required parameters - so it goes directly to stasis and does not pass Go^H^Hdialplan, but the ;2 channel doesn’t appear to run the dialplan either but also doesn’t enter stasis directly. It is lost in limbo?

I’ve just now noticed that I’m confusing the order of extension and context in the endpoint. I’m fixing and re-checking.

This is asking for trouble as the Hangup line will also match the h extension, so will be added after the Goto. Whilst the Goto means it will never be reached, you might change things.

I’d suggest using Local/stasis@application-outbound or Local/myapp@application-outbound, and using and explicit extension name, not a wildcard.

Why would the Hangup() be added after the Goto()? the Goto() is at h,2 while the Hangup() is at _.,3 - AFAIK, no _. action will ever be run for a hangup event because the h already matched.

Because _. matches h and because there is no h,3, it is the most specific match to h.3. There are several cases where it is actually useful to have most priorities matching lots of things, but with some being more specific. I’ve used it with caller ID matches.

OK, thanks. I was not aware of that - the documentation mentions that specifically, but I didn’t RTFM :-/

Ok, after trying a few things, and none works - here’s the rub:

  1. I have a context that is known to work for entering a channel into stasis - I use it for all my “SIP to stasis” workflows and it works great. It matches on _. as I’ve shown previously, so I don’t think there should be an extension issue but even if there is - from my reading of the documentation I understand that the channel will be disconnected if that happens.
  2. I’m creating a local channel to the endpoint Local/some-extension@application-outbound/n (application-outbound is the context that calls Stasis()). Here’s the ARI debug log for that request:
<--- ARI request received from: 127.0.0.1:57076 --->
content-type: application/json
content-length: 18
host: 127.0.0.1
connection: close
authorization: Basic XXXXXXXXXXXXXXXX
endpoint: Local/some-extension@application-outbound/n
app: myapp
channelId: 948f71ff-b0c5-41ea-a292-e60d06ea5d98
otherChannelId: 2f323d0e-5463-4c96-82eb-1aeceddac254
body:
{
  "variables": null
}
  1. The only ARI events I get for the corresponding ;2 channel (with ID 2f323d0e-5463-4c96-82eb-1aeceddac254) are these:
<--- Sending ARI event to 127.0.0.1:55240 --->
{
  "type": "ChannelCreated",
  "timestamp": "2021-09-11T16:11:23.158+0000",
  "channel": {
    "id": "2f323d0e-5463-4c96-82eb-1aeceddac254",
    "name": "Local/some-extension@application-outbound-00000004;2",
    "state": "Ring",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "",
      "app_data": ""
    },
    "creationtime": "2021-09-11T16:11:23.158+0000",
    "language": "en"
  },
  "asterisk_id": "02:0c:f8:8e:2b:88",
  "application": "myapp"
}

...

<--- Sending ARI event to 127.0.0.1:55240 --->
{
  "type": "ChannelDestroyed",
  "timestamp": "2021-09-11T16:11:38.109+0000",
  "cause": 0,
  "cause_txt": "Unknown",
  "channel": {
    "id": "2f323d0e-5463-4c96-82eb-1aeceddac254",
    "name": "Local/some-extension@application-outbound-00000004;2",
    "state": "Ring",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "",
      "app_data": ""
    },
    "creationtime": "2021-09-11T16:11:23.158+0000",
    "language": "en"
  },
  "asterisk_id": "02:0c:f8:8e:2b:88",
  "application": "myapp"
}

(the “destroyed” event gets sent after everything is done - i.e. the application tries to add it to a bridge, fails and shuts down everything).

  1. No dial plan events are ever sent (for either channel), and 2f323d0e-5463-4c96-82eb-1aeceddac254 never sends a StasisStart event (which I do get for 948f71ff-b0c5-41ea-a292-e60d06ea5d98 - the ;1 local channel, and for every SIP channel I create).
  2. Listing the existing channel (while the process hasn’t failed yet), I can see both channels, one in “Down” state (which I expect for a new channel) and one in “Ring” state:

Response for /channels request:

<--- Sending ARI response to 127.0.0.1:57274 --->
200 OK
Content-type: application/json
[
  {
    "id": "2f323d0e-5463-4c96-82eb-1aeceddac254",
    "name": "Local/some-extension@application-outbound-00000004;2",
    "state": "Ring",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "",
      "app_data": ""
    },
    "creationtime": "2021-09-11T16:11:23.158+0000",
    "language": "en"
  },
  {
    "id": "948f71ff-b0c5-41ea-a292-e60d06ea5d98",
    "name": "Local/some-extension@application-outbound-00000004;1",
    "state": "Down",
    "caller": {
      "name": "",
      "number": "1234"
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "Stasis",
      "app_data": "myapp"
    },
    "creationtime": "2021-09-11T16:11:23.153+0000",
    "language": "en"
  }
]

I don’t know why ;2 is in “Ring” state, but it appears to be on purpose according to local_request_with_stream_topology().

Which begs the question - does anybody here actually uses Local channels with ARI? I took a look at the code for ast_ari_channels_create() and it looks like it tries to register two channels with stasis, one is the first channel created by the selected technology and the other is the “originator” - an optional field that I do not provide. At no point that code looks at the “other channel” - in fact local_request_with_stream_topology() drops any reference to it immediately after it is created.

That’s the normals starting state for an incoming channel. If you want to change the state, you have to answer it.

Can you explain why it makes sense to have one side of the local channel in “down” state and the “other” in “ring” state? And what is the purpose of the “originator” field?

I think I’m missing a lot of context about what workflow was envisioned for how people would use channels/create and “Local” channels.

I should have said incoming.

Firstly ARI is new, and the normal way of using would be to use Dial from an incoming, real , channel. That incoming channel might not have been answered yet, so the ;1 side of the local channel needs to start in an unanswered state. The ;2 side looks like an incoming call to the dialplan, so also has to start in an answered state.

Local channels also get used as the channel side of an originate, to do some setup work before calling the underlying channel. You don’t want the ;1 side to be up until underlying channel is answered, because, for example, the application might be playback, and you don’t want that running whilst the called channel is still ringing.

I’m not sure whether or not it would surprise me if anyone was using local channels with ARI. My personal view is that ARI is overused and a lot of what people attempt with it would be better done with dialplan and, maybe, AMI or AGI, and that operations that require local channels are more likely to be suited to dialplan based designs, but the number of people asking about designs which use ARI for third party control and don’t have any dial plan means there may well be people using local channels.

The original ARI concept seems to have been closer to first party control, where the dialplan did the donkey work, and app_stasis and ARI was used to create custom dialplan functions.

I can confidently say that yes, Local channels are heavily used with ARI every day. Is it used in the specific way you’re using it? I don’t know, but Local channels do work with ARI. Local channels themselves are just hard to grasp. As David said, you’d need to answer the “;2” side.

While they are inherently linked it’s best to think of them separately as an outgoing call and an incoming call.

If you dial an outgoing PJSIP channel is it automatically immediately answered? No. Same for Local channel.
If a PJSIP channel dials into Asterisk is it automatically immediately answered? No. Same for Local channel.

The “originator” field is unrelated to Local channels, and is used to associate an originated call with a dialing channel for CDR/ID purposes.

Its interesting that you say that, as at least ARI channels/create is definitely looking like an API for a first-class ARI only application - the “app” field is mandatory and there are no facilities to provide context and extension (the endpoint is apparently just to produce a name for the channel - at least for the “Local” tech use case). From my tests as well as reading the source code, that API cannot trigger any kind of dial plan action (specifically it does not use any of the ast_dial* calls). More and more it looks to me that channels/create is someone’s idea of an ARI/Stasis only workflow that was started and never completed - it is recommended for use by this BKM article Asterisk 14 ARI: Create, Bridge, Dial. ⋆ Asterisk as a way to create channels with no dial plan attachment that can later be manipulated by ARI applications as much as they want, but apparently that API can’t handle local channels properly.

The thing is - with channels/create I can’t answer the “;2” side: it doesn’t enter stasis and I can’t do any ARI operation on it as I get “Channel not in stasis” error. It also doesn’t go into the dial plan (the ast_ari_channels_create() method makes no calls to an ast_dial* API). So I’m not sure how to answer that channel.

Can you provide more information/context as to what is the purpose of channels/create subscribing the “originator” channel into stasis?

In further to the “how to get local channels into stasis”, I tried to use the “originate” APIs in the ARI channels API to create my local channel - calling the “originate with Id” API, with this request:

content-type: application/json
content-length: 18
host: 127.0.0.1
connection: close
authorization: Basic XXXXXXXX
endpoint: Local/some-extension@application-outbound/n
app: myapp
appArgs: 
otherChannelId: baa70145-11b3-4d96-a6a6-113cf8c0bf19
body:
{
  "variables": null
}

In that case the “;2” runs the dial plan (completely ignoring the fact that I specified “app” and didn’t specify “context” and “extension”) and enters into stasis through the call to Stasis(myapp) in the context specified in the “endpoint”; but the “;1” does not enter stasis nor runs a dial plan: it appears to try to “dial” using the ast_dial* API from dial.c but there’s nothing to dial out to (it isn’t a SIP channel) and the application information gets overwritten to “AppDial2” and application args to “(outgoing connection)” - I have no idea why but it is hard coded into main/dial.c. StasisStart is not run for “;1” and then I can’t control it either.

I get these events for “;1” and then nothing (until it is destroyed):

<--- Sending ARI event to 127.0.0.1:43596 --->
{
  "type": "ChannelDialplan",
  "timestamp": "2021-09-11T22:24:06.598+0000",
  "dialplan_app": "AppDial2",
  "dialplan_app_data": "(Outgoing Line)",
  "channel": {
    "id": "b62f1aa6-ac5c-4ddf-bd48-0b9aee14c898",
    "name": "Local/some-extension@application-outbound-0000000a;1",
    "state": "Down",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "AppDial2",
      "app_data": "(Outgoing Line)"
    },
    "creationtime": "2021-09-11T22:24:06.596+0000",
    "language": "en"
  },
  "asterisk_id": "06:4e:42:b6:a3:3a",
  "application": "myapp"
}
<--- Sending ARI event to 127.0.0.1:43596 --->
{
  "type": "Dial",
  "timestamp": "2021-09-11T22:24:06.599+0000",
  "dialstatus": "",
  "forward": "",
  "dialstring": "some-extension@application-outbound/n",
  "peer": {
    "id": "b62f1aa6-ac5c-4ddf-bd48-0b9aee14c898",
    "name": "Local/some-extension@application-outbound-0000000a;1",
    "state": "Down",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "AppDial2",
      "app_data": "(Outgoing Line)"
    },
    "creationtime": "2021-09-11T22:24:06.596+0000",
    "language": "en"
  },
  "asterisk_id": "06:4e:42:b6:a3:3a",
  "application": "myapp"
}

Running GET /channels on this setup (using ARI Channels “originateWithId” API), I get this:

[
  {
    "id": "baa70145-11b3-4d96-a6a6-113cf8c0bf19",
    "name": "Local/some-extension@application-outbound-0000000a;2",
    "state": "Ring",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 2,
      "app_name": "Stasis",
      "app_data": "myapp"
    },
    "creationtime": "2021-09-11T22:24:06.598+0000",
    "language": "en"
  },
  {
    "id": "b62f1aa6-ac5c-4ddf-bd48-0b9aee14c898",
    "name": "Local/some-extension@application-outbound-0000000a;1",
    "state": "Down",
    "caller": {
      "name": "",
      "number": ""
    },
    "connected": {
      "name": "",
      "number": ""
    },
    "accountcode": "",
    "dialplan": {
      "context": "application-outbound",
      "exten": "some-extension",
      "priority": 1,
      "app_name": "AppDial2",
      "app_data": "(Outgoing Line)"
    },
    "creationtime": "2021-09-11T22:24:06.596+0000",
    "language": "en"
  }
]

So that doesn’t work either…

I think you mean can’t create a back to back channel without involving dialplan. Local channels were conceived in a way in which the execution of dialplan is a fundamental part of their purpose.