Conference & Broadcast with C# and ASP.NET MVC

January 10, 2017
Written by
Hector Ortega
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by
Kat King
Twilion
Paul Kamp
Twilion

conference-broadcast-csharp

This ASP.NET MVC sample application is inspired by the Rapid Response Kit, built by Twilio and used all over the world by organizations who need to act quickly in disastrous situations.

Aid workers can use the tools in this app to communicate immediately with a large group of volunteers. In situations where all parties need to talk at once the organizer can quickly spin up a conference line. In other situations she can broadcast a spoken message to a list of volunteer phone numbers.

To run this sample app yourself, download the code and follow the instructions on GitHub. You might also want to click around the views for this app, since in this tutorial we will only be covering the Twilio pieces.

Create a Conference Number

Before we can call our conference line we need to configure one of our Twilio numbers to send our web application an HTTP request when we get an incoming call.

Click on one of your numbers and configure the Voice URL to point to our app. In our code the route will be /Conference/Join

Twilio Console configured for Conference Broadcast

Create a Simple Conference Call

Our Twilio number is now configured to send HTTP requests to this controller method on any incoming voice calls. Our app responds with TwiML to tell Twilio how to handle the call.

We use the Twilio C# library to generate some TwiML that tells Twilio to Dial into a Conference that we're naming RapidResponseRoom. This means that anyone who calls your Twilio number will automatically join this conference.

Editor: this is a migrated tutorial. Clone the original from https://github.com/TwilioDevEd/conference-broadcast-csharp

using System;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Web.Mvc;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;
using Twilio.TwiML.Voice;

namespace ConferenceBroadcast.Web.Controllers
{
    public class ConferenceController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;

        public ConferenceController() : this(new PhoneNumbers()) {}

        public ConferenceController(IPhoneNumbers phoneNumbers)
        {
            _phoneNumbers = phoneNumbers;
        }

        // GET: Conference
        public ActionResult Index()
        {
            ViewBag.RapidResponseNumber = _phoneNumbers.RapidResponse;
            return View();
        }

        // POST: Conference/Join
        [HttpPost]
        public ActionResult Join()
        {
            var response = new VoiceResponse();
            response.Say("You are about to join the Rapid Response conference");
            response.Append(new Gather(action: Url.ActionUri("Connect", "Conference"))
                                    .Say("Press 1 to join as a listener")
                                    .Say("Press 2 to join as a speaker")
                                    .Say("Press 3 to join as the moderator"));

            return TwiML(response);
        }

        // POST: Conference/Connect
        [HttpPost]
        public ActionResult Connect(string digits)
        {
            var isMuted = digits.Equals("1"); // Listener
            var canControlConferenceOnEnter = digits.Equals("3"); // Moderator

            var response = new VoiceResponse();
            response.Say("You have joined the conference");

            var dial = new Dial();
            dial.Conference("RapidResponseRoom",
                waitUrl: new Uri("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient"),
                muted: isMuted,
                startConferenceOnEnter: canControlConferenceOnEnter,
                endConferenceOnExit: canControlConferenceOnEnter);

            response.Append(dial);

            return TwiML(response);
        }
    }
}

Next we'll turn this into a moderated conference line, with a moderator and listeners.

Create a Moderated Conference

In this scenario we ask for the caller's role before we connect them to the conference. These roles are:

  • Moderator: can start and end the conference
  • Speaker: can speak on the conference call
  • Listener: is muted and can only listen to the call

In this controller we Say a simple message and then ask the caller to choose a role.

using System;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Web.Mvc;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;
using Twilio.TwiML.Voice;

namespace ConferenceBroadcast.Web.Controllers
{
    public class ConferenceController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;

        public ConferenceController() : this(new PhoneNumbers()) {}

        public ConferenceController(IPhoneNumbers phoneNumbers)
        {
            _phoneNumbers = phoneNumbers;
        }

        // GET: Conference
        public ActionResult Index()
        {
            ViewBag.RapidResponseNumber = _phoneNumbers.RapidResponse;
            return View();
        }

        // POST: Conference/Join
        [HttpPost]
        public ActionResult Join()
        {
            var response = new VoiceResponse();
            response.Say("You are about to join the Rapid Response conference");
            response.Append(new Gather(action: Url.ActionUri("Connect", "Conference"))
                                    .Say("Press 1 to join as a listener")
                                    .Say("Press 2 to join as a speaker")
                                    .Say("Press 3 to join as the moderator"));

            return TwiML(response);
        }

