Conference & Broadcast with Java and Spark

January 10, 2017
Written by
Reviewed by
Paul Kamp
Twilion
Kat King
Twilion

conference-broadcast-java

This Spark 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 Java 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-spark/

package com.twilio.conferencebroadcast.controllers;

import java.util.HashMap;
import java.util.Map;

import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.twiml.*;

import spark.ModelAndView;
import spark.Request;
import spark.Route;
import spark.TemplateViewRoute;

@SuppressWarnings({"rawtypes", "unchecked"})
public class ConferenceController {
  AppSetup appSetup;

  public ConferenceController() {
    this.appSetup = new AppSetup();
  }

  public ConferenceController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public Route join = (request, response) -> {
    response.type("application/xml");

    return getXMLJoinResponse();
  };
  public Route connect = (request, response) -> {
    response.type("application/xml");

    return getXMLConnectResponse(request);
  };

  public TemplateViewRoute index = (request, response) -> {
    Map<String, String> map = new HashMap();
    String number = appSetup.getConferenceNumber();
    map.put("conference_number", number);

    return new ModelAndView(map, "conference.mustache");
  };

  /**
   * Generates the xml necessary to ask the user what role will be used to join the call
   * 
   * @return XML response
   */
  public String getXMLJoinResponse() {
    String message =
        "You are about to join the Rapid Response conference." + "Press 1 to join as a listener."
            + "Press 2 to join as a speaker." + "Press 3 to join as the moderator.";

    Say sayMessage = new Say.Builder(message).build();
    Gather gather = new Gather.Builder()
        .action("/conference/connect")
        .method(Method.POST)
        .say(sayMessage)
        .build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().gather(gather).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Twilio's response building error");
      return "Twilio's response building error";
    }
  }

  /**
   * Returns necessary xml to join the conference call
   * 
   * @param request
   * @return XML string
   */
  public String getXMLConnectResponse(Request request) {
    Boolean muted = false;
    Boolean moderator = false;
    String digits = request.queryParams("Digits");

    if (digits.equals("1")) {
      muted = true;
    }
    if (digits.equals("3")) {
      moderator = true;
    }

    String defaultMessage = "You have joined the conference.";
    Say sayMessage = new Say.Builder(defaultMessage).build();

    Conference conference = new Conference.Builder("RapidResponseRoom")
        .waitUrl("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient")
        .muted(muted)
        .startConferenceOnEnter(moderator)
        .endConferenceOnExit(moderator)
        .build();

    Dial dial = new Dial.Builder().conference(conference).build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().say(sayMessage).dial(dial).build();
    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Twilio's response building error");
      return "Twilio's response building error";
    }
  }
}

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.

package com.twilio.conferencebroadcast.controllers;

import java.util.HashMap;
import java.util.Map;

import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.twiml.*;

import spark.ModelAndView;
import spark.Request;
import spark.Route;
import spark.TemplateViewRoute;

@SuppressWarnings({"rawtypes", "unchecked"})
public class ConferenceController {
  AppSetup appSetup;

  public ConferenceController() {
    this.appSetup = new AppSetup();
  }

  public ConferenceController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public Route join = (request, response) -> {
    response.type("application/xml");

    return getXMLJoinResponse();
  };
  public Route connect = (request, response) -> {
    response.type("application/xml");

    return getXMLConnectResponse(request);
  };

  public TemplateViewRoute index = (request, response) -> {
    Map<String, String> map = new HashMap();
    String number = appSetup.getConferenceNumber();
    map.put("conference_number", number);

    return new ModelAndView(map, "conference.mustache");
  };

