Implemented Android Demo Application for VoIP API
The app showcased the ability to send real-time voice data between two endpoints using the VoIP API. Users can also configure session parameters such as the endpoint information and codec used. Bug: webrtc:11723 Change-Id: I682f4aa743b707759536bce59e598789a77b7ec6 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/178467 Reviewed-by: Kári Helgason <kthelgason@webrtc.org> Reviewed-by: Sami Kalliomäki <sakal@webrtc.org> Reviewed-by: Tim Na <natim@webrtc.org> Commit-Queue: Tim Na <natim@webrtc.org> Cr-Commit-Position: refs/heads/master@{#31775}
This commit is contained in:
@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.webrtc.examples.androidvoip;
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import org.webrtc.ContextUtils;
|
||||
|
||||
public class MainActivity extends Activity implements OnVoipClientTaskCompleted {
|
||||
private static final int NUM_SUPPORTED_CODECS = 6;
|
||||
|
||||
private VoipClient voipClient;
|
||||
private List<String> supportedCodecs;
|
||||
private boolean[] isDecoderSelected;
|
||||
private Set<Integer> selectedDecoders;
|
||||
|
||||
private Toast toast;
|
||||
private ScrollView scrollView;
|
||||
private TextView localIPAddressTextView;
|
||||
private EditText localPortNumberEditText;
|
||||
private EditText remoteIPAddressEditText;
|
||||
private EditText remotePortNumberEditText;
|
||||
private Spinner encoderSpinner;
|
||||
private Button decoderSelectionButton;
|
||||
private TextView decodersTextView;
|
||||
private ToggleButton sessionButton;
|
||||
private RelativeLayout switchLayout;
|
||||
private Switch sendSwitch;
|
||||
private Switch playoutSwitch;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstance) {
|
||||
ContextUtils.initialize(getApplicationContext());
|
||||
|
||||
super.onCreate(savedInstance);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
System.loadLibrary("examples_androidvoip_jni");
|
||||
|
||||
voipClient = new VoipClient(getApplicationContext(), this);
|
||||
voipClient.getAndSetUpLocalIPAddress();
|
||||
voipClient.getAndSetUpSupportedCodecs();
|
||||
|
||||
isDecoderSelected = new boolean[NUM_SUPPORTED_CODECS];
|
||||
selectedDecoders = new HashSet<>();
|
||||
|
||||
toast = Toast.makeText(this, "", Toast.LENGTH_SHORT);
|
||||
|
||||
scrollView = (ScrollView) findViewById(R.id.scroll_view);
|
||||
localIPAddressTextView = (TextView) findViewById(R.id.local_ip_address_text_view);
|
||||
localPortNumberEditText = (EditText) findViewById(R.id.local_port_number_edit_text);
|
||||
remoteIPAddressEditText = (EditText) findViewById(R.id.remote_ip_address_edit_text);
|
||||
remotePortNumberEditText = (EditText) findViewById(R.id.remote_port_number_edit_text);
|
||||
encoderSpinner = (Spinner) findViewById(R.id.encoder_spinner);
|
||||
decoderSelectionButton = (Button) findViewById(R.id.decoder_selection_button);
|
||||
decodersTextView = (TextView) findViewById(R.id.decoders_text_view);
|
||||
sessionButton = (ToggleButton) findViewById(R.id.session_button);
|
||||
switchLayout = (RelativeLayout) findViewById(R.id.switch_layout);
|
||||
sendSwitch = (Switch) findViewById(R.id.start_send_switch);
|
||||
playoutSwitch = (Switch) findViewById(R.id.start_playout_switch);
|
||||
|
||||
setUpSessionButton();
|
||||
setUpSendAndPlayoutSwitch();
|
||||
}
|
||||
|
||||
private void setUpEncoderSpinner(List<String> supportedCodecs) {
|
||||
ArrayAdapter<String> encoderAdapter =
|
||||
new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, supportedCodecs);
|
||||
encoderAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
encoderSpinner.setAdapter(encoderAdapter);
|
||||
encoderSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
voipClient.setEncoder((String) parent.getSelectedItem());
|
||||
}
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {}
|
||||
});
|
||||
}
|
||||
|
||||
private List<String> getSelectedDecoders() {
|
||||
List<String> decoders = new ArrayList<>();
|
||||
for (int i = 0; i < supportedCodecs.size(); i++) {
|
||||
if (selectedDecoders.contains(i)) {
|
||||
decoders.add(supportedCodecs.get(i));
|
||||
}
|
||||
}
|
||||
return decoders;
|
||||
}
|
||||
|
||||
private void setUpDecoderSelectionButton(List<String> supportedCodecs) {
|
||||
decoderSelectionButton.setOnClickListener((view) -> {
|
||||
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
|
||||
dialogBuilder.setTitle(R.string.dialog_title);
|
||||
|
||||
// Populate multi choice items with supported decoders.
|
||||
String[] supportedCodecsArray = supportedCodecs.toArray(new String[0]);
|
||||
dialogBuilder.setMultiChoiceItems(
|
||||
supportedCodecsArray, isDecoderSelected, (dialog, position, isChecked) -> {
|
||||
if (isChecked) {
|
||||
selectedDecoders.add(position);
|
||||
} else if (!isChecked) {
|
||||
selectedDecoders.remove(position);
|
||||
}
|
||||
});
|
||||
|
||||
// "Ok" button.
|
||||
dialogBuilder.setPositiveButton(R.string.ok_label, (dialog, position) -> {
|
||||
List<String> decoders = getSelectedDecoders();
|
||||
String result = decoders.stream().collect(Collectors.joining(", "));
|
||||
if (result.isEmpty()) {
|
||||
decodersTextView.setText(R.string.decoders_text_view_default);
|
||||
} else {
|
||||
decodersTextView.setText(result);
|
||||
}
|
||||
voipClient.setDecoders(decoders);
|
||||
});
|
||||
|
||||
// "Dismiss" button.
|
||||
dialogBuilder.setNegativeButton(
|
||||
R.string.dismiss_label, (dialog, position) -> { dialog.dismiss(); });
|
||||
|
||||
// "Clear All" button.
|
||||
dialogBuilder.setNeutralButton(R.string.clear_all_label, (dialog, position) -> {
|
||||
Arrays.fill(isDecoderSelected, false);
|
||||
selectedDecoders.clear();
|
||||
decodersTextView.setText(R.string.decoders_text_view_default);
|
||||
});
|
||||
|
||||
AlertDialog dialog = dialogBuilder.create();
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpSessionButton() {
|
||||
sessionButton.setOnCheckedChangeListener((button, isChecked) -> {
|
||||
// Ask for permission on RECORD_AUDIO if not granted.
|
||||
if (ContextCompat.checkSelfPermission(this, permission.RECORD_AUDIO)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
String[] sList = {permission.RECORD_AUDIO};
|
||||
ActivityCompat.requestPermissions(this, sList, 1);
|
||||
}
|
||||
|
||||
if (isChecked) {
|
||||
// Order matters here, addresses have to be set before starting session
|
||||
// before setting codec.
|
||||
voipClient.setLocalAddress(localIPAddressTextView.getText().toString(),
|
||||
Integer.parseInt(localPortNumberEditText.getText().toString()));
|
||||
voipClient.setRemoteAddress(remoteIPAddressEditText.getText().toString(),
|
||||
Integer.parseInt(remotePortNumberEditText.getText().toString()));
|
||||
voipClient.startSession();
|
||||
voipClient.setEncoder((String) encoderSpinner.getSelectedItem());
|
||||
voipClient.setDecoders(getSelectedDecoders());
|
||||
} else {
|
||||
voipClient.stopSession();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpSendAndPlayoutSwitch() {
|
||||
sendSwitch.setOnCheckedChangeListener((button, isChecked) -> {
|
||||
if (isChecked) {
|
||||
voipClient.startSend();
|
||||
} else {
|
||||
voipClient.stopSend();
|
||||
}
|
||||
});
|
||||
|
||||
playoutSwitch.setOnCheckedChangeListener((button, isChecked) -> {
|
||||
if (isChecked) {
|
||||
voipClient.startPlayout();
|
||||
} else {
|
||||
voipClient.stopPlayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpIPAddressEditTexts(String localIPAddress) {
|
||||
if (localIPAddress.isEmpty()) {
|
||||
showToast("Please check your network configuration");
|
||||
} else {
|
||||
localIPAddressTextView.setText(localIPAddress);
|
||||
// By default remote IP address is the same as local IP address.
|
||||
remoteIPAddressEditText.setText(localIPAddress);
|
||||
}
|
||||
}
|
||||
|
||||
private void showToast(String message) {
|
||||
toast.cancel();
|
||||
toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
|
||||
toast.setGravity(Gravity.TOP, 0, 200);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
voipClient.close();
|
||||
voipClient = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetLocalIPAddressCompleted(String localIPAddress) {
|
||||
runOnUiThread(() -> { setUpIPAddressEditTexts(localIPAddress); });
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetSupportedCodecsCompleted(List<String> supportedCodecs) {
|
||||
runOnUiThread(() -> {
|
||||
this.supportedCodecs = supportedCodecs;
|
||||
setUpEncoderSpinner(supportedCodecs);
|
||||
setUpDecoderSelectionButton(supportedCodecs);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVoipClientInitializationCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (!isSuccessful) {
|
||||
showToast("Error initializing audio device");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSessionCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (isSuccessful) {
|
||||
showToast("Session started");
|
||||
switchLayout.setVisibility(View.VISIBLE);
|
||||
scrollView.post(() -> { scrollView.fullScroll(ScrollView.FOCUS_DOWN); });
|
||||
} else {
|
||||
showToast("Failed to start session");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopSessionCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (isSuccessful) {
|
||||
showToast("Session stopped");
|
||||
// Set listeners to null so the checked state can be changed programmatically.
|
||||
sendSwitch.setOnCheckedChangeListener(null);
|
||||
playoutSwitch.setOnCheckedChangeListener(null);
|
||||
sendSwitch.setChecked(false);
|
||||
playoutSwitch.setChecked(false);
|
||||
// Redo the switch listener setup.
|
||||
setUpSendAndPlayoutSwitch();
|
||||
switchLayout.setVisibility(View.GONE);
|
||||
} else {
|
||||
showToast("Failed to stop session");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSendCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (isSuccessful) {
|
||||
showToast("Started sending");
|
||||
} else {
|
||||
showToast("Error initializing microphone");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopSendCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (isSuccessful) {
|
||||
showToast("Stopped sending");
|
||||
} else {
|
||||
showToast("Microphone termination failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartPlayoutCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (isSuccessful) {
|
||||
showToast("Started playout");
|
||||
} else {
|
||||
showToast("Error initializing speaker");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopPlayoutCompleted(boolean isSuccessful) {
|
||||
runOnUiThread(() -> {
|
||||
if (isSuccessful) {
|
||||
showToast("Stopped playout");
|
||||
} else {
|
||||
showToast("Speaker termination failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUninitializedVoipClient() {
|
||||
runOnUiThread(() -> { showToast("Voip client is uninitialized"); });
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.webrtc.examples.androidvoip;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OnVoipClientTaskCompleted {
|
||||
void onGetLocalIPAddressCompleted(String localIPAddress);
|
||||
void onGetSupportedCodecsCompleted(List<String> supportedCodecs);
|
||||
void onVoipClientInitializationCompleted(boolean isSuccessful);
|
||||
void onStartSessionCompleted(boolean isSuccessful);
|
||||
void onStopSessionCompleted(boolean isSuccessful);
|
||||
void onStartSendCompleted(boolean isSuccessful);
|
||||
void onStopSendCompleted(boolean isSuccessful);
|
||||
void onStartPlayoutCompleted(boolean isSuccessful);
|
||||
void onStopPlayoutCompleted(boolean isSuccessful);
|
||||
void onUninitializedVoipClient();
|
||||
}
|
||||
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.webrtc.examples.androidvoip;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class VoipClient {
|
||||
private static final String TAG = "VoipClient";
|
||||
|
||||
private final HandlerThread thread;
|
||||
private final Handler handler;
|
||||
|
||||
private long nativeClient;
|
||||
private OnVoipClientTaskCompleted listener;
|
||||
|
||||
public VoipClient(Context applicationContext, OnVoipClientTaskCompleted listener) {
|
||||
this.listener = listener;
|
||||
thread = new HandlerThread(TAG + "Thread");
|
||||
thread.start();
|
||||
handler = new Handler(thread.getLooper());
|
||||
|
||||
handler.post(() -> {
|
||||
nativeClient = nativeCreateClient(applicationContext);
|
||||
listener.onVoipClientInitializationCompleted(/* isSuccessful */ nativeClient != 0);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isInitialized() {
|
||||
return nativeClient != 0;
|
||||
}
|
||||
|
||||
public void getAndSetUpSupportedCodecs() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onGetSupportedCodecsCompleted(nativeGetSupportedCodecs(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getAndSetUpLocalIPAddress() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onGetLocalIPAddressCompleted(nativeGetLocalIPAddress(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setEncoder(String encoder) {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
nativeSetEncoder(nativeClient, encoder);
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setDecoders(List<String> decoders) {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
nativeSetDecoders(nativeClient, decoders);
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setLocalAddress(String ipAddress, int portNumber) {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
nativeSetLocalAddress(nativeClient, ipAddress, portNumber);
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setRemoteAddress(String ipAddress, int portNumber) {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
nativeSetRemoteAddress(nativeClient, ipAddress, portNumber);
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void startSession() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onStartSessionCompleted(nativeStartSession(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopSession() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onStopSessionCompleted(nativeStopSession(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void startSend() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onStartSendCompleted(nativeStartSend(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopSend() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onStopSendCompleted(nativeStopSend(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void startPlayout() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onStartPlayoutCompleted(nativeStartPlayout(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopPlayout() {
|
||||
handler.post(() -> {
|
||||
if (isInitialized()) {
|
||||
listener.onStopPlayoutCompleted(nativeStopPlayout(nativeClient));
|
||||
} else {
|
||||
listener.onUninitializedVoipClient();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
handler.post(() -> {
|
||||
nativeDelete(nativeClient);
|
||||
nativeClient = 0;
|
||||
});
|
||||
thread.quitSafely();
|
||||
}
|
||||
|
||||
private static native long nativeCreateClient(Context applicationContext);
|
||||
private static native List<String> nativeGetSupportedCodecs(long nativeAndroidVoipClient);
|
||||
private static native String nativeGetLocalIPAddress(long nativeAndroidVoipClient);
|
||||
private static native void nativeSetEncoder(long nativeAndroidVoipClient, String encoder);
|
||||
private static native void nativeSetDecoders(long nativeAndroidVoipClient, List<String> decoders);
|
||||
private static native void nativeSetLocalAddress(
|
||||
long nativeAndroidVoipClient, String ipAddress, int portNumber);
|
||||
private static native void nativeSetRemoteAddress(
|
||||
long nativeAndroidVoipClient, String ipAddress, int portNumber);
|
||||
private static native boolean nativeStartSession(long nativeAndroidVoipClient);
|
||||
private static native boolean nativeStopSession(long nativeAndroidVoipClient);
|
||||
private static native boolean nativeStartSend(long nativeAndroidVoipClient);
|
||||
private static native boolean nativeStopSend(long nativeAndroidVoipClient);
|
||||
private static native boolean nativeStartPlayout(long nativeAndroidVoipClient);
|
||||
private static native boolean nativeStopPlayout(long nativeAndroidVoipClient);
|
||||
private static native void nativeDelete(long nativeAndroidVoipClient);
|
||||
}
|
||||
Reference in New Issue
Block a user