        // POST: Conference/Connect
        [HttpPost]
        public ActionResult Connect(string digits)
        {
            var isMuted = digits.Equals("1"); // Listener
            var canControlConferenceOnEnter = digits.Equals("3"); // Moderator

            var response = new VoiceResponse();
            response.Say("You have joined the conference");

            var dial = new Dial();
            dial.Conference("RapidResponseRoom",
                waitUrl: new Uri("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient"),
                muted: isMuted,
                startConferenceOnEnter: canControlConferenceOnEnter,
                endConferenceOnExit: canControlConferenceOnEnter);

            response.Append(dial);

            return TwiML(response);
        }
    }
}

So our caller have listened to a few role options they can choose from, and next they will choose one. For this we tell Twilio to Gather a button press from the caller's phone so we know which role they want to use. Let's see how next.

Connect to a Moderated Conference

The <Gather> verb from the previous step included an action parameter that took an absolute or relative URL as a value — in our case, the Conference/Connect route.

When the caller finishes entering digits Twilio makes a GET or POST request to this URL including a Digits parameter with the number our caller chose.

We use that parameter to set a couple variables, isMuted and canControlConferenceOnEnter, which we then use to configure our Dial and Conference TwiML elements.

using System;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Web.Mvc;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;
using Twilio.TwiML.Voice;

namespace ConferenceBroadcast.Web.Controllers
{
    public class ConferenceController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;

        public ConferenceController() : this(new PhoneNumbers()) {}

        public ConferenceController(IPhoneNumbers phoneNumbers)
        {
            _phoneNumbers = phoneNumbers;
        }

        // GET: Conference
        public ActionResult Index()
        {
            ViewBag.RapidResponseNumber = _phoneNumbers.RapidResponse;
            return View();
        }

        // POST: Conference/Join
        [HttpPost]
        public ActionResult Join()
        {
            var response = new VoiceResponse();
            response.Say("You are about to join the Rapid Response conference");
            response.Append(new Gather(action: Url.ActionUri("Connect", "Conference"))
                                    .Say("Press 1 to join as a listener")
                                    .Say("Press 2 to join as a speaker")
                                    .Say("Press 3 to join as the moderator"));

            return TwiML(response);
        }

        // POST: Conference/Connect
        [HttpPost]
        public ActionResult Connect(string digits)
        {
            var isMuted = digits.Equals("1"); // Listener
            var canControlConferenceOnEnter = digits.Equals("3"); // Moderator

            var response = new VoiceResponse();
            response.Say("You have joined the conference");

            var dial = new Dial();
            dial.Conference("RapidResponseRoom",
                waitUrl: new Uri("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient"),
                muted: isMuted,
                startConferenceOnEnter: canControlConferenceOnEnter,
                endConferenceOnExit: canControlConferenceOnEnter);

            response.Append(dial);

            return TwiML(response);
        }
    }
}

That's it for connecting callers to our conference room. Let's explore next the other feature of this app: broadcasting a voice message to a list of phone numbers.

Voice Broadcast

In addition to hosting conference calls, an organizer can use our application to broadcast a voice message to a list of phone numbers. She can do this by choosing a recording from a dropdown, entering a list of phone numbers and clicking Submit.

To power this feature, we'll use Twilio's REST API to fetch all of the recordings associated with our account. If our organizer wants to record a new message, we'll call her phone and record her response.

using ConferenceBroadcast.Web.Domain.Twilio;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace ConferenceBroadcast.Web.Controllers
{
    public class RecordingsController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;
        private readonly IClient _client;
        private ICustomRequest _customRequest;

        public RecordingsController() : this(new Client(), new PhoneNumbers()) {}

        public RecordingsController(IClient client, IPhoneNumbers phoneNumbers, ICustomRequest customRequest = null)
        {
            _client = client;
            _phoneNumbers = phoneNumbers;
            _customRequest = customRequest;
        }

        protected override void Initialize(RequestContext requestContext)
        {
            base.Initialize(requestContext);
            _customRequest = new CustomRequest(requestContext.HttpContext.Request);
        }

        // GET: Recordings
        public async Task<ActionResult> Index()
        {
            var recordings = await _client.Recordings();

            var formattedRecordings = recordings.Select(r => new
            {
                url = ResolveUrl(r.Uri.ToString()),
                date = r.DateCreated?.ToString("ddd, dd MMM yyyy HH:mm:ss")
            });

            return Json(formattedRecordings, JsonRequestBehavior.AllowGet);
        }

        // POST: Recordings/Create
        [HttpPost]
        public async Task<ActionResult> Create(string phoneNumber)
        {
            var url = $"{_customRequest.Url}{Url.ActionUri("Record", "Recordings")}";

            await _client.Call(phoneNumber, _phoneNumbers.Twilio, url);

            return new EmptyResult();
        }

        // POST: Recording/Record
        [HttpPost]
        public ActionResult Record()
        {
            var response = new VoiceResponse();
            response
                .Say("Please record your message after the beep. Press star to end your recording.")
                .Record(finishOnKey: "*", action: Url.ActionUri("Hangup", "Recordings"));

            return TwiML(response);
        }

        // POST: Recording/Hangup
        [HttpPost]
        public ActionResult Hangup()
        {
            var response = new VoiceResponse();
            response
                .Say("Your recording has been saved. Good bye!")
                .Hangup();

            return TwiML(response);
        }

        private static string ResolveUrl(string uri)
        {
            return $"https://api.twilio.com{uri.Replace(".json", ".mp3")}";
        }
    }
}