  /**
   * Generates the xml necessary to ask the user what role will be used to join the call
   * 
   * @return XML response
   */
  public String getXMLJoinResponse() {
    String message =
        "You are about to join the Rapid Response conference." + "Press 1 to join as a listener."
            + "Press 2 to join as a speaker." + "Press 3 to join as the moderator.";

    Say sayMessage = new Say.Builder(message).build();
    Gather gather = new Gather.Builder()
        .action("/conference/connect")
        .method(Method.POST)
        .say(sayMessage)
        .build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().gather(gather).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Twilio's response building error");
      return "Twilio's response building error";
    }
  }

  /**
   * Returns necessary xml to join the conference call
   * 
   * @param request
   * @return XML string
   */
  public String getXMLConnectResponse(Request request) {
    Boolean muted = false;
    Boolean moderator = false;
    String digits = request.queryParams("Digits");

    if (digits.equals("1")) {
      muted = true;
    }
    if (digits.equals("3")) {
      moderator = true;
    }

    String defaultMessage = "You have joined the conference.";
    Say sayMessage = new Say.Builder(defaultMessage).build();

    Conference conference = new Conference.Builder("RapidResponseRoom")
        .waitUrl("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient")
        .muted(muted)
        .startConferenceOnEnter(moderator)
        .endConferenceOnExit(moderator)
        .build();

    Dial dial = new Dial.Builder().conference(conference).build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().say(sayMessage).dial(dial).build();
    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Twilio's response building error");
      return "Twilio's response building error";
    }
  }
}

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 POST request to this URL including a Digits parameter with the number our caller chose.

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

package com.twilio.conferencebroadcast.controllers;

import java.util.HashMap;
import java.util.Map;

import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.twiml.*;

import spark.ModelAndView;
import spark.Request;
import spark.Route;
import spark.TemplateViewRoute;

@SuppressWarnings({"rawtypes", "unchecked"})
public class ConferenceController {
  AppSetup appSetup;

  public ConferenceController() {
    this.appSetup = new AppSetup();
  }

  public ConferenceController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public Route join = (request, response) -> {
    response.type("application/xml");

    return getXMLJoinResponse();
  };
  public Route connect = (request, response) -> {
    response.type("application/xml");

    return getXMLConnectResponse(request);
  };

  public TemplateViewRoute index = (request, response) -> {
    Map<String, String> map = new HashMap();
    String number = appSetup.getConferenceNumber();
    map.put("conference_number", number);

    return new ModelAndView(map, "conference.mustache");
  };

  /**
   * Generates the xml necessary to ask the user what role will be used to join the call
   * 
   * @return XML response
   */
  public String getXMLJoinResponse() {
    String message =
        "You are about to join the Rapid Response conference." + "Press 1 to join as a listener."
            + "Press 2 to join as a speaker." + "Press 3 to join as the moderator.";

    Say sayMessage = new Say.Builder(message).build();
    Gather gather = new Gather.Builder()
        .action("/conference/connect")
        .method(Method.POST)
        .say(sayMessage)
        .build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().gather(gather).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Twilio's response building error");
      return "Twilio's response building error";
    }
  }

  /**
   * Returns necessary xml to join the conference call
   * 
   * @param request
   * @return XML string
   */
  public String getXMLConnectResponse(Request request) {
    Boolean muted = false;
    Boolean moderator = false;
    String digits = request.queryParams("Digits");

    if (digits.equals("1")) {
      muted = true;
    }
    if (digits.equals("3")) {
      moderator = true;
    }

    String defaultMessage = "You have joined the conference.";
    Say sayMessage = new Say.Builder(defaultMessage).build();

    Conference conference = new Conference.Builder("RapidResponseRoom")
        .waitUrl("http://twimlets.com/holdmusic?Bucket=com.twilio.music.ambient")
        .muted(muted)
        .startConferenceOnEnter(moderator)
        .endConferenceOnExit(moderator)
        .build();

    Dial dial = new Dial.Builder().conference(conference).build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().say(sayMessage).dial(dial).build();
    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Twilio's response building error");
      return "Twilio's response building error";
    }
  }
}

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.

package com.twilio.conferencebroadcast;

import com.twilio.conferencebroadcast.controllers.BroadcastController;
import com.twilio.conferencebroadcast.controllers.ConferenceController;
import com.twilio.conferencebroadcast.controllers.HomeController;
import com.twilio.conferencebroadcast.controllers.RecordingController;
import com.twilio.conferencebroadcast.lib.AppSetup;
import spark.Spark;
import spark.template.mustache.MustacheTemplateEngine;

