diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/libwebrtc/examples/androidvoip | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
13 files changed, 1754 insertions, 0 deletions
diff --git a/third_party/libwebrtc/examples/androidvoip/AndroidManifest.xml b/third_party/libwebrtc/examples/androidvoip/AndroidManifest.xml new file mode 100644 index 0000000000..8e096b0452 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/AndroidManifest.xml @@ -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-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" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/third_party/libwebrtc/examples/androidvoip/BUILD.gn b/third_party/libwebrtc/examples/androidvoip/BUILD.gn new file mode 100644 index 0000000000..3d5186f279 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/BUILD.gn @@ -0,0 +1,95 @@ +# 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 = 31 + + 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:base_java", + "//sdk/android:java_audio_device_module_java", + "//sdk/android:video_java", + "//third_party/androidx:androidx_core_core_java", + "//third_party/androidx: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", + "../../rtc_base:async_packet_socket", + "../../rtc_base:async_udp_socket", + "../../rtc_base:logging", + "../../rtc_base:network", + "../../rtc_base:socket_address", + "../../rtc_base:socket_server", + "../../rtc_base:ssl", + "../../rtc_base:threading", + "//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", + "//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. + } +} diff --git a/third_party/libwebrtc/examples/androidvoip/DEPS b/third_party/libwebrtc/examples/androidvoip/DEPS new file mode 100644 index 0000000000..edb714dd44 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+sdk/android/native_api", +] diff --git a/third_party/libwebrtc/examples/androidvoip/OWNERS b/third_party/libwebrtc/examples/androidvoip/OWNERS new file mode 100644 index 0000000000..e7d3200562 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/OWNERS @@ -0,0 +1,2 @@ +natim@webrtc.org +xalep@webrtc.org diff --git a/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/MainActivity.java b/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/MainActivity.java new file mode 100644 index 0000000000..d06d6adf0d --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/MainActivity.java @@ -0,0 +1,341 @@ +/* + * 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) { + if (toast != null) { + 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"); }); + } +} diff --git a/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java b/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java new file mode 100644 index 0000000000..bb85e048bb --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/OnVoipClientTaskCompleted.java @@ -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(); +} diff --git a/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/VoipClient.java b/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/VoipClient.java new file mode 100644 index 0000000000..69a993d344 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/java/org/webrtc/examples/androidvoip/VoipClient.java @@ -0,0 +1,191 @@ +/* + * 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; +import org.webrtc.CalledByNative; + +public class VoipClient { + private long nativeClient; + private OnVoipClientTaskCompleted listener; + + public VoipClient(Context applicationContext, OnVoipClientTaskCompleted listener) { + this.listener = listener; + nativeClient = nativeCreateClient(applicationContext, this); + } + + private boolean isInitialized() { + return nativeClient != 0; + } + + public void getAndSetUpSupportedCodecs() { + if (isInitialized()) { + nativeGetSupportedCodecs(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void getAndSetUpLocalIPAddress() { + if (isInitialized()) { + nativeGetLocalIPAddress(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void setEncoder(String encoder) { + if (isInitialized()) { + nativeSetEncoder(nativeClient, encoder); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void setDecoders(List<String> decoders) { + if (isInitialized()) { + nativeSetDecoders(nativeClient, decoders); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void setLocalAddress(String ipAddress, int portNumber) { + if (isInitialized()) { + nativeSetLocalAddress(nativeClient, ipAddress, portNumber); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void setRemoteAddress(String ipAddress, int portNumber) { + if (isInitialized()) { + nativeSetRemoteAddress(nativeClient, ipAddress, portNumber); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void startSession() { + if (isInitialized()) { + nativeStartSession(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void stopSession() { + if (isInitialized()) { + nativeStopSession(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void startSend() { + if (isInitialized()) { + nativeStartSend(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void stopSend() { + if (isInitialized()) { + nativeStopSend(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void startPlayout() { + if (isInitialized()) { + nativeStartPlayout(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void stopPlayout() { + if (isInitialized()) { + nativeStopPlayout(nativeClient); + } else { + listener.onUninitializedVoipClient(); + } + } + + public void close() { + nativeDelete(nativeClient); + nativeClient = 0; + } + + @CalledByNative + public void onGetLocalIPAddressCompleted(String localIPAddress) { + listener.onGetLocalIPAddressCompleted(localIPAddress); + } + + @CalledByNative + public void onGetSupportedCodecsCompleted(List<String> supportedCodecs) { + listener.onGetSupportedCodecsCompleted(supportedCodecs); + } + + @CalledByNative + public void onStartSessionCompleted(boolean isSuccessful) { + listener.onStartSessionCompleted(isSuccessful); + } + + @CalledByNative + public void onStopSessionCompleted(boolean isSuccessful) { + listener.onStopSessionCompleted(isSuccessful); + } + + @CalledByNative + public void onStartSendCompleted(boolean isSuccessful) { + listener.onStartSendCompleted(isSuccessful); + } + + @CalledByNative + public void onStopSendCompleted(boolean isSuccessful) { + listener.onStopSendCompleted(isSuccessful); + } + + @CalledByNative + public void onStartPlayoutCompleted(boolean isSuccessful) { + listener.onStartPlayoutCompleted(isSuccessful); + } + + @CalledByNative + public void onStopPlayoutCompleted(boolean isSuccessful) { + listener.onStopPlayoutCompleted(isSuccessful); + } + + private static native long nativeCreateClient( + Context applicationContext, VoipClient javaVoipClient); + private static native void nativeGetSupportedCodecs(long nativeAndroidVoipClient); + private static native void 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 void nativeStartSession(long nativeAndroidVoipClient); + private static native void nativeStopSession(long nativeAndroidVoipClient); + private static native void nativeStartSend(long nativeAndroidVoipClient); + private static native void nativeStopSend(long nativeAndroidVoipClient); + private static native void nativeStartPlayout(long nativeAndroidVoipClient); + private static native void nativeStopPlayout(long nativeAndroidVoipClient); + private static native void nativeDelete(long nativeAndroidVoipClient); +} diff --git a/third_party/libwebrtc/examples/androidvoip/jni/android_voip_client.cc b/third_party/libwebrtc/examples/androidvoip/jni/android_voip_client.cc new file mode 100644 index 0000000000..cf07e87e50 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/jni/android_voip_client.cc @@ -0,0 +1,514 @@ +/* + * 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" +#include "sdk/android/native_api/jni/jvm.h" +#include "sdk/android/native_api/jni/scoped_java_ref.h" + +namespace { + +#define RUN_ON_VOIP_THREAD(method, ...) \ + if (!voip_thread_->IsCurrent()) { \ + voip_thread_->PostTask( \ + std::bind(&AndroidVoipClient::method, this, ##__VA_ARGS__)); \ + return; \ + } \ + RTC_DCHECK_RUN_ON(voip_thread_.get()); + +// 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::Socket> socket( + thread->socketserver()->CreateSocket(family, SOCK_DGRAM)); + if (!socket) { + RTC_LOG_ERR(LS_ERROR) << "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_DCHECK_NOTREACHED(); + return -1; +} + +} // namespace + +namespace webrtc_examples { + +void AndroidVoipClient::Init( + JNIEnv* env, + const webrtc::JavaParamRef<jobject>& application_context) { + 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(); + + voip_thread_->Start(); + + // 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_->BlockingCall([this, &config] { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + supported_codecs_ = config.encoder_factory->GetSupportedEncoders(); + env_ = webrtc::AttachCurrentThreadIfNeeded(); + voip_engine_ = webrtc::CreateVoipEngine(std::move(config)); + }); +} + +AndroidVoipClient::~AndroidVoipClient() { + voip_thread_->BlockingCall([this] { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + JavaVM* jvm = nullptr; + env_->GetJavaVM(&jvm); + if (!jvm) { + RTC_LOG(LS_ERROR) << "Failed to retrieve JVM"; + return; + } + jint res = jvm->DetachCurrentThread(); + if (res != JNI_OK) { + RTC_LOG(LS_ERROR) << "DetachCurrentThread failed: " << res; + } + }); + + voip_thread_->Stop(); +} + +AndroidVoipClient* AndroidVoipClient::Create( + JNIEnv* env, + const webrtc::JavaParamRef<jobject>& application_context, + const webrtc::JavaParamRef<jobject>& j_voip_client) { + // Using `new` to access a non-public constructor. + auto voip_client = + absl::WrapUnique(new AndroidVoipClient(env, j_voip_client)); + voip_client->Init(env, application_context); + return voip_client.release(); +} + +void AndroidVoipClient::GetSupportedCodecs(JNIEnv* env) { + RUN_ON_VOIP_THREAD(GetSupportedCodecs, 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; + Java_VoipClient_onGetSupportedCodecsCompleted( + env_, j_voip_client_, NativeToJavaList(env_, names, convert_function)); +} + +void AndroidVoipClient::GetLocalIPAddress(JNIEnv* env) { + RUN_ON_VOIP_THREAD(GetLocalIPAddress, env); + + std::string local_ip_address; + rtc::IPAddress ipv4_address = QueryDefaultLocalAddress(AF_INET); + if (!ipv4_address.IsNil()) { + local_ip_address = ipv4_address.ToString(); + } else { + rtc::IPAddress ipv6_address = QueryDefaultLocalAddress(AF_INET6); + if (!ipv6_address.IsNil()) { + local_ip_address = ipv6_address.ToString(); + } + } + Java_VoipClient_onGetLocalIPAddressCompleted( + env_, j_voip_client_, webrtc::NativeToJavaString(env_, local_ip_address)); +} + +void AndroidVoipClient::SetEncoder(const std::string& encoder) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + return; + } + for (const webrtc::AudioCodecSpec& codec : supported_codecs_) { + if (codec.format.name == encoder) { + webrtc::VoipResult result = voip_engine_->Codec().SetSendCodec( + *channel_, GetPayloadType(codec.format.name), codec.format); + RTC_CHECK(result == webrtc::VoipResult::kOk); + return; + } + } +} + +void AndroidVoipClient::SetEncoder( + JNIEnv* env, + const webrtc::JavaParamRef<jstring>& j_encoder_string) { + const std::string& chosen_encoder = + webrtc::JavaToNativeString(env, j_encoder_string); + voip_thread_->PostTask( + [this, chosen_encoder] { SetEncoder(chosen_encoder); }); +} + +void AndroidVoipClient::SetDecoders(const std::vector<std::string>& decoders) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + return; + } + std::map<int, webrtc::SdpAudioFormat> decoder_specs; + for (const webrtc::AudioCodecSpec& codec : supported_codecs_) { + if (std::find(decoders.begin(), decoders.end(), codec.format.name) != + decoders.end()) { + decoder_specs.insert({GetPayloadType(codec.format.name), codec.format}); + } + } + + webrtc::VoipResult result = + voip_engine_->Codec().SetReceiveCodecs(*channel_, decoder_specs); + RTC_CHECK(result == webrtc::VoipResult::kOk); +} + +void AndroidVoipClient::SetDecoders( + JNIEnv* env, + const webrtc::JavaParamRef<jobject>& j_decoder_strings) { + const std::vector<std::string>& chosen_decoders = + webrtc::JavaListToNativeVector<std::string, jstring>( + env, j_decoder_strings, &webrtc::JavaToNativeString); + voip_thread_->PostTask( + [this, chosen_decoders] { SetDecoders(chosen_decoders); }); +} + +void AndroidVoipClient::SetLocalAddress(const std::string& ip_address, + const int port_number) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + rtp_local_address_ = rtc::SocketAddress(ip_address, port_number); + rtcp_local_address_ = rtc::SocketAddress(ip_address, port_number + 1); +} + +void AndroidVoipClient::SetLocalAddress( + JNIEnv* env, + const webrtc::JavaParamRef<jstring>& j_ip_address_string, + jint j_port_number_int) { + const std::string& ip_address = + webrtc::JavaToNativeString(env, j_ip_address_string); + voip_thread_->PostTask([this, ip_address, j_port_number_int] { + SetLocalAddress(ip_address, j_port_number_int); + }); +} + +void AndroidVoipClient::SetRemoteAddress(const std::string& ip_address, + const int port_number) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + rtp_remote_address_ = rtc::SocketAddress(ip_address, port_number); + rtcp_remote_address_ = rtc::SocketAddress(ip_address, port_number + 1); +} + +void AndroidVoipClient::SetRemoteAddress( + JNIEnv* env, + const webrtc::JavaParamRef<jstring>& j_ip_address_string, + jint j_port_number_int) { + const std::string& ip_address = + webrtc::JavaToNativeString(env, j_ip_address_string); + voip_thread_->PostTask([this, ip_address, j_port_number_int] { + SetRemoteAddress(ip_address, j_port_number_int); + }); +} + +void AndroidVoipClient::StartSession(JNIEnv* env) { + RUN_ON_VOIP_THREAD(StartSession, env); + + // CreateChannel guarantees to return valid channel id. + channel_ = voip_engine_->Base().CreateChannel(this, absl::nullopt); + + rtp_socket_.reset(rtc::AsyncUDPSocket::Create(voip_thread_->socketserver(), + rtp_local_address_)); + if (!rtp_socket_) { + RTC_LOG_ERR(LS_ERROR) << "Socket creation failed"; + Java_VoipClient_onStartSessionCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + 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(LS_ERROR) << "Socket creation failed"; + Java_VoipClient_onStartSessionCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + rtcp_socket_->SignalReadPacket.connect( + this, &AndroidVoipClient::OnSignalReadRTCPPacket); + Java_VoipClient_onStartSessionCompleted(env_, j_voip_client_, + /*isSuccessful=*/true); +} + +void AndroidVoipClient::StopSession(JNIEnv* env) { + RUN_ON_VOIP_THREAD(StopSession, env); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + Java_VoipClient_onStopSessionCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + if (voip_engine_->Base().StopSend(*channel_) != webrtc::VoipResult::kOk || + voip_engine_->Base().StopPlayout(*channel_) != webrtc::VoipResult::kOk) { + Java_VoipClient_onStopSessionCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + + rtp_socket_->Close(); + rtcp_socket_->Close(); + + webrtc::VoipResult result = voip_engine_->Base().ReleaseChannel(*channel_); + RTC_CHECK(result == webrtc::VoipResult::kOk); + + channel_ = absl::nullopt; + Java_VoipClient_onStopSessionCompleted(env_, j_voip_client_, + /*isSuccessful=*/true); +} + +void AndroidVoipClient::StartSend(JNIEnv* env) { + RUN_ON_VOIP_THREAD(StartSend, env); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + Java_VoipClient_onStartSendCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + bool sending_started = + (voip_engine_->Base().StartSend(*channel_) == webrtc::VoipResult::kOk); + Java_VoipClient_onStartSendCompleted(env_, j_voip_client_, sending_started); +} + +void AndroidVoipClient::StopSend(JNIEnv* env) { + RUN_ON_VOIP_THREAD(StopSend, env); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + Java_VoipClient_onStopSendCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + bool sending_stopped = + (voip_engine_->Base().StopSend(*channel_) == webrtc::VoipResult::kOk); + Java_VoipClient_onStopSendCompleted(env_, j_voip_client_, sending_stopped); +} + +void AndroidVoipClient::StartPlayout(JNIEnv* env) { + RUN_ON_VOIP_THREAD(StartPlayout, env); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + Java_VoipClient_onStartPlayoutCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + bool playout_started = + (voip_engine_->Base().StartPlayout(*channel_) == webrtc::VoipResult::kOk); + Java_VoipClient_onStartPlayoutCompleted(env_, j_voip_client_, + playout_started); +} + +void AndroidVoipClient::StopPlayout(JNIEnv* env) { + RUN_ON_VOIP_THREAD(StopPlayout, env); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + Java_VoipClient_onStopPlayoutCompleted(env_, j_voip_client_, + /*isSuccessful=*/false); + return; + } + bool playout_stopped = + (voip_engine_->Base().StopPlayout(*channel_) == webrtc::VoipResult::kOk); + Java_VoipClient_onStopPlayoutCompleted(env_, j_voip_client_, playout_stopped); +} + +void AndroidVoipClient::Delete(JNIEnv* env) { + delete this; +} + +void AndroidVoipClient::SendRtpPacket(const std::vector<uint8_t>& packet_copy) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + if (!rtp_socket_->SendTo(packet_copy.data(), packet_copy.size(), + rtp_remote_address_, rtc::PacketOptions())) { + RTC_LOG(LS_ERROR) << "Failed to send RTP packet"; + } +} + +bool AndroidVoipClient::SendRtp(const uint8_t* packet, + size_t length, + const webrtc::PacketOptions& options) { + std::vector<uint8_t> packet_copy(packet, packet + length); + voip_thread_->PostTask([this, packet_copy = std::move(packet_copy)] { + SendRtpPacket(packet_copy); + }); + return true; +} + +void AndroidVoipClient::SendRtcpPacket( + const std::vector<uint8_t>& packet_copy) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + if (!rtcp_socket_->SendTo(packet_copy.data(), packet_copy.size(), + rtcp_remote_address_, rtc::PacketOptions())) { + RTC_LOG(LS_ERROR) << "Failed to send RTCP packet"; + } +} + +bool AndroidVoipClient::SendRtcp(const uint8_t* packet, size_t length) { + std::vector<uint8_t> packet_copy(packet, packet + length); + voip_thread_->PostTask([this, packet_copy = std::move(packet_copy)] { + SendRtcpPacket(packet_copy); + }); + return true; +} + +void AndroidVoipClient::ReadRTPPacket(const std::vector<uint8_t>& packet_copy) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + return; + } + webrtc::VoipResult result = voip_engine_->Network().ReceivedRTPPacket( + *channel_, + rtc::ArrayView<const uint8_t>(packet_copy.data(), packet_copy.size())); + RTC_CHECK(result == webrtc::VoipResult::kOk); +} + +void AndroidVoipClient::OnSignalReadRTPPacket(rtc::AsyncPacketSocket* socket, + const char* rtp_packet, + size_t size, + const rtc::SocketAddress& addr, + const int64_t& timestamp) { + std::vector<uint8_t> packet_copy(rtp_packet, rtp_packet + size); + voip_thread_->PostTask([this, packet_copy = std::move(packet_copy)] { + ReadRTPPacket(packet_copy); + }); +} + +void AndroidVoipClient::ReadRTCPPacket( + const std::vector<uint8_t>& packet_copy) { + RTC_DCHECK_RUN_ON(voip_thread_.get()); + + if (!channel_) { + RTC_LOG(LS_ERROR) << "Channel has not been created"; + return; + } + webrtc::VoipResult result = voip_engine_->Network().ReceivedRTCPPacket( + *channel_, + rtc::ArrayView<const uint8_t>(packet_copy.data(), packet_copy.size())); + RTC_CHECK(result == webrtc::VoipResult::kOk); +} + +void AndroidVoipClient::OnSignalReadRTCPPacket(rtc::AsyncPacketSocket* socket, + const char* rtcp_packet, + size_t size, + const rtc::SocketAddress& addr, + const int64_t& timestamp) { + std::vector<uint8_t> packet_copy(rtcp_packet, rtcp_packet + size); + voip_thread_->PostTask([this, packet_copy = std::move(packet_copy)] { + ReadRTCPPacket(packet_copy); + }); +} + +static jlong JNI_VoipClient_CreateClient( + JNIEnv* env, + const webrtc::JavaParamRef<jobject>& application_context, + const webrtc::JavaParamRef<jobject>& j_voip_client) { + return webrtc::NativeToJavaPointer( + AndroidVoipClient::Create(env, application_context, j_voip_client)); +} + +} // namespace webrtc_examples diff --git a/third_party/libwebrtc/examples/androidvoip/jni/android_voip_client.h b/third_party/libwebrtc/examples/androidvoip/jni/android_voip_client.h new file mode 100644 index 0000000000..8e1edd5ef9 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/jni/android_voip_client.h @@ -0,0 +1,189 @@ +/* + * 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 +// operations with rtc::Thread (voip_thread_), this is to comply +// with consistent thread usage requirement with ProcessThread used +// within VoipEngine, as well as providing asynchronicity to the +// caller. 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, + const webrtc::JavaParamRef<jobject>& j_voip_client); + + ~AndroidVoipClient() override; + + // Provides client with a Java List of Strings containing names of + // the built-in supported codecs through callback. + void GetSupportedCodecs(JNIEnv* env); + + // Provides client with a Java String of the default local IPv4 address + // through callback. If IPv4 address is not found, provide the default + // local IPv6 address. If IPv6 address is not found, provide an empty + // string. + void GetLocalIPAddress(JNIEnv* env); + + // Sets the encoder used by the VoIP API. + void SetEncoder(JNIEnv* env, + const webrtc::JavaParamRef<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::JavaParamRef<jstring>& j_ip_address_string, + jint j_port_number_int); + void SetRemoteAddress( + JNIEnv* env, + const webrtc::JavaParamRef<jstring>& j_ip_address_string, + jint j_port_number_int); + + // Starts a VoIP session, then calls a callback method with a boolean + // value indicating if the session has started successfully. The VoIP + // operations below can only be used after a session has already started. + void StartSession(JNIEnv* env); + + // Stops the current session, then calls a callback method with a + // boolean value indicating if the session has stopped successfully. + void StopSession(JNIEnv* env); + + // Starts sending RTP/RTCP packets to the remote endpoint, then calls + // a callback method with a boolean value indicating if sending + // has started successfully. + void StartSend(JNIEnv* env); + + // Stops sending RTP/RTCP packets to the remote endpoint, then calls + // a callback method with a boolean value indicating if sending + // has stopped successfully. + void StopSend(JNIEnv* env); + + // Starts playing out the voice data received from the remote endpoint, + // then calls a callback method with a boolean value indicating if + // playout has started successfully. + void StartPlayout(JNIEnv* env); + + // Stops playing out the voice data received from the remote endpoint, + // then calls a callback method with a boolean value indicating if + // playout has stopped successfully. + void 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>& j_voip_client) + : voip_thread_(rtc::Thread::CreateWithSocketServer()), + j_voip_client_(env, j_voip_client) {} + + void Init(JNIEnv* env, + const webrtc::JavaParamRef<jobject>& application_context); + + // Overloaded methods having native C++ variables as arguments. + void SetEncoder(const std::string& encoder); + void SetDecoders(const std::vector<std::string>& decoders); + void SetLocalAddress(const std::string& ip_address, int port_number); + void SetRemoteAddress(const std::string& ip_address, int port_number); + + // Methods to send and receive RTP/RTCP packets. Takes in a + // copy of a packet as a vector to prolong the lifetime of + // the packet as these methods will be called asynchronously. + void SendRtpPacket(const std::vector<uint8_t>& packet_copy); + void SendRtcpPacket(const std::vector<uint8_t>& packet_copy); + void ReadRTPPacket(const std::vector<uint8_t>& packet_copy); + void ReadRTCPPacket(const std::vector<uint8_t>& packet_copy); + + // Used to invoke operations and send/receive RTP/RTCP packets. + std::unique_ptr<rtc::Thread> voip_thread_; + // Reference to the VoipClient java instance used to + // invoke callbacks when operations are finished. + webrtc::ScopedJavaGlobalRef<jobject> j_voip_client_ + RTC_GUARDED_BY(voip_thread_); + // A list of AudioCodecSpec supported by the built-in + // encoder/decoder factories. + std::vector<webrtc::AudioCodecSpec> supported_codecs_ + RTC_GUARDED_BY(voip_thread_); + // A JNI context used by the voip_thread_. + JNIEnv* env_ RTC_GUARDED_BY(voip_thread_); + // The entry point to all VoIP APIs. + std::unique_ptr<webrtc::VoipEngine> voip_engine_ RTC_GUARDED_BY(voip_thread_); + // Used by the VoIP API to facilitate a VoIP session. + absl::optional<webrtc::ChannelId> channel_ RTC_GUARDED_BY(voip_thread_); + // Members below are used for network related operations. + std::unique_ptr<rtc::AsyncUDPSocket> rtp_socket_ RTC_GUARDED_BY(voip_thread_); + std::unique_ptr<rtc::AsyncUDPSocket> rtcp_socket_ + RTC_GUARDED_BY(voip_thread_); + rtc::SocketAddress rtp_local_address_ RTC_GUARDED_BY(voip_thread_); + rtc::SocketAddress rtcp_local_address_ RTC_GUARDED_BY(voip_thread_); + rtc::SocketAddress rtp_remote_address_ RTC_GUARDED_BY(voip_thread_); + rtc::SocketAddress rtcp_remote_address_ RTC_GUARDED_BY(voip_thread_); +}; + +} // namespace webrtc_examples + +#endif // EXAMPLES_ANDROIDVOIP_JNI_ANDROID_VOIP_CLIENT_H_ diff --git a/third_party/libwebrtc/examples/androidvoip/jni/onload.cc b/third_party/libwebrtc/examples/androidvoip/jni/onload.cc new file mode 100644 index 0000000000..b952de348b --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/jni/onload.cc @@ -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 diff --git a/third_party/libwebrtc/examples/androidvoip/res/layout/activity_main.xml b/third_party/libwebrtc/examples/androidvoip/res/layout/activity_main.xml new file mode 100644 index 0000000000..c7fa5a9b31 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/res/layout/activity_main.xml @@ -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> diff --git a/third_party/libwebrtc/examples/androidvoip/res/values/colors.xml b/third_party/libwebrtc/examples/androidvoip/res/values/colors.xml new file mode 100644 index 0000000000..4dadaa9941 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/res/values/colors.xml @@ -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>
\ No newline at end of file diff --git a/third_party/libwebrtc/examples/androidvoip/res/values/strings.xml b/third_party/libwebrtc/examples/androidvoip/res/values/strings.xml new file mode 100644 index 0000000000..d519bfbbb6 --- /dev/null +++ b/third_party/libwebrtc/examples/androidvoip/res/values/strings.xml @@ -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> |