Now that we know what this feature is all about, let's take a dig into the code.

Fetch Recordings

This route fetches all of the recordings associated with our Twilio account. We could filter these results by date or call sid using Twilio's API, but for this example we just pull all recordings.

In order to use Twilio's handy API we need to first create our Twilio client, which we can easily do by passing our credentials.

Once we get all of the recordings we need to render a JSON response of our recordings object. This route will be called by our Javascript on page load.

As a side note, keep in mind that _client.Recordings() is just a thin wrapper for RecordingResource.ReadAsync().

using ConferenceBroadcast.Web.Domain.Twilio;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace ConferenceBroadcast.Web.Controllers
{
    public class RecordingsController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;
        private readonly IClient _client;
        private ICustomRequest _customRequest;

        public RecordingsController() : this(new Client(), new PhoneNumbers()) {}

        public RecordingsController(IClient client, IPhoneNumbers phoneNumbers, ICustomRequest customRequest = null)
        {
            _client = client;
            _phoneNumbers = phoneNumbers;
            _customRequest = customRequest;
        }

        protected override void Initialize(RequestContext requestContext)
        {
            base.Initialize(requestContext);
            _customRequest = new CustomRequest(requestContext.HttpContext.Request);
        }

        // GET: Recordings
        public async Task<ActionResult> Index()
        {
            var recordings = await _client.Recordings();

            var formattedRecordings = recordings.Select(r => new
            {
                url = ResolveUrl(r.Uri.ToString()),
                date = r.DateCreated?.ToString("ddd, dd MMM yyyy HH:mm:ss")
            });

            return Json(formattedRecordings, JsonRequestBehavior.AllowGet);
        }

        // POST: Recordings/Create
        [HttpPost]
        public async Task<ActionResult> Create(string phoneNumber)
        {
            var url = $"{_customRequest.Url}{Url.ActionUri("Record", "Recordings")}";

            await _client.Call(phoneNumber, _phoneNumbers.Twilio, url);

            return new EmptyResult();
        }

        // POST: Recording/Record
        [HttpPost]
        public ActionResult Record()
        {
            var response = new VoiceResponse();
            response
                .Say("Please record your message after the beep. Press star to end your recording.")
                .Record(finishOnKey: "*", action: Url.ActionUri("Hangup", "Recordings"));

            return TwiML(response);
        }

        // POST: Recording/Hangup
        [HttpPost]
        public ActionResult Hangup()
        {
            var response = new VoiceResponse();
            response
                .Say("Your recording has been saved. Good bye!")
                .Hangup();

            return TwiML(response);
        }

        private static string ResolveUrl(string uri)
        {
            return $"https://api.twilio.com{uri.Replace(".json", ".mp3")}";
        }
    }
}

We can fetch all the stored recordings, but how can we record a new message? Let's see that next.

Record a new Message

If the organizer needs to make a new recording, we simply call her and record the call. Twilio makes this simple with the Record verb.

Here we Say something to the caller and then Record her message. There are many more options we can pass to Record, but here we simply tell it to stop recording when * is pressed and to redirect to recording/hangup, so the call drops when the recording is finished.