import static spark.Spark.*;

/**
 * Main application class. The environment is set up here, and all necessary services are run.
 */
public class App {
  public static void main(String[] args) {
    AppSetup appSetup = new AppSetup();

    /**
     * Sets the port in which the application will run. Takes the port value from PORT
     * environment variable, if not set, uses Spark default port 4567.
     */
    port(appSetup.getPortNumber());

    /**
     * Gets the entity manager based on environment variable DATABASE_URL and injects it into
     * AppointmentService which handles all DB operations.
     */
    // EntityManagerFactory factory = appSetup.getEntityManagerFactory();

    /**
     * Specifies the directory within resources that will be publicly available when the
     * application is running. Place static web files in this directory (JS, CSS).
     */
    Spark.staticFileLocation("/public");

    ConferenceController conferenceController = new ConferenceController();
    BroadcastController broadcastController = new BroadcastController();
    RecordingController recordingController = new RecordingController();

    /**
     * Home route
     */
    get("/", new HomeController().index, new MustacheTemplateEngine());

    /**
     * Defines routes for everything related to conference calls
     */
    get("/conference", conferenceController.index, new MustacheTemplateEngine());
    post("/conference", conferenceController.join);
    post("/conference/connect", conferenceController.connect);

    /**
     * Defines routes for everything related to broadcast calls
     */
    get("/broadcast", broadcastController.index, new MustacheTemplateEngine());
    post("/broadcast/record", broadcastController.record);
    post("/broadcast/hangup", broadcastController.hangup);
    post("/broadcast/send", broadcastController.send, new MustacheTemplateEngine());
    post("/broadcast/play", broadcastController.play);

    /**
     * Defines routes for everything related to recordings
     */
    get("/recording/index", recordingController.index);
    post("/recording/create", recordingController.create);
  }
}

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

Initialize the Client

Before performing any request to the Twilio API using the helper library, you must initialize the client using the static method init() with your credentials on the Twilio class. After this is done, you can make request to the API as much as you like!

package com.twilio.conferencebroadcast.controllers;

import com.twilio.Twilio;
import com.twilio.base.ResourceSet;
import com.twilio.conferencebroadcast.exceptions.UndefinedEnvironmentVariableException;
import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.conferencebroadcast.lib.RecordingUriTransformer;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.rest.api.v2010.account.Recording;
import com.twilio.type.PhoneNumber;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import spark.Request;
import spark.Route;

import java.net.URI;
import java.net.URISyntaxException;

public class RecordingController {
  AppSetup appSetup;

  public RecordingController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public RecordingController() {
    this.appSetup = new AppSetup();
  }

  public Route index = (request, response) -> {
    response.type("application/json");

    return getRecordingsAsJSON();
  };

  public Route create = (request, response) -> {
    int status = createRecording(request);
    response.status(status);
    return "";
  };

  private void initializeTwilioClient() {
    String accountSid = null;
    String authToken = null;

    try {
      accountSid = appSetup.getAccountSid();
      authToken = appSetup.getAuthToken();
    } catch (UndefinedEnvironmentVariableException e) {
      System.out.println(e.getLocalizedMessage());
    }

    Twilio.init(accountSid, authToken);
  }

  /**
   * Function that creates a recording remotely using Twilio's rest client
   * 
   * @param request Request holds the phone_number parameter
   * @return Returns the status of the request
   */
  public int createRecording(Request request) {
    initializeTwilioClient();

    String phoneNumber = request.queryParams("phone_number");
    String twilioNumber = null;
    try {
      twilioNumber = appSetup.getTwilioPhoneNumber();
    } catch (UndefinedEnvironmentVariableException e) {
      e.printStackTrace();
    }
    String path = request.url().replace(request.uri(), "") + "/broadcast/record";

    Call call;
    try {
      call = Call.creator(new PhoneNumber(phoneNumber), new PhoneNumber(twilioNumber), new URI(path))
          .create();
    } catch (URISyntaxException e) {
      System.out.println("Invalid URL used in call creator");
    }

    return 200;
  }

