Adding a channel as an announcer
To add a channel as an announcer to a holding bridge, you must specify a role of announcer
:
POST /bridges/{bridge_id}/addChannel?channel=56789&role=announcer
Music on hold, media playback, recording, and other such things
When dealing with holding bridges, given the particular media rules and channel roles involves, there are some additional catches that you have to be aware when manipulating the bridge:
- Playing music on hold to the bridge will play it for all participants, as well playing media to the bridge. However, you can only do one of those operations - you cannot play media to a holding bridge while you are simultaneously playing music on hold to the bridge. Initiating a
/play
operation on a holding bridge should only be done after stopping the music on hold; likewise, starting music on hold on a bridge with a/play
operation currently in progress will fail. - Recording a holding bridge - while possible - is not terribly interesting. Participant media is dropped - so at best, you'll only record the entertainment that was played to the participants.
There can be only one!
You cannot have an announcer channel in a holding bridge at the same time that you perform a play
operation or have music on hold playing to the bridge. Holding bridges do not mix the media between announcers. Since media from the play
operation has to go to all participants, as does your announcer channel's media, the holding bridge will become quite confused about your application's intent.
Example: Infinite wait area
Now that we all know that holding bridges are perfect for building what many callers fear - the dreaded waiting area of doom - let's make one! This example ARI application will do the following:
- When a channel enters into the Stasis application, it will be put into a existing holding bridge or a newly created one if none exist.
- Music on hold will be played on the bridge.
- Periodically, the
thnk-u-for-patience
sound will be played to the bridge thanking the users for their patience, which they will need since this holding bridge will never progress beyond this point! - When a channel leaves a holding bridge, if no other channels remain, the bridge will be destroyed.
This example will use a similar structure to the bridge-hold python example. Unlike that example, however, it will use some form of a timer to perform our periodic announcement to the holding bridge, and when all the channels have left the infinite wait area, we'll destroy the holding bridge (cleaning up resources is always good!)
Dialplan
For this example, we need to just drop the channel into Stasis, specifying our application:
exten => 1000,1,NoOp() same => n,Stasis(bridge-infinite-wait) same => n,Hangup()
Python
When a channel enters our Stasis application, we first look for an existing holding bridge or create one if none is found. When we create a new bridge, we start music on hold in the bridge and create a timer that will call a callback after 30 seconds. That callback temporarily stops the music on hold, and starts a play operation on the bridge that thanks everyone for their patience. When the play operation finishes, it resumes music on hold.
# find or create a holding bridge holding_bridge = None # Announcer timer announcer_timer = None def find_or_create_bridge(): """Find our infinite wait bridge, or create a new one Returns: The one and only holding bridge """ global holding_bridge global announcer_timer if holding_bridge: return holding_bridge bridges = [candidate for candidate in client.bridges.list() if candidate.json['bridge_type'] == 'holding'] if bridges: bridge = bridges[0] print "Using bridge %s" % bridge.id else: bridge = client.bridges.create(type='holding') bridge.startMoh() print "Created bridge %s" % bridge.id def play_announcement(bridge): """Play an announcement to the bridge""" def on_playback_finished(playback, ev): """Handler for the announcement's PlaybackFinished event""" global announcer_timer global holding_bridge holding_bridge.startMoh() announcer_timer = threading.Timer(30, play_announcement, [holding_bridge]) announcer_timer.start() bridge.stopMoh() print "Letting the everyone know we care..." thanks_playback = bridge.play(media='sound:thnk-u-for-patience') thanks_playback.on_event('PlaybackFinished', on_playback_finished) holding_bridge = bridge holding_bridge.on_event('ChannelLeftBridge', on_channel_left_bridge) # After 30 seconds, let everyone in the bridge know that we care announcer_timer = threading.Timer(30, play_announcement, [holding_bridge]) announcer_timer.start() return bridge
The function that does this work, find_or_create_bridge
, is called from our StasisStart
event handler. The bridge that it returns will have the new channel added to it.
def stasis_start_cb(channel_obj, ev): """Handler for StasisStart event""" bridge = find_or_create_bridge() channel = channel_obj.get('channel') print "Channel %s just entered our application, adding it to bridge %s" % ( channel.json.get('name'), holding_bridge.id) channel.answer() bridge.addChannel(channel=channel.id)
In the find_or_create_bridge
function, we also subscribed for the ChannelLeftBridge
event. We'll add a callback handler for this in that function as well. When the channel leaves the bridge, we'll check to see if there are no more channels in the bridge and - if so - destroy the bridge.
def on_channel_left_bridge(bridge, ev): """Handler for ChannelLeftBridge event""" global holding_bridge global announcer_timer channel = ev.get('channel') channel_count = len(bridge.json.get('channels')) print "Channel %s left bridge %s" % (channel.get('name'), bridge.id) if holding_bridge.id == bridge.id and channel_count == 0: if announcer_timer: announcer_timer.cancel() announcer_timer = None print "Destroying bridge %s" % bridge.id holding_bridge.destroy() holding_bridge = None
bridge-infinite-wait.py
The full source code for bridge-infinite-wait.py
is shown below:
#!/usr/bin/env python import ari import logging import threading logging.basicConfig(level=logging.ERROR) client = ari.connect('http://localhost:8088', 'asterisk', 'asterisk') # find or create a holding bridge holding_bridge = None # Announcer timer announcer_timer = None def find_or_create_bridge(): """Find our infinite wait bridge, or create a new one Returns: The one and only holding bridge """ global holding_bridge global announcer_timer if holding_bridge: return holding_bridge bridges = [candidate for candidate in client.bridges.list() if candidate.json['bridge_type'] == 'holding'] if bridges: bridge = bridges[0] print "Using bridge %s" % bridge.id else: bridge = client.bridges.create(type='holding') bridge.startMoh() print "Created bridge %s" % bridge.id def play_announcement(bridge): """Play an announcement to the bridge""" def on_playback_finished(playback, ev): """Handler for the announcement's PlaybackFinished event""" global announcer_timer global holding_bridge holding_bridge.startMoh() announcer_timer = threading.Timer(30, play_announcement, [holding_bridge]) announcer_timer.start() bridge.stopMoh() print "Letting the everyone know we care..." thanks_playback = bridge.play(media='sound:thnk-u-for-patience') thanks_playback.on_event('PlaybackFinished', on_playback_finished) def on_channel_left_bridge(bridge, ev): """Handler for ChannelLeftBridge event""" global holding_bridge global announcer_timer channel = ev.get('channel') channel_count = len(bridge.json.get('channels')) print "Channel %s left bridge %s" % (channel.get('name'), bridge.id) if holding_bridge.id == bridge.id and channel_count == 0: if announcer_timer: announcer_timer.cancel() announcer_timer = None print "Destroying bridge %s" % bridge.id holding_bridge.destroy() holding_bridge = None holding_bridge = bridge holding_bridge.on_event('ChannelLeftBridge', on_channel_left_bridge) # After 30 seconds, let everyone in the bridge know that we care announcer_timer = threading.Timer(30, play_announcement, [holding_bridge]) announcer_timer.start() return bridge def stasis_start_cb(channel_obj, ev): """Handler for StasisStart event""" bridge = find_or_create_bridge() channel = channel_obj.get('channel') print "Channel %s just entered our application, adding it to bridge %s" % ( channel.json.get('name'), holding_bridge.id) channel.answer() bridge.addChannel(channel=channel.id) def stasis_end_cb(channel, ev): """Handler for StasisEnd event""" print "Channel %s just left our application" % channel.json.get('name') client.on_channel_event('StasisStart', stasis_start_cb) client.on_channel_event('StasisEnd', stasis_end_cb) client.run(apps='bridge-infinite-wait')
bridge-infinite-wait.py in action
Created bridge 950c4805-c33c-4895-ad9a-2798055e4939 Channel PJSIP/alice-00000000 just entered our application, adding it to bridge 950c4805-c33c-4895-ad9a-2798055e4939 Letting the everyone know we care... Channel PJSIP/alice-00000000 left bridge 950c4805-c33c-4895-ad9a-2798055e4939 Destroying bridge 950c4805-c33c-4895-ad9a-2798055e4939 Channel PJSIP/alice-00000000 just left our application
JavaScript (Node.js)
When a channel enters our Stasis application, we first look for an existing holding bridge or create one if none is found. When we create a new bridge, we start music on hold in the bridge and create a timer that will call a callback after 30 seconds. That callback temporarily stops the music on hold, and starts a play operation on the bridge that thanks everyone for their patience. When the play operation finishes, it resumes music on hold.
In all cases, we add the channel to the bridge via the joinBridge
function.
console.log('Channel %s just entered our application', channel.name); // find or create a holding bridge client.bridges.list(function(err, bridges) { if (err) { throw err; } var bridge = bridges.filter(function(candidate) { return candidate.bridge_type === 'holding'; })[0]; if (bridge) { console.log('Using bridge %s', bridge.id); joinBridge(bridge); } else { client.bridges.create({type: 'holding'}, function(err, newBridge) { if (err) { throw err; } console.log('Created bridge %s', newBridge.id); newBridge.startMoh(function(err) { if (err) { throw err; } }); joinBridge(newBridge); timer = setTimeout(play_announcement, 30000); // callback that will let our users know how much we care function play_announcement() { console.log('Letting everyone know we care...'); newBridge.stopMoh(function(err) { if (err) { throw err; } var playback = client.Playback(); newBridge.play({media: 'sound:thnk-u-for-patience'}, playback, function(err, playback) { if (err) { throw err; } }); playback.once('PlaybackFinished', function(event, playback) { newBridge.startMoh(function(err) { if (err) { throw err; } }); timer = setTimeout(play_announcement, 30000); }); }); } }); }
The joinBridge function involves registered a callback for the ChannelLeftBridge event and adds the channel to the bridge.
function joinBridge(bridge) { channel.once('ChannelLeftBridge', function(event, instances) { channelLeftBridge(event, instances, bridge); }); bridge.addChannel({channel: channel.id}, function(err) { if (err) { throw err; } }); channel.answer(function(err) { if (err) { throw err; } }); }
Notice that we use an anonymous function to pass the bridge as an extra parameter to the ChannelLeftBridge callback so we can keep the handler at the same level as joinBridge and avoid another indentation level of callbacks. Finally, we can handle destroying the bridge when the last channel contained in it has left:
// Handler for ChannelLeftBridge event function channelLeftBridge(event, instances, bridge) { var holdingBridge = instances.bridge; var channel = instances.channel; console.log('Channel %s left bridge %s', channel.name, bridge.id); if (holdingBridge.id === bridge.id && holdingBridge.channels.length === 0) { if (timer) { clearTimeout(timer); } bridge.destroy(function(err) { if (err) { throw err; } }); } }
bridge-infinite-wait.js
The full source code for bridge-infinite-wait.js
is shown below:
/*jshint node:true*/ 'use strict'; var ari = require('ari-client'); var util = require('util'); var timer = null; ari.connect('http://localhost:8088', 'asterisk', 'asterisk', clientLoaded); // handler for client being loaded function clientLoaded (err, client) { if (err) { throw err; } // handler for StasisStart event function stasisStart(event, channel) { console.log('Channel %s just entered our application', channel.name); // find or create a holding bridge client.bridges.list(function(err, bridges) { if (err) { throw err; } var bridge = bridges.filter(function(candidate) { return candidate.bridge_type === 'holding'; })[0]; if (bridge) { console.log('Using bridge %s', bridge.id); joinBridge(bridge); } else { client.bridges.create({type: 'holding'}, function(err, newBridge) { if (err) { throw err; } console.log('Created bridge %s', newBridge.id); newBridge.startMoh(function(err) { if (err) { throw err; } }); joinBridge(newBridge); timer = setTimeout(play_announcement, 30000); // callback that will let our users know how much we care function play_announcement() { console.log('Letting everyone know we care...'); newBridge.stopMoh(function(err) { if (err) { throw err; } var playback = client.Playback(); newBridge.play({media: 'sound:thnk-u-for-patience'}, playback, function(err, playback) { if (err) { throw err; } }); playback.once('PlaybackFinished', function(event, playback) { newBridge.startMoh(function(err) { if (err) { throw err; } }); timer = setTimeout(play_announcement, 30000); }); }); } }); } }); function joinBridge(bridge) { channel.once('ChannelLeftBridge', function(event, instances) { channelLeftBridge(event, instances, bridge); }); bridge.addChannel({channel: channel.id}, function(err) { if (err) { throw err; } }); channel.answer(function(err) { if (err) { throw err; } }); } // Handler for ChannelLeftBridge event function channelLeftBridge(event, instances, bridge) { var holdingBridge = instances.bridge; var channel = instances.channel; console.log('Channel %s left bridge %s', channel.name, bridge.id); if (holdingBridge.id === bridge.id && holdingBridge.channels.length === 0) { if (timer) { clearTimeout(timer); } bridge.destroy(function(err) { if (err) { throw err; } }); } } } // handler for StasisEnd event function stasisEnd(event, channel) { console.log('Channel %s just left our application', channel.name); } client.on('StasisStart', stasisStart); client.on('StasisEnd', stasisEnd); console.log('starting'); client.start('bridge-infinite-wait'); }
bridge-infinite-wait.js in action
The following shows the output of the bridge-infinite-wait.js
script when a PJSIP
channel for alice
enters the application:
Channel PJSIP/alice-00000001 just entered our application Created bridge 31a4a193-36a7-412b-854b-cf2cf5f90bbd Letting everyone know we care... Channel PJSIP/alice-00000001 left bridge 31a4a193-36a7-412b-854b-cf2cf5f90bbd Channel PJSIP/alice-00000001 just left our application