using ConferenceBroadcast.Web.Domain.Twilio;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace ConferenceBroadcast.Web.Controllers
{
    public class RecordingsController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;
        private readonly IClient _client;
        private ICustomRequest _customRequest;

        public RecordingsController() : this(new Client(), new PhoneNumbers()) {}

        public RecordingsController(IClient client, IPhoneNumbers phoneNumbers, ICustomRequest customRequest = null)
        {
            _client = client;
            _phoneNumbers = phoneNumbers;
            _customRequest = customRequest;
        }

        protected override void Initialize(RequestContext requestContext)
        {
            base.Initialize(requestContext);
            _customRequest = new CustomRequest(requestContext.HttpContext.Request);
        }

        // GET: Recordings
        public async Task<ActionResult> Index()
        {
            var recordings = await _client.Recordings();

            var formattedRecordings = recordings.Select(r => new
            {
                url = ResolveUrl(r.Uri.ToString()),
                date = r.DateCreated?.ToString("ddd, dd MMM yyyy HH:mm:ss")
            });

            return Json(formattedRecordings, JsonRequestBehavior.AllowGet);
        }

        // POST: Recordings/Create
        [HttpPost]
        public async Task<ActionResult> Create(string phoneNumber)
        {
            var url = $"{_customRequest.Url}{Url.ActionUri("Record", "Recordings")}";

            await _client.Call(phoneNumber, _phoneNumbers.Twilio, url);

            return new EmptyResult();
        }

        // POST: Recording/Record
        [HttpPost]
        public ActionResult Record()
        {
            var response = new VoiceResponse();
            response
                .Say("Please record your message after the beep. Press star to end your recording.")
                .Record(finishOnKey: "*", action: Url.ActionUri("Hangup", "Recordings"));

            return TwiML(response);
        }

        // POST: Recording/Hangup
        [HttpPost]
        public ActionResult Hangup()
        {
            var response = new VoiceResponse();
            response
                .Say("Your recording has been saved. Good bye!")
                .Hangup();

            return TwiML(response);
        }

        private static string ResolveUrl(string uri)
        {
            return $"https://api.twilio.com{uri.Replace(".json", ".mp3")}";
        }
    }
}

We just saw how to list all recorded messages and how to record a new one. The last thing left is allowing a caller to broadcast one of those recorded messages. We'll see that next.

Broadcast a Recorded Message

This controller processes our voice broadcast webform, starting with the phone numbers our organizer provided. Because they are comma separated we wrote the helper method VolunteersNumbers.

Next we initiate a phone call to each number using Twilio's REST API.

When Twilio connects this call it will make a request to the Url parameter to get further instructions. We include a recordingUrl parameter in that URL so that our Play action method will know which recording to use.

That makes the work for our Broadcast/Play route simple — we just Play the recording.

using System;
using Client = ConferenceBroadcast.Web.Domain.Twilio.Client;
using ConferenceBroadcast.Web.Domain.Twilio;
using ConferenceBroadcast.Web.Domain.Twilio.Configuration;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using Twilio.AspNet.Mvc;
using Twilio.TwiML;

namespace ConferenceBroadcast.Web.Controllers
{
    public class BroadcastController : TwilioController
    {
        private readonly IPhoneNumbers _phoneNumbers;
        private readonly IClient _client;
        private ICustomRequest _customRequest;

        public BroadcastController() : this(new Client(), new PhoneNumbers()) {}

        public BroadcastController(IClient client, IPhoneNumbers phoneNumbers, ICustomRequest customRequest = null)
        {
            _client = client;
            _phoneNumbers = phoneNumbers;
            _customRequest = customRequest;
        }

        protected override void Initialize(RequestContext requestContext)
        {
            base.Initialize(requestContext);
            _customRequest = new CustomRequest(requestContext.HttpContext.Request);
        }

        // GET: Broadcast
        public ActionResult Index()
        {
            return View();
        }

        // GET: Broadcast/Send
        public async Task<ActionResult> Send(string numbers, string recordingUrl)
        {
            var url = _customRequest.Url + Url.Action("Play", new {recordingUrl});

            var calls = VolunteersNumbers(numbers).Select(
                number => _client.Call(number, _phoneNumbers.Twilio, url));

            await Task.WhenAll(calls);

            return View();
        }

        // POST: Broadcast/Play
        [HttpPost]
        public ActionResult Play(string recordingUrl)
        {
            var response = new VoiceResponse();
            response.Play(new Uri(recordingUrl));

            return TwiML(response);
        }

        private static IEnumerable<string> VolunteersNumbers(string numbers)
        {
            return numbers.Split(',').Select(n => n.Trim());
        }
    }
}

That's it! We've just implemented two major features of a Rapid Response Kit! Maybe in the future you can tackle some of the other features and build a full-fledge kit!

Where to next?

If you're a .NET developer working with Twilio, you might enjoy these other tutorials:

Automated Survey

Instantly collect structured data from your users with a survey conducted over a voice call or SMS text messages. Learn how to create your own survey in Rails.

IVR: Phone Trees

Create a fully functional automated phone tree, with some Reese's Pieces sprinkled about.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, we'd love to hear it. Tweet @twilio to let us know what you think!