  /**
   * Creates a JSON string that contains the url and date of all the user's recordings
   * 
   * @return Returns a JSON string
   */
  public String getRecordingsAsJSON() {
    initializeTwilioClient();

    ResourceSet<Recording> recordings = Recording.reader().read();

    JSONArray jsonRecordings = new JSONArray();

    for (Recording recording : recordings) {
      JSONObject obj = new JSONObject();
      obj.put("url", RecordingUriTransformer.transform(recording.getUri()));
      obj.put("date", recording.getDateCreated().toString("yyyy-M-dd HH:mm:ss"));
      jsonRecordings.add(obj);
    }

    return jsonRecordings.toJSONString();
  }
}

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.

package com.twilio.conferencebroadcast.controllers;

import com.twilio.Twilio;
import com.twilio.base.ResourceSet;
import com.twilio.conferencebroadcast.exceptions.UndefinedEnvironmentVariableException;
import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.conferencebroadcast.lib.RecordingUriTransformer;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.rest.api.v2010.account.Recording;
import com.twilio.type.PhoneNumber;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import spark.Request;
import spark.Route;

import java.net.URI;
import java.net.URISyntaxException;

public class RecordingController {
  AppSetup appSetup;

  public RecordingController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public RecordingController() {
    this.appSetup = new AppSetup();
  }

  public Route index = (request, response) -> {
    response.type("application/json");

    return getRecordingsAsJSON();
  };

  public Route create = (request, response) -> {
    int status = createRecording(request);
    response.status(status);
    return "";
  };

  private void initializeTwilioClient() {
    String accountSid = null;
    String authToken = null;

    try {
      accountSid = appSetup.getAccountSid();
      authToken = appSetup.getAuthToken();
    } catch (UndefinedEnvironmentVariableException e) {
      System.out.println(e.getLocalizedMessage());
    }

    Twilio.init(accountSid, authToken);
  }

  /**
   * Function that creates a recording remotely using Twilio's rest client
   * 
   * @param request Request holds the phone_number parameter
   * @return Returns the status of the request
   */
  public int createRecording(Request request) {
    initializeTwilioClient();

    String phoneNumber = request.queryParams("phone_number");
    String twilioNumber = null;
    try {
      twilioNumber = appSetup.getTwilioPhoneNumber();
    } catch (UndefinedEnvironmentVariableException e) {
      e.printStackTrace();
    }
    String path = request.url().replace(request.uri(), "") + "/broadcast/record";

    Call call;
    try {
      call = Call.creator(new PhoneNumber(phoneNumber), new PhoneNumber(twilioNumber), new URI(path))
          .create();
    } catch (URISyntaxException e) {
      System.out.println("Invalid URL used in call creator");
    }

    return 200;
  }

  /**
   * Creates a JSON string that contains the url and date of all the user's recordings
   * 
   * @return Returns a JSON string
   */
  public String getRecordingsAsJSON() {
    initializeTwilioClient();

    ResourceSet<Recording> recordings = Recording.reader().read();

    JSONArray jsonRecordings = new JSONArray();

    for (Recording recording : recordings) {
      JSONObject obj = new JSONObject();
      obj.put("url", RecordingUriTransformer.transform(recording.getUri()));
      obj.put("date", recording.getDateCreated().toString("yyyy-M-dd HH:mm:ss"));
      jsonRecordings.add(obj);
    }

    return jsonRecordings.toJSONString();
  }
}

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

Recording 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 broadcast/hangup, so the call drops when the recording is finished.

package com.twilio.conferencebroadcast.controllers;

import com.twilio.Twilio;
import com.twilio.conferencebroadcast.exceptions.UndefinedEnvironmentVariableException;
import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.twiml.Hangup;
import com.twilio.twiml.Method;
import com.twilio.twiml.Play;
import com.twilio.twiml.Record;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;
import com.twilio.type.PhoneNumber;
import spark.ModelAndView;
import spark.Request;
import spark.Route;
import spark.TemplateViewRoute;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

public class BroadcastController {
  AppSetup appSetup;

  public BroadcastController() {
    this.appSetup = new AppSetup();
  }

