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:
@ -27,6 +27,7 @@ group("examples") {
|
|||||||
":AppRTCMobile",
|
":AppRTCMobile",
|
||||||
":AppRTCMobile_test_apk",
|
":AppRTCMobile_test_apk",
|
||||||
":libwebrtc_unity",
|
":libwebrtc_unity",
|
||||||
|
"androidvoip",
|
||||||
]
|
]
|
||||||
|
|
||||||
# TODO(sakal): We include some code from the tests. Remove this dependency
|
# TODO(sakal): We include some code from the tests. Remove this dependency
|
||||||
|
38
examples/androidvoip/AndroidManifest.xml
Normal file
38
examples/androidvoip/AndroidManifest.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
* Copyright 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.webrtc.examples.androidvoip">
|
||||||
|
|
||||||
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="27" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||||
|
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true">
|
||||||
|
<activity android:name=".MainActivity"
|
||||||
|
android:windowSoftInputMode="stateHidden">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
88
examples/androidvoip/BUILD.gn
Normal file
88
examples/androidvoip/BUILD.gn
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import("//webrtc.gni")
|
||||||
|
|
||||||
|
if (is_android) {
|
||||||
|
rtc_android_apk("androidvoip") {
|
||||||
|
testonly = true
|
||||||
|
apk_name = "androidvoip"
|
||||||
|
android_manifest = "AndroidManifest.xml"
|
||||||
|
min_sdk_version = 21
|
||||||
|
target_sdk_version = 27
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
"java/org/webrtc/examples/androidvoip/MainActivity.java",
|
||||||
|
"java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java",
|
||||||
|
"java/org/webrtc/examples/androidvoip/VoipClient.java",
|
||||||
|
]
|
||||||
|
|
||||||
|
deps = [
|
||||||
|
":resources",
|
||||||
|
"//modules/audio_device:audio_device_java",
|
||||||
|
"//rtc_base:base_java",
|
||||||
|
"//sdk/android:java_audio_device_module_java",
|
||||||
|
"//sdk/android:video_java",
|
||||||
|
"//third_party/android_deps:androidx_core_core_java",
|
||||||
|
"//third_party/android_deps:androidx_legacy_legacy_support_v4_java",
|
||||||
|
]
|
||||||
|
|
||||||
|
shared_libraries = [ ":examples_androidvoip_jni" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_jni("generated_jni") {
|
||||||
|
testonly = true
|
||||||
|
sources = [ "java/org/webrtc/examples/androidvoip/VoipClient.java" ]
|
||||||
|
namespace = "webrtc_examples"
|
||||||
|
jni_generator_include = "//sdk/android/src/jni/jni_generator_helper.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
rtc_shared_library("examples_androidvoip_jni") {
|
||||||
|
testonly = true
|
||||||
|
sources = [
|
||||||
|
"jni/android_voip_client.cc",
|
||||||
|
"jni/android_voip_client.h",
|
||||||
|
"jni/onload.cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
suppressed_configs += [ "//build/config/android:hide_all_but_jni_onload" ]
|
||||||
|
configs += [ "//build/config/android:hide_all_but_jni" ]
|
||||||
|
|
||||||
|
deps = [
|
||||||
|
":generated_jni",
|
||||||
|
"//api:transport_api",
|
||||||
|
"//api/audio_codecs:audio_codecs_api",
|
||||||
|
"//api/audio_codecs:builtin_audio_decoder_factory",
|
||||||
|
"//api/audio_codecs:builtin_audio_encoder_factory",
|
||||||
|
"//api/task_queue:default_task_queue_factory",
|
||||||
|
"//api/voip:voip_api",
|
||||||
|
"//api/voip:voip_engine_factory",
|
||||||
|
"//modules/utility:utility",
|
||||||
|
"//rtc_base",
|
||||||
|
"//rtc_base/third_party/sigslot:sigslot",
|
||||||
|
"//sdk/android:native_api_audio_device_module",
|
||||||
|
"//sdk/android:native_api_base",
|
||||||
|
"//sdk/android:native_api_jni",
|
||||||
|
"//third_party/abseil-cpp/absl/memory:memory",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
android_resources("resources") {
|
||||||
|
testonly = true
|
||||||
|
custom_package = "org.webrtc.examples.androidvoip"
|
||||||
|
sources = [
|
||||||
|
"res/layout/activity_main.xml",
|
||||||
|
"res/values/colors.xml",
|
||||||
|
"res/values/strings.xml",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Needed for Bazel converter.
|
||||||
|
resource_dirs = [ "res" ]
|
||||||
|
assert(resource_dirs != []) # Mark as used.
|
||||||
|
}
|
||||||
|
}
|
3
examples/androidvoip/DEPS
Normal file
3
examples/androidvoip/DEPS
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
include_rules = [
|
||||||
|
"+sdk/android/native_api",
|
||||||
|
]
|
2
examples/androidvoip/OWNERS
Normal file
2
examples/androidvoip/OWNERS
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
natim@webrtc.org
|
||||||
|
sakal@webrtc.org
|
@ -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);
|
||||||
|
}
|
405
examples/androidvoip/jni/android_voip_client.cc
Normal file
405
examples/androidvoip/jni/android_voip_client.cc
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "examples/androidvoip/jni/android_voip_client.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "absl/memory/memory.h"
|
||||||
|
#include "api/audio_codecs/builtin_audio_decoder_factory.h"
|
||||||
|
#include "api/audio_codecs/builtin_audio_encoder_factory.h"
|
||||||
|
#include "api/task_queue/default_task_queue_factory.h"
|
||||||
|
#include "api/voip/voip_codec.h"
|
||||||
|
#include "api/voip/voip_engine_factory.h"
|
||||||
|
#include "api/voip/voip_network.h"
|
||||||
|
#include "examples/androidvoip/generated_jni/VoipClient_jni.h"
|
||||||
|
#include "rtc_base/logging.h"
|
||||||
|
#include "rtc_base/network.h"
|
||||||
|
#include "rtc_base/socket_server.h"
|
||||||
|
#include "sdk/android/native_api/audio_device_module/audio_device_android.h"
|
||||||
|
#include "sdk/android/native_api/jni/java_types.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Connects a UDP socket to a public address and returns the local
|
||||||
|
// address associated with it. Since it binds to the "any" address
|
||||||
|
// internally, it returns the default local address on a multi-homed
|
||||||
|
// endpoint. Implementation copied from
|
||||||
|
// BasicNetworkManager::QueryDefaultLocalAddress.
|
||||||
|
rtc::IPAddress QueryDefaultLocalAddress(int family) {
|
||||||
|
const char kPublicIPv4Host[] = "8.8.8.8";
|
||||||
|
const char kPublicIPv6Host[] = "2001:4860:4860::8888";
|
||||||
|
const int kPublicPort = 53;
|
||||||
|
std::unique_ptr<rtc::Thread> thread = rtc::Thread::CreateWithSocketServer();
|
||||||
|
|
||||||
|
RTC_DCHECK(thread->socketserver() != nullptr);
|
||||||
|
RTC_DCHECK(family == AF_INET || family == AF_INET6);
|
||||||
|
|
||||||
|
std::unique_ptr<rtc::AsyncSocket> socket(
|
||||||
|
thread->socketserver()->CreateAsyncSocket(family, SOCK_DGRAM));
|
||||||
|
if (!socket) {
|
||||||
|
RTC_LOG_ERR(LERROR) << "Socket creation failed";
|
||||||
|
return rtc::IPAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto host = family == AF_INET ? kPublicIPv4Host : kPublicIPv6Host;
|
||||||
|
if (socket->Connect(rtc::SocketAddress(host, kPublicPort)) < 0) {
|
||||||
|
if (socket->GetError() != ENETUNREACH &&
|
||||||
|
socket->GetError() != EHOSTUNREACH) {
|
||||||
|
RTC_LOG(LS_INFO) << "Connect failed with " << socket->GetError();
|
||||||
|
}
|
||||||
|
return rtc::IPAddress();
|
||||||
|
}
|
||||||
|
return socket->GetLocalAddress().ipaddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assigned payload type for supported built-in codecs. PCMU, PCMA,
|
||||||
|
// and G722 have set payload types. Whereas opus, ISAC, and ILBC
|
||||||
|
// have dynamic payload types.
|
||||||
|
enum class PayloadType : int {
|
||||||
|
kPcmu = 0,
|
||||||
|
kPcma = 8,
|
||||||
|
kG722 = 9,
|
||||||
|
kOpus = 96,
|
||||||
|
kIsac = 97,
|
||||||
|
kIlbc = 98,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the payload type corresponding to codec_name. Only
|
||||||
|
// supports the built-in codecs.
|
||||||
|
int GetPayloadType(const std::string& codec_name) {
|
||||||
|
RTC_DCHECK(codec_name == "PCMU" || codec_name == "PCMA" ||
|
||||||
|
codec_name == "G722" || codec_name == "opus" ||
|
||||||
|
codec_name == "ISAC" || codec_name == "ILBC");
|
||||||
|
|
||||||
|
if (codec_name == "PCMU") {
|
||||||
|
return static_cast<int>(PayloadType::kPcmu);
|
||||||
|
} else if (codec_name == "PCMA") {
|
||||||
|
return static_cast<int>(PayloadType::kPcma);
|
||||||
|
} else if (codec_name == "G722") {
|
||||||
|
return static_cast<int>(PayloadType::kG722);
|
||||||
|
} else if (codec_name == "opus") {
|
||||||
|
return static_cast<int>(PayloadType::kOpus);
|
||||||
|
} else if (codec_name == "ISAC") {
|
||||||
|
return static_cast<int>(PayloadType::kIsac);
|
||||||
|
} else if (codec_name == "ILBC") {
|
||||||
|
return static_cast<int>(PayloadType::kIlbc);
|
||||||
|
}
|
||||||
|
|
||||||
|
RTC_NOTREACHED();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace webrtc_examples {
|
||||||
|
|
||||||
|
AndroidVoipClient::AndroidVoipClient(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& application_context) {
|
||||||
|
voip_thread_ = rtc::Thread::CreateWithSocketServer();
|
||||||
|
voip_thread_->Start();
|
||||||
|
|
||||||
|
webrtc::VoipEngineConfig config;
|
||||||
|
config.encoder_factory = webrtc::CreateBuiltinAudioEncoderFactory();
|
||||||
|
config.decoder_factory = webrtc::CreateBuiltinAudioDecoderFactory();
|
||||||
|
config.task_queue_factory = webrtc::CreateDefaultTaskQueueFactory();
|
||||||
|
config.audio_device_module =
|
||||||
|
webrtc::CreateJavaAudioDeviceModule(env, application_context.obj());
|
||||||
|
config.audio_processing = webrtc::AudioProcessingBuilder().Create();
|
||||||
|
|
||||||
|
supported_codecs_ = config.encoder_factory->GetSupportedEncoders();
|
||||||
|
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/audio_device/android/audio_device_template.h,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
voip_thread_->Invoke<void>(RTC_FROM_HERE, [&] {
|
||||||
|
voip_engine_ = webrtc::CreateVoipEngine(std::move(config));
|
||||||
|
if (!voip_engine_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "VoipEngine creation failed";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidVoipClient::~AndroidVoipClient() {
|
||||||
|
voip_thread_->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidVoipClient* AndroidVoipClient::Create(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& application_context) {
|
||||||
|
// Using `new` to access a non-public constructor.
|
||||||
|
auto voip_client =
|
||||||
|
absl::WrapUnique(new AndroidVoipClient(env, application_context));
|
||||||
|
if (!voip_client->voip_engine_) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return voip_client.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
webrtc::ScopedJavaLocalRef<jobject> AndroidVoipClient::GetSupportedCodecs(
|
||||||
|
JNIEnv* env) {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const webrtc::AudioCodecSpec& spec : supported_codecs_) {
|
||||||
|
names.push_back(spec.format.name);
|
||||||
|
}
|
||||||
|
webrtc::ScopedJavaLocalRef<jstring> (*convert_function)(
|
||||||
|
JNIEnv*, const std::string&) = &webrtc::NativeToJavaString;
|
||||||
|
return NativeToJavaList(env, names, convert_function);
|
||||||
|
}
|
||||||
|
|
||||||
|
webrtc::ScopedJavaLocalRef<jstring> AndroidVoipClient::GetLocalIPAddress(
|
||||||
|
JNIEnv* env) {
|
||||||
|
rtc::IPAddress ipv4_address = QueryDefaultLocalAddress(AF_INET);
|
||||||
|
if (!ipv4_address.IsNil()) {
|
||||||
|
return webrtc::NativeToJavaString(env, ipv4_address.ToString());
|
||||||
|
}
|
||||||
|
rtc::IPAddress ipv6_address = QueryDefaultLocalAddress(AF_INET6);
|
||||||
|
if (!ipv6_address.IsNil()) {
|
||||||
|
return webrtc::NativeToJavaString(env, ipv6_address.ToString());
|
||||||
|
}
|
||||||
|
return webrtc::NativeToJavaString(env, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::SetEncoder(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaRef<jstring>& j_encoder_string) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::string& chosen_encoder =
|
||||||
|
webrtc::JavaToNativeString(env, j_encoder_string);
|
||||||
|
for (const webrtc::AudioCodecSpec& encoder : supported_codecs_) {
|
||||||
|
if (encoder.format.name == chosen_encoder) {
|
||||||
|
voip_engine_->Codec().SetSendCodec(
|
||||||
|
*channel_, GetPayloadType(encoder.format.name), encoder.format);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::SetDecoders(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& j_decoder_strings) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::vector<std::string> chosen_decoders =
|
||||||
|
webrtc::JavaListToNativeVector<std::string, jstring>(
|
||||||
|
env, j_decoder_strings, &webrtc::JavaToNativeString);
|
||||||
|
std::map<int, webrtc::SdpAudioFormat> decoder_specs;
|
||||||
|
|
||||||
|
for (const webrtc::AudioCodecSpec& decoder : supported_codecs_) {
|
||||||
|
if (std::find(chosen_decoders.begin(), chosen_decoders.end(),
|
||||||
|
decoder.format.name) != chosen_decoders.end()) {
|
||||||
|
decoder_specs.insert(
|
||||||
|
{GetPayloadType(decoder.format.name), decoder.format});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voip_engine_->Codec().SetReceiveCodecs(*channel_, decoder_specs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::SetLocalAddress(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaRef<jstring>& j_ip_address_string,
|
||||||
|
jint j_port_number_int) {
|
||||||
|
const std::string& ip_address =
|
||||||
|
webrtc::JavaToNativeString(env, j_ip_address_string);
|
||||||
|
rtp_local_address_ = rtc::SocketAddress(ip_address, j_port_number_int);
|
||||||
|
rtcp_local_address_ = rtc::SocketAddress(ip_address, j_port_number_int + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::SetRemoteAddress(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaRef<jstring>& j_ip_address_string,
|
||||||
|
jint j_port_number_int) {
|
||||||
|
const std::string& ip_address =
|
||||||
|
webrtc::JavaToNativeString(env, j_ip_address_string);
|
||||||
|
rtp_remote_address_ = rtc::SocketAddress(ip_address, j_port_number_int);
|
||||||
|
rtcp_remote_address_ = rtc::SocketAddress(ip_address, j_port_number_int + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean AndroidVoipClient::StartSession(JNIEnv* env) {
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/utility/source/process_thread_impl.cc,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
channel_ = voip_thread_->Invoke<absl::optional<webrtc::ChannelId>>(
|
||||||
|
RTC_FROM_HERE,
|
||||||
|
[this] { return voip_engine_->Base().CreateChannel(this, 0); });
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel creation failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtp_socket_.reset(rtc::AsyncUDPSocket::Create(voip_thread_->socketserver(),
|
||||||
|
rtp_local_address_));
|
||||||
|
if (!rtp_socket_) {
|
||||||
|
RTC_LOG_ERR(LERROR) << "Socket creation failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rtp_socket_->SignalReadPacket.connect(
|
||||||
|
this, &AndroidVoipClient::OnSignalReadRTPPacket);
|
||||||
|
|
||||||
|
rtcp_socket_.reset(rtc::AsyncUDPSocket::Create(voip_thread_->socketserver(),
|
||||||
|
rtcp_local_address_));
|
||||||
|
if (!rtcp_socket_) {
|
||||||
|
RTC_LOG_ERR(LERROR) << "Socket creation failed";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rtcp_socket_->SignalReadPacket.connect(
|
||||||
|
this, &AndroidVoipClient::OnSignalReadRTCPPacket);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean AndroidVoipClient::StopSession(JNIEnv* env) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!StopSend(env) || !StopPlayout(env)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtp_socket_->Close();
|
||||||
|
rtcp_socket_->Close();
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/utility/source/process_thread_impl.cc,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
voip_thread_->Invoke<void>(RTC_FROM_HERE, [this] {
|
||||||
|
voip_engine_->Base().ReleaseChannel(*channel_);
|
||||||
|
});
|
||||||
|
channel_ = absl::nullopt;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean AndroidVoipClient::StartSend(JNIEnv* env) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/audio_device/android/opensles_recorder.cc,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
|
||||||
|
return voip_engine_->Base().StartSend(*channel_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean AndroidVoipClient::StopSend(JNIEnv* env) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/audio_device/android/opensles_recorder.cc,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
|
||||||
|
return voip_engine_->Base().StopSend(*channel_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean AndroidVoipClient::StartPlayout(JNIEnv* env) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/audio_device/android/opensles_player.cc,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
|
||||||
|
return voip_engine_->Base().StartPlayout(*channel_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
jboolean AndroidVoipClient::StopPlayout(JNIEnv* env) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Due to consistent thread requirement on
|
||||||
|
// modules/audio_device/android/opensles_player.cc,
|
||||||
|
// code is invoked in the context of voip_thread_.
|
||||||
|
return voip_thread_->Invoke<bool>(RTC_FROM_HERE, [this] {
|
||||||
|
return voip_engine_->Base().StopPlayout(*channel_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::Delete(JNIEnv* env) {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AndroidVoipClient::SendRtp(const uint8_t* packet,
|
||||||
|
size_t length,
|
||||||
|
const webrtc::PacketOptions& options) {
|
||||||
|
if (!rtp_socket_->SendTo(packet, length, rtp_remote_address_,
|
||||||
|
rtc::PacketOptions())) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Failed to send RTP packet";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AndroidVoipClient::SendRtcp(const uint8_t* packet, size_t length) {
|
||||||
|
if (!rtcp_socket_->SendTo(packet, length, rtcp_remote_address_,
|
||||||
|
rtc::PacketOptions())) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Failed to send RTCP packet";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::OnSignalReadRTPPacket(rtc::AsyncPacketSocket* socket,
|
||||||
|
const char* rtp_packet,
|
||||||
|
size_t size,
|
||||||
|
const rtc::SocketAddress& addr,
|
||||||
|
const int64_t& timestamp) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
voip_engine_->Network().ReceivedRTPPacket(
|
||||||
|
*channel_, rtc::ArrayView<const uint8_t>(
|
||||||
|
reinterpret_cast<const uint8_t*>(rtp_packet), size));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AndroidVoipClient::OnSignalReadRTCPPacket(rtc::AsyncPacketSocket* socket,
|
||||||
|
const char* rtcp_packet,
|
||||||
|
size_t size,
|
||||||
|
const rtc::SocketAddress& addr,
|
||||||
|
const int64_t& timestamp) {
|
||||||
|
if (!channel_) {
|
||||||
|
RTC_LOG(LS_ERROR) << "Channel has not been created";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
voip_engine_->Network().ReceivedRTCPPacket(
|
||||||
|
*channel_, rtc::ArrayView<const uint8_t>(
|
||||||
|
reinterpret_cast<const uint8_t*>(rtcp_packet), size));
|
||||||
|
}
|
||||||
|
|
||||||
|
static jlong JNI_VoipClient_CreateClient(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& application_context) {
|
||||||
|
return webrtc::NativeToJavaPointer(
|
||||||
|
AndroidVoipClient::Create(env, application_context));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace webrtc_examples
|
156
examples/androidvoip/jni/android_voip_client.h
Normal file
156
examples/androidvoip/jni/android_voip_client.h
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_
|
||||||
|
#define EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "api/audio_codecs/audio_format.h"
|
||||||
|
#include "api/call/transport.h"
|
||||||
|
#include "api/voip/voip_base.h"
|
||||||
|
#include "api/voip/voip_engine.h"
|
||||||
|
#include "rtc_base/async_packet_socket.h"
|
||||||
|
#include "rtc_base/async_udp_socket.h"
|
||||||
|
#include "rtc_base/socket_address.h"
|
||||||
|
#include "rtc_base/third_party/sigslot/sigslot.h"
|
||||||
|
#include "rtc_base/thread.h"
|
||||||
|
#include "sdk/android/native_api/jni/scoped_java_ref.h"
|
||||||
|
|
||||||
|
namespace webrtc_examples {
|
||||||
|
|
||||||
|
// AndroidVoipClient facilitates the use of the VoIP API defined in
|
||||||
|
// api/voip/voip_engine.h. One instance of AndroidVoipClient should
|
||||||
|
// suffice for most VoIP applications. AndroidVoipClient implements
|
||||||
|
// webrtc::Transport to send RTP/RTCP packets to the remote endpoint.
|
||||||
|
// It also creates methods (slots) for sockets to connect to in
|
||||||
|
// order to receive RTP/RTCP packets. AndroidVoipClient does all
|
||||||
|
// VoipBase related operations with rtc::Thread (voip_thread_), this
|
||||||
|
// is to comply with consistent thread usage requirement with
|
||||||
|
// ProcessThread used within VoipEngine. AndroidVoipClient is meant
|
||||||
|
// to be used by Java through JNI.
|
||||||
|
class AndroidVoipClient : public webrtc::Transport,
|
||||||
|
public sigslot::has_slots<> {
|
||||||
|
public:
|
||||||
|
// Returns a pointer to an AndroidVoipClient object. Clients should
|
||||||
|
// use this factory method to create AndroidVoipClient objects. The
|
||||||
|
// method will return a nullptr in case of initialization errors.
|
||||||
|
// It is the client's responsibility to delete the pointer when
|
||||||
|
// they are done with it (this class provides a Delete() method).
|
||||||
|
static AndroidVoipClient* Create(
|
||||||
|
JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& application_context);
|
||||||
|
|
||||||
|
~AndroidVoipClient() override;
|
||||||
|
|
||||||
|
// Returns a Java List of Strings containing names of the built-in
|
||||||
|
// supported codecs.
|
||||||
|
webrtc::ScopedJavaLocalRef<jobject> GetSupportedCodecs(JNIEnv* env);
|
||||||
|
|
||||||
|
// Returns a Java String of the default local IPv4 address. If IPv4
|
||||||
|
// address is not found, returns the default local IPv6 address. If
|
||||||
|
// IPv6 address is not found, returns an empty string.
|
||||||
|
webrtc::ScopedJavaLocalRef<jstring> GetLocalIPAddress(JNIEnv* env);
|
||||||
|
|
||||||
|
// Sets the encoder used by the VoIP API.
|
||||||
|
void SetEncoder(JNIEnv* env,
|
||||||
|
const webrtc::JavaRef<jstring>& j_encoder_string);
|
||||||
|
|
||||||
|
// Sets the decoders used by the VoIP API.
|
||||||
|
void SetDecoders(JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& j_decoder_strings);
|
||||||
|
|
||||||
|
// Sets two local/remote addresses, one for RTP packets, and another for
|
||||||
|
// RTCP packets. The RTP address will have IP address j_ip_address_string
|
||||||
|
// and port number j_port_number_int, the RTCP address will have IP address
|
||||||
|
// j_ip_address_string and port number j_port_number_int+1.
|
||||||
|
void SetLocalAddress(JNIEnv* env,
|
||||||
|
const webrtc::JavaRef<jstring>& j_ip_address_string,
|
||||||
|
jint j_port_number_int);
|
||||||
|
void SetRemoteAddress(JNIEnv* env,
|
||||||
|
const webrtc::JavaRef<jstring>& j_ip_address_string,
|
||||||
|
jint j_port_number_int);
|
||||||
|
|
||||||
|
// Starts a VoIP session. The VoIP operations below can only be
|
||||||
|
// used after a session has already started. Returns true if session
|
||||||
|
// started successfully and false otherwise.
|
||||||
|
jboolean StartSession(JNIEnv* env);
|
||||||
|
|
||||||
|
// Stops the current session. Returns true if session stopped
|
||||||
|
// successfully and false otherwise.
|
||||||
|
jboolean StopSession(JNIEnv* env);
|
||||||
|
|
||||||
|
// Starts sending RTP/RTCP packets to the remote endpoint. Returns
|
||||||
|
// the return value of StartSend in api/voip/voip_base.h.
|
||||||
|
jboolean StartSend(JNIEnv* env);
|
||||||
|
|
||||||
|
// Stops sending RTP/RTCP packets to the remote endpoint. Returns
|
||||||
|
// the return value of StopSend in api/voip/voip_base.h.
|
||||||
|
jboolean StopSend(JNIEnv* env);
|
||||||
|
|
||||||
|
// Starts playing out the voice data received from the remote endpoint.
|
||||||
|
// Returns the return value of StartPlayout in api/voip/voip_base.h.
|
||||||
|
jboolean StartPlayout(JNIEnv* env);
|
||||||
|
|
||||||
|
// Stops playing out the voice data received from the remote endpoint.
|
||||||
|
// Returns the return value of StopPlayout in api/voip/voip_base.h.
|
||||||
|
jboolean StopPlayout(JNIEnv* env);
|
||||||
|
|
||||||
|
// Deletes this object. Used by client when they are done.
|
||||||
|
void Delete(JNIEnv* env);
|
||||||
|
|
||||||
|
// Implementation for Transport.
|
||||||
|
bool SendRtp(const uint8_t* packet,
|
||||||
|
size_t length,
|
||||||
|
const webrtc::PacketOptions& options) override;
|
||||||
|
bool SendRtcp(const uint8_t* packet, size_t length) override;
|
||||||
|
|
||||||
|
// Slots for sockets to connect to.
|
||||||
|
void OnSignalReadRTPPacket(rtc::AsyncPacketSocket* socket,
|
||||||
|
const char* rtp_packet,
|
||||||
|
size_t size,
|
||||||
|
const rtc::SocketAddress& addr,
|
||||||
|
const int64_t& timestamp);
|
||||||
|
void OnSignalReadRTCPPacket(rtc::AsyncPacketSocket* socket,
|
||||||
|
const char* rtcp_packet,
|
||||||
|
size_t size,
|
||||||
|
const rtc::SocketAddress& addr,
|
||||||
|
const int64_t& timestamp);
|
||||||
|
|
||||||
|
private:
|
||||||
|
AndroidVoipClient(JNIEnv* env,
|
||||||
|
const webrtc::JavaParamRef<jobject>& application_context);
|
||||||
|
|
||||||
|
// Used to invoke VoipBase operations and send/receive
|
||||||
|
// RTP/RTCP packets.
|
||||||
|
std::unique_ptr<rtc::Thread> voip_thread_;
|
||||||
|
// A list of AudioCodecSpec supported by the built-in
|
||||||
|
// encoder/decoder factories.
|
||||||
|
std::vector<webrtc::AudioCodecSpec> supported_codecs_;
|
||||||
|
// The entry point to all VoIP APIs.
|
||||||
|
std::unique_ptr<webrtc::VoipEngine> voip_engine_;
|
||||||
|
// Used by the VoIP API to facilitate a VoIP session.
|
||||||
|
absl::optional<webrtc::ChannelId> channel_;
|
||||||
|
// Members below are used for network related operations.
|
||||||
|
std::unique_ptr<rtc::AsyncUDPSocket> rtp_socket_;
|
||||||
|
std::unique_ptr<rtc::AsyncUDPSocket> rtcp_socket_;
|
||||||
|
rtc::SocketAddress rtp_local_address_;
|
||||||
|
rtc::SocketAddress rtcp_local_address_;
|
||||||
|
rtc::SocketAddress rtp_remote_address_;
|
||||||
|
rtc::SocketAddress rtcp_remote_address_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace webrtc_examples
|
||||||
|
|
||||||
|
#endif // EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_
|
28
examples/androidvoip/jni/onload.cc
Normal file
28
examples/androidvoip/jni/onload.cc
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#include "rtc_base/ssl_adapter.h"
|
||||||
|
#include "sdk/android/native_api/base/init.h"
|
||||||
|
|
||||||
|
namespace webrtc_examples {
|
||||||
|
|
||||||
|
extern "C" jint JNIEXPORT JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved) {
|
||||||
|
webrtc::InitAndroid(jvm);
|
||||||
|
RTC_CHECK(rtc::InitializeSSL()) << "Failed to InitializeSSL()";
|
||||||
|
return JNI_VERSION_1_6;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void JNIEXPORT JNICALL JNI_OnUnLoad(JavaVM* jvm, void* reserved) {
|
||||||
|
RTC_CHECK(rtc::CleanupSSL()) << "Failed to CleanupSSL()";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace webrtc_examples
|
303
examples/androidvoip/res/layout/activity_main.xml
Normal file
303
examples/androidvoip/res/layout/activity_main.xml
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/scroll_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
tools:context="org.webrtc.examples.androidvoip.MainActivity">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:text="@string/local_endpoint_text_view"
|
||||||
|
android:textSize="19dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/almost_black" />
|
||||||
|
|
||||||
|
<!--Local IP Adress-->
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:text="@string/ip_address_text_view"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/local_ip_address_text_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!--Local Port Number-->
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:text="@string/port_number_text_view"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/local_port_number_edit_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="10000"
|
||||||
|
android:inputType="number"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:text="@string/remote_endpoint_text_view"
|
||||||
|
android:textSize="19dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/almost_black" />
|
||||||
|
|
||||||
|
<!--Remote IP Adress-->
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:text="@string/ip_address_text_view"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/remote_ip_address_edit_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:inputType="number"
|
||||||
|
android:digits="0123456789."
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!--Remote Port Number-->
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:text="@string/port_number_text_view"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/remote_port_number_edit_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="10000"
|
||||||
|
android:inputType="number"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:text="@string/encoder_text_view"
|
||||||
|
android:textSize="19dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/almost_black" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/encoder_spinner"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginTop="10dp"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="25dp"
|
||||||
|
android:text="@string/decoder_text_view"
|
||||||
|
android:textSize="19dp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/almost_black" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/decoder_selection_button"
|
||||||
|
android:text="@string/decoder_selection_button"
|
||||||
|
style="?android:attr/buttonBarButtonStyle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/decoders_text_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:layout_marginBottom="30dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:text="@string/decoders_text_view_default"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/switch_layout"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:visibility="gone" >
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:layout_marginBottom="45dp"
|
||||||
|
android:background="@color/light_gray" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/start_send_switch_layout"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_below="@id/divider" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:gravity="left"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/start_send_text_view"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/start_send_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:gravity="right"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_below="@id/start_send_switch_layout">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/start_playout_text_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginLeft="15dp"
|
||||||
|
android:gravity="left"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/start_playout_text_view"
|
||||||
|
android:textSize="16dp" />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
android:id="@+id/start_playout_switch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="15dp"
|
||||||
|
android:gravity="right"
|
||||||
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
android:id="@+id/session_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:textOff="@string/session_button_text_off"
|
||||||
|
android:textOn="@string/session_button_text_on"
|
||||||
|
style="?android:attr/buttonStyle" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</ScrollView>
|
5
examples/androidvoip/res/values/colors.xml
Normal file
5
examples/androidvoip/res/values/colors.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="almost_black">#484848</color>
|
||||||
|
<color name="light_gray">#D3D3D3</color>
|
||||||
|
</resources>
|
19
examples/androidvoip/res/values/strings.xml
Normal file
19
examples/androidvoip/res/values/strings.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">androidvoip</string>
|
||||||
|
<string name="local_endpoint_text_view">Local Endpoint</string>
|
||||||
|
<string name="remote_endpoint_text_view">Remote Endpoint</string>
|
||||||
|
<string name="ip_address_text_view">IP Address:</string>
|
||||||
|
<string name="port_number_text_view">Port Number:</string>
|
||||||
|
<string name="encoder_text_view">Select Encoder</string>
|
||||||
|
<string name="decoder_text_view">Select Decoder</string>
|
||||||
|
<string name="decoder_selection_button">Configure Selection</string>
|
||||||
|
<string name="decoders_text_view_default">No decoders selected</string>
|
||||||
|
<string name="dialog_title">Choose Decoders</string>
|
||||||
|
<string name="ok_label">Ok</string>
|
||||||
|
<string name="dismiss_label">Dismiss</string>
|
||||||
|
<string name="clear_all_label">Clear All</string>
|
||||||
|
<string name="start_send_text_view">Start Sending</string>
|
||||||
|
<string name="start_playout_text_view">Start Playout</string>
|
||||||
|
<string name="session_button_text_off">Start Session</string>
|
||||||
|
<string name="session_button_text_on">Stop Session</string>
|
||||||
|
</resources>
|
Reference in New Issue
Block a user