  public BroadcastController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public TemplateViewRoute index = (request, response) -> {
    Map<String, String> map = new HashMap();

    return new ModelAndView(map, "broadcast.mustache");
  };
  public Route record = (request, response) -> {
    response.type("application/xml");

    return getXMLRecordResponse();
  };
  public Route hangup = (request, response) -> {
    response.type("application/xml");

    return getXMLHangupResponse();
  };
  public Route play = (request, response) -> {
    return getXMLPlayResponse(request);
  };

  public TemplateViewRoute send = (request, response) -> {
    Map<String, String> map = new HashMap();
    broadcastSend(request);

    map.put("message", "true");
    map.put("notice", "Broadcast was successfully sent");

    return new ModelAndView(map, "broadcast.mustache");
  };

  /**
   * Returns the xml response that will play the recorded message for the given URL
   * 
   * @param request
   * @return xml response
   */
  public String getXMLPlayResponse(Request request) {
    String recordingUrl = request.queryParams("recording_url");

    Play play = new Play.Builder(recordingUrl).build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().play(play).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      return "Unable tu create valid TwiML";
    }
  }

  /**
   * Method that will create the remote calls using Twilio's rest client for every number especified
   * in the CSV.
   * 
   * @param request
   */
  public void broadcastSend(Request request) {
    initializeTwilioClient();

    String numbers = request.queryParams("numbers");
    String recordingUrl = request.queryParams("recording_url");
    String[] parsedNumbers = numbers.split(",");
    String url =
        request.url().replace(request.uri(), "") + "/broadcast/play?recording_url=" + recordingUrl;
    String twilioNumber = null;
    try {
      twilioNumber = appSetup.getTwilioPhoneNumber();
    } catch (UndefinedEnvironmentVariableException e) {
      e.printStackTrace();
    }

    for (String number : parsedNumbers) {
      try {
        Call.creator(new PhoneNumber(number), new PhoneNumber(twilioNumber), new URI(url)).create();
      } catch (TwilioException e) {
        System.out.println("Twilio rest client error " + e.getLocalizedMessage());
        System.out.println("Remember not to use localhost to access this app, use your ngrok URL");
      } catch (URISyntaxException e) {
        System.out.println(e.getLocalizedMessage());
      }
    }
  }

  /**
   * This XML response is necessary to end the call when a new recording is made
   * 
   * @return
   */
  public String getXMLHangupResponse() {
    Say say = new Say.Builder("Your recording has been saved. Good bye.").build();
    Hangup hangup = new Hangup();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().say(say).hangup(hangup).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Unable to create twiml response");
      return "Unable to create twiml response";
    }
  }

  public String getXMLRecordResponse() {
    Say say = new Say.Builder(
        "Please record your message after the beep. Press star to end your recording.").build();
    Record record = new Record.Builder()
        .action("/broadcast/hangup")
        .method(Method.POST)
        .finishOnKey("*")
        .build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder()
        .say(say)
        .record(record)
        .build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Unable to create Twiml Response");
      return "Unable to create Twiml Response";
    }
  }

  private void initializeTwilioClient() {
    String accountSid = null;
    String authToken = null;

    try {
      accountSid = appSetup.getAccountSid();
      authToken = appSetup.getAuthToken();
    } catch (UndefinedEnvironmentVariableException e) {
      System.out.println(e.getLocalizedMessage());
    }

    Twilio.init(accountSid, authToken);
  }
}

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.

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 recording_url parameter in that URL so that our broadcast.play controller will know which recording to use.

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

package com.twilio.conferencebroadcast.controllers;

import com.twilio.Twilio;
import com.twilio.conferencebroadcast.exceptions.UndefinedEnvironmentVariableException;
import com.twilio.conferencebroadcast.lib.AppSetup;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Call;
import com.twilio.twiml.Hangup;
import com.twilio.twiml.Method;
import com.twilio.twiml.Play;
import com.twilio.twiml.Record;
import com.twilio.twiml.Say;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;
import com.twilio.type.PhoneNumber;
import spark.ModelAndView;
import spark.Request;
import spark.Route;
import spark.TemplateViewRoute;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

public class BroadcastController {
  AppSetup appSetup;

  public BroadcastController() {
    this.appSetup = new AppSetup();
  }

  public BroadcastController(AppSetup appSetup) {
    this.appSetup = appSetup;
  }

  public TemplateViewRoute index = (request, response) -> {
    Map<String, String> map = new HashMap();

    return new ModelAndView(map, "broadcast.mustache");
  };
  public Route record = (request, response) -> {
    response.type("application/xml");

    return getXMLRecordResponse();
  };
  public Route hangup = (request, response) -> {
    response.type("application/xml");

    return getXMLHangupResponse();
  };
  public Route play = (request, response) -> {
    return getXMLPlayResponse(request);
  };

  public TemplateViewRoute send = (request, response) -> {
    Map<String, String> map = new HashMap();
    broadcastSend(request);

    map.put("message", "true");
    map.put("notice", "Broadcast was successfully sent");

    return new ModelAndView(map, "broadcast.mustache");
  };

  /**
   * Returns the xml response that will play the recorded message for the given URL
   * 
   * @param request
   * @return xml response
   */
  public String getXMLPlayResponse(Request request) {
    String recordingUrl = request.queryParams("recording_url");

    Play play = new Play.Builder(recordingUrl).build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().play(play).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      return "Unable tu create valid TwiML";
    }
  }

  /**
   * Method that will create the remote calls using Twilio's rest client for every number especified
   * in the CSV.
   * 
   * @param request
   */
  public void broadcastSend(Request request) {
    initializeTwilioClient();

    String numbers = request.queryParams("numbers");
    String recordingUrl = request.queryParams("recording_url");
    String[] parsedNumbers = numbers.split(",");
    String url =
        request.url().replace(request.uri(), "") + "/broadcast/play?recording_url=" + recordingUrl;
    String twilioNumber = null;
    try {
      twilioNumber = appSetup.getTwilioPhoneNumber();
    } catch (UndefinedEnvironmentVariableException e) {
      e.printStackTrace();
    }

    for (String number : parsedNumbers) {
      try {
        Call.creator(new PhoneNumber(number), new PhoneNumber(twilioNumber), new URI(url)).create();
      } catch (TwilioException e) {
        System.out.println("Twilio rest client error " + e.getLocalizedMessage());
        System.out.println("Remember not to use localhost to access this app, use your ngrok URL");
      } catch (URISyntaxException e) {
        System.out.println(e.getLocalizedMessage());
      }
    }
  }

  /**
   * This XML response is necessary to end the call when a new recording is made
   * 
   * @return
   */
  public String getXMLHangupResponse() {
    Say say = new Say.Builder("Your recording has been saved. Good bye.").build();
    Hangup hangup = new Hangup();

    VoiceResponse voiceResponse = new VoiceResponse.Builder().say(say).hangup(hangup).build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Unable to create twiml response");
      return "Unable to create twiml response";
    }
  }

  public String getXMLRecordResponse() {
    Say say = new Say.Builder(
        "Please record your message after the beep. Press star to end your recording.").build();
    Record record = new Record.Builder()
        .action("/broadcast/hangup")
        .method(Method.POST)
        .finishOnKey("*")
        .build();

    VoiceResponse voiceResponse = new VoiceResponse.Builder()
        .say(say)
        .record(record)
        .build();

    try {
      return voiceResponse.toXml();
    } catch (TwiMLException e) {
      System.out.println("Unable to create Twiml Response");
      return "Unable to create Twiml Response";
    }
  }

  private void initializeTwilioClient() {
    String accountSid = null;
    String authToken = null;

    try {
      accountSid = appSetup.getAccountSid();
      authToken = appSetup.getAuthToken();
    } catch (UndefinedEnvironmentVariableException e) {
      System.out.println(e.getLocalizedMessage());
    }

    Twilio.init(accountSid, authToken);
  }
}

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?

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

If you're a Java 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 Java.

SMS and MMS Notifications

Send SMS alerts to a list of system administrators if something goes wrong on your server.

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!