diff --git a/QNRTC-API-Examples/.gitignore b/QNRTC-API-Examples/.gitignore
new file mode 100644
index 0000000..2c2a4eb
--- /dev/null
+++ b/QNRTC-API-Examples/.gitignore
@@ -0,0 +1,80 @@
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+# Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/*
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
diff --git a/QNRTC-API-Examples/README.md b/QNRTC-API-Examples/README.md
new file mode 100644
index 0000000..e567605
--- /dev/null
+++ b/QNRTC-API-Examples/README.md
@@ -0,0 +1,2 @@
+# QNRTC-API-Examples
+七牛实时音视频 API Examples
diff --git a/QNRTC-API-Examples/app/.gitignore b/QNRTC-API-Examples/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/QNRTC-API-Examples/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/build.gradle b/QNRTC-API-Examples/app/build.gradle
new file mode 100644
index 0000000..5ea78c0
--- /dev/null
+++ b/QNRTC-API-Examples/app/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ compileSdkVersion 31
+ buildToolsVersion '30.0.3'
+
+ defaultConfig {
+ applicationId "com.qiniu.droid.rtc.api.examples"
+ minSdkVersion 18
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ if (buildWithQNDroidRTCLibrary) {
+ implementation project(':library')
+ } else {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ }
+ implementation 'androidx.appcompat:appcompat:1.4.1'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ implementation 'com.squareup.okhttp3:okhttp:4.8.1'
+ implementation 'com.qiniu:happy-dns:0.2.18'
+ implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.24'
+}
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/libs/qndroid-rtc-4.0.1.jar b/QNRTC-API-Examples/app/libs/qndroid-rtc-4.0.1.jar
new file mode 100644
index 0000000..f00d7aa
Binary files /dev/null and b/QNRTC-API-Examples/app/libs/qndroid-rtc-4.0.1.jar differ
diff --git a/QNRTC-API-Examples/app/proguard-rules.pro b/QNRTC-API-Examples/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/QNRTC-API-Examples/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/release/output-metadata.json b/QNRTC-API-Examples/app/release/output-metadata.json
new file mode 100644
index 0000000..b0e587a
--- /dev/null
+++ b/QNRTC-API-Examples/app/release/output-metadata.json
@@ -0,0 +1,18 @@
+{
+ "version": 2,
+ "artifactType": {
+ "type": "APK",
+ "kind": "Directory"
+ },
+ "applicationId": "com.qiniu.droid.rtc.api.examples",
+ "variantName": "release",
+ "elements": [
+ {
+ "type": "SINGLE",
+ "filters": [],
+ "versionCode": 1,
+ "versionName": "1.0.0",
+ "outputFile": "app-release.apk"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/AndroidManifest.xml b/QNRTC-API-Examples/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e0651c7
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/AndroidManifest.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/assets/music.mp3 b/QNRTC-API-Examples/app/src/main/assets/music.mp3
new file mode 100644
index 0000000..9b4eda6
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/assets/music.mp3 differ
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/AudioMixerActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/AudioMixerActivity.java
new file mode 100644
index 0000000..2a8ab19
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/AudioMixerActivity.java
@@ -0,0 +1,567 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.os.Environment;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioMixer;
+import com.qiniu.droid.rtc.QNAudioMixerListener;
+import com.qiniu.droid.rtc.QNAudioMixerState;
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Locale;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 音频通话 + 混音场景
+ * 本示例仅演示音频 Track 的发布订阅 + 混音的场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地麦克风音频采集 Track
+ * 5. 加入房间
+ * 6. 发布本地麦克风音频 Track
+ * 7. 订阅远端音频 Track(可选操作)
+ * 8. 混音操作
+ * 9. 离开房间
+ * 10. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 背景音乐混音的使用指南,请参考 https://developer.qiniu.com/rtc/8771/background-music-mix-android
+ * - 背景音乐混音的注意事项,请参考 https://developer.qiniu.com/rtc/8771/background-music-mix-android#3
+ * - 混音场景错误码,请参考 https://developer.qiniu.com/rtc/9904/rtc-error-code-android#4
+ */
+public class AudioMixerActivity extends AppCompatActivity {
+ private static final String TAG = "AudioMixerActivity";
+ private QNRTCClient mClient;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNAudioMixer mAudioMixer;
+ private QNAudioMixerState mAudioMixerState = QNAudioMixerState.COMPLETED;
+
+ private TextView mRemoteTrackTipsView;
+ private EditText mMusicUrlEditText;
+ private EditText mLoopTimeEditText;
+ private Button mStartAudioMixButton;
+ private Button mPauseAudioMixButton;
+ private Switch mEarMonitorOnSwitch;
+ private SeekBar mProgressSeekBar;
+ private TextView mProgressTextView;
+ private SeekBar mMicrophoneMixVolumeSeekBar;
+ private SeekBar mMusicMixVolumeSeekBar;
+ private SeekBar mMusicPlayVolumeSeekBar;
+ private boolean mIsAudioMixerControllable = true;
+ private String mFirstRemoteUserID = null;
+ private String mMusicPath;
+
+ private float mMicrophoneMixVolume = 1.0f;
+ private float mMusicMixVolume = 1.0f;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_audio_mixer);
+
+ // 检查本地是否存在指定音乐文件
+ checkMusicFile();
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地麦克风音频采集 Track
+ initLocalTracks();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing() && mClient != null) {
+ // 9. 离开房间
+ mClient.leave();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 10. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 初始化本地视图
+ */
+ private void initView() {
+ // 初始化远端音频提示视图
+ mRemoteTrackTipsView = findViewById(R.id.remote_window_tips_view);
+
+ mMusicUrlEditText = findViewById(R.id.music_url_edit_text);
+ mMusicUrlEditText.setText(mMusicPath);
+ mLoopTimeEditText = findViewById(R.id.loop_times_edit_text);
+ mProgressTextView = findViewById(R.id.progress_text);
+ mProgressTextView.setText(String.format(getString(R.string.audio_mix_progress), "00:00", "00:00"));
+
+ // 初始化开始、停止混音控件
+ mStartAudioMixButton = findViewById(R.id.start_mix_button);
+ mStartAudioMixButton.setOnClickListener(v -> {
+ if (TextUtils.isEmpty(mMusicUrlEditText.getText())) {
+ ToastUtils.showShortToast(AudioMixerActivity.this, getString(R.string.invalid_music_url_toast));
+ return;
+ }
+ if (TextUtils.isEmpty(mLoopTimeEditText.getText())) {
+ ToastUtils.showShortToast(AudioMixerActivity.this, getString(R.string.invalid_loop_times_toast));
+ return;
+ }
+ if (mAudioMixerState == QNAudioMixerState.STOPPED || mAudioMixerState == QNAudioMixerState.COMPLETED) {
+ // 开始音乐混音
+ startAudioMix(mMusicUrlEditText.getText().toString(),
+ Integer.parseInt(mLoopTimeEditText.getText().toString()), mProgressSeekBar);
+ } else if (mAudioMixerState == QNAudioMixerState.MIXING || mAudioMixerState == QNAudioMixerState.PAUSED) {
+ mAudioMixer.stop();
+ }
+ });
+
+ // 初始化暂停、恢复混音控件
+ mPauseAudioMixButton = findViewById(R.id.pause_mix_button);
+ mPauseAudioMixButton.setOnClickListener(v -> {
+ if (mAudioMixerState == QNAudioMixerState.STOPPED || mAudioMixerState == QNAudioMixerState.COMPLETED) {
+ ToastUtils.showShortToast(AudioMixerActivity.this, getString(R.string.audio_mix_first_toast));
+ return;
+ }
+ if (mAudioMixerState == QNAudioMixerState.MIXING) {
+ // 暂停音乐混音
+ mAudioMixer.pause();
+ } else if (mAudioMixerState == QNAudioMixerState.PAUSED) {
+ // 从暂停处开始恢复音乐混音
+ mAudioMixer.resume();
+ }
+ });
+
+ mEarMonitorOnSwitch = findViewById(R.id.ear_monitor_on);
+ mEarMonitorOnSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (mAudioMixer == null) {
+ ToastUtils.showShortToast(AudioMixerActivity.this, getString(R.string.audio_mix_first_toast));
+ return;
+ }
+ // 开启返听,建议在佩戴耳机的场景下使用该接口
+ mAudioMixer.enableEarMonitor(isChecked);
+ });
+
+ mProgressSeekBar = findViewById(R.id.audio_mix_progress);
+ mProgressSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mAudioMixer != null) {
+ // 跳到指定位置进行混音,单位为 us
+ mAudioMixer.seekTo(seekBar.getProgress());
+ }
+ }
+ });
+
+ // 初始化麦克风混音音量设置控件
+ mMicrophoneMixVolumeSeekBar = findViewById(R.id.seek_bar_microphone_volume);
+ mMicrophoneMixVolumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mAudioMixer != null) {
+ // 设置麦克风混音音量,【 0.0f - 1.0f 】
+ mMicrophoneMixVolume = seekBar.getProgress() / 100.0f;
+ mAudioMixer.setMixingVolume(mMicrophoneMixVolume, mMusicMixVolume);
+ }
+ }
+ });
+
+ // 初始化音乐混音音量设置控件
+ mMusicMixVolumeSeekBar = findViewById(R.id.seek_bar_music_volume);
+ mMusicMixVolumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mAudioMixer != null) {
+ // 设置音乐混音音量,【 0.0f - 1.0f 】
+ mMusicMixVolume = seekBar.getProgress() / 100.0f;
+ mAudioMixer.setMixingVolume(mMicrophoneMixVolume, mMusicMixVolume);
+ }
+ }
+ });
+
+ // 初始化音乐本地播放音量设置控件
+ mMusicPlayVolumeSeekBar = findViewById(R.id.seek_bar_music_play_volume);
+ mMusicPlayVolumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mAudioMixer != null) {
+ // 设置音乐本地播放音量,【 0.0f - 1.0f 】
+ mAudioMixer.setPlayingVolume(seekBar.getProgress() / 100.0f);
+ }
+ }
+ });
+ setAudioMixerControllable(false);
+ }
+
+ /**
+ * 初始化本地麦克风采集 Track
+ */
+ private void initLocalTracks() {
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ /**
+ * 开启音乐混音
+ *
+ * @param filePath 混音文件路径
+ * @param loopTimes 混音次数,-1 为无限循环
+ * @param durationProgress 混音进度控件
+ */
+ private void startAudioMix(String filePath, int loopTimes, final SeekBar durationProgress) {
+ if (mMicrophoneAudioTrack == null) {
+ ToastUtils.showShortToast(getApplicationContext(), "请先创建音频轨道");
+ return;
+ }
+ // 创建混音管理器 QNAudioMixer 实例
+ // 当前仅支持同一时间混一路音频,重复对不同的 QNAudioMixer 执行 start 操作,以后执行 start 的 QNAudioMixer 为准进行混音。
+ mAudioMixer = mMicrophoneAudioTrack.createAudioMixer(filePath, new QNAudioMixerListener() {
+ /**
+ * 混音状态改变时触发
+ *
+ * @param state 当前状态
+ */
+ @Override
+ public void onStateChanged(QNAudioMixerState state) {
+ Log.i(TAG, "混音状态改变 : " + state.name());
+ mAudioMixerState = state;
+ if (state == QNAudioMixerState.MIXING) {
+ setAudioMixerControllable(true);
+ mStartAudioMixButton.setText(getString(R.string.stop_audio_mix));
+ mPauseAudioMixButton.setText(getString(R.string.pause_audio_mix));
+ }
+ if (state == QNAudioMixerState.PAUSED) {
+ mPauseAudioMixButton.setText(getString(R.string.resume_audio_mix));
+ }
+ if (state == QNAudioMixerState.STOPPED || state == QNAudioMixerState.COMPLETED) {
+ durationProgress.setProgress(0);
+ setAudioMixerControllable(false);
+ mStartAudioMixButton.setText(getString(R.string.start_audio_mix));
+ mPauseAudioMixButton.setText(getString(R.string.pause_audio_mix));
+ }
+ }
+
+ /**
+ * 混音过程中触发
+ *
+ * @param currentTimeUs 当前的混音时间
+ */
+ @Override
+ public void onMixing(long currentTimeUs) {
+ durationProgress.setMax((int) mAudioMixer.getDuration());
+ durationProgress.setProgress((int) currentTimeUs);
+ SimpleDateFormat formatter = new SimpleDateFormat("mm:ss", Locale.CHINA);
+ mProgressTextView.setText(String.format(getString(R.string.audio_mix_progress),
+ formatter.format(currentTimeUs / 1000), formatter.format(mAudioMixer.getDuration() / 1000)));
+ }
+
+ /**
+ * 混音发生错误时触发
+ * 对应错误码可参考 https://developer.qiniu.com/rtc/9904/rtc-error-code-android#4
+ *
+ * @param errorCode 错误码
+ */
+ @Override
+ public void onError(int errorCode) {
+ ToastUtils.showShortToast(AudioMixerActivity.this, String.format(getString(R.string.audio_mix_error), errorCode));
+ }
+ });
+ // 开始混音
+ mAudioMixer.start(loopTimes);
+ }
+
+ /**
+ * 设置混音相关控件是否可操作
+ *
+ * @param controllable 是否可操作
+ */
+ private void setAudioMixerControllable(boolean controllable) {
+ if (mIsAudioMixerControllable == controllable) {
+ return;
+ }
+ mIsAudioMixerControllable = controllable;
+ mMicrophoneMixVolumeSeekBar.setEnabled(mIsAudioMixerControllable);
+ mMusicMixVolumeSeekBar.setEnabled(mIsAudioMixerControllable);
+ mMusicPlayVolumeSeekBar.setEnabled(mIsAudioMixerControllable);
+ mEarMonitorOnSwitch.setEnabled(mIsAudioMixerControllable);
+ }
+
+ /**
+ * 检查音乐文件是否存在,不存在则拷贝到存储中
+ */
+ private void checkMusicFile() {
+ try {
+ mMusicPath = getExternalFilesDir(Environment.DIRECTORY_MUSIC) + File.separator + "music.mp3";
+ File musicFile = new File(mMusicPath);
+ if (musicFile.exists()) {
+ return;
+ }
+ AlertDialog alertDialog = new AlertDialog.Builder(this)
+ .setMessage(getString(R.string.prepare_music_tips))
+ .setCancelable(false)
+ .show();
+ InputStream is = getAssets().open("music.mp3");
+ FileOutputStream fos = new FileOutputStream(mMusicPath);
+ byte[] buffer = new byte[1024];
+ int byteCount;
+ while ((byteCount = is.read(buffer)) != -1) {
+ fos.write(buffer, 0, byteCount);
+ }
+ fos.flush();
+ is.close();
+ fos.close();
+ alertDialog.dismiss();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(AudioMixerActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地麦克风音频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(AudioMixerActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(AudioMixerActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(AudioMixerActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音频 Track
+ for (QNRemoteTrack remoteTrack : trackList) {
+ if (remoteTrack.isAudio()) {
+ mClient.subscribe(remoteTrack);
+ }
+ }
+ } else {
+ ToastUtils.showShortToast(AudioMixerActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteTrackTipsView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (remoteUserID.equals(mFirstRemoteUserID) && !remoteAudioTracks.isEmpty()) {
+ // 成功订阅远端音频 Track 后,SDK 会默认对音频 Track 进行渲染,无需其他操作
+ mRemoteTrackTipsView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
+
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CameraMicrophoneActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CameraMicrophoneActivity.java
new file mode 100644
index 0000000..5095562
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CameraMicrophoneActivity.java
@@ -0,0 +1,382 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.SeekBar;
+import android.widget.Switch;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.SwitchCompat;
+
+/**
+ * 1v1 摄像头 + 麦克风音视频通话场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地音视频 Track
+ * 5. 加入房间
+ * 6. 发布本地音视频 Track
+ * 7. 订阅远端音视频 Track
+ * 8. 离开房间
+ * 9. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class CameraMicrophoneActivity extends AppCompatActivity {
+ private static final String TAG = "CameraMicrophoneActivity";
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNBeautySetting mBeautySetting;
+
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_camera_microphone);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地音视频 Track
+ initLocalTracks();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 8. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 9. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ SwitchCompat beautyOnSwitch = findViewById(R.id.switch_beauty_on);
+ beautyOnSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ // 控制美颜的开关
+ mBeautySetting.setEnable(isChecked);
+ mCameraVideoTrack.setBeauty(mBeautySetting);
+ });
+
+ SeekBar beautyStrengthSeekBar = findViewById(R.id.seek_bar_beauty_strength);
+ beautyStrengthSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mBeautySetting.setSmoothLevel(seekBar.getProgress() / 100.0f);
+ mCameraVideoTrack.setBeauty(mBeautySetting);
+ }
+ });
+
+ SeekBar beautyReddenSeekBar = findViewById(R.id.seek_bar_redden);
+ beautyReddenSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mBeautySetting.setRedden(seekBar.getProgress() / 100.0f);
+ mCameraVideoTrack.setBeauty(mBeautySetting);
+ }
+ });
+
+ SeekBar beautyWhitenSeekBar = findViewById(R.id.seek_bar_whiten);
+ beautyWhitenSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mBeautySetting.setWhiten(seekBar.getProgress() / 100.0f);
+ mCameraVideoTrack.setBeauty(mBeautySetting);
+ }
+ });
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(false); // 设置是否开启大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+ // 初始化并配置美颜
+ mBeautySetting = new QNBeautySetting(0.5f, 0.5f, 0.5f);
+ mCameraVideoTrack.setBeauty(mBeautySetting);
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(CameraMicrophoneActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(CameraMicrophoneActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(CameraMicrophoneActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(CameraMicrophoneActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(CameraMicrophoneActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ remoteVideoTracks.get(0).play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomAVCaptureActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomAVCaptureActivity.java
new file mode 100644
index 0000000..bdcf55b
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomAVCaptureActivity.java
@@ -0,0 +1,349 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.qiniu.droid.rtc.QNAudioFrame;
+import com.qiniu.droid.rtc.QNAudioQuality;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomAudioTrack;
+import com.qiniu.droid.rtc.QNCustomAudioTrackConfig;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNCustomVideoTrack;
+import com.qiniu.droid.rtc.QNCustomVideoTrackConfig;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.QNVideoFrame;
+import com.qiniu.droid.rtc.QNVideoFrameType;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.capture.ExtAudioCapture;
+import com.qiniu.droid.rtc.api.examples.capture.ExtVideoCapture;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import org.webrtc.RendererCommon;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 自定义音视频数据导入通话场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地音视频 Track
+ * 5. 加入房间
+ * 6. 发布本地音视频 Track
+ * 7. 订阅远端音视频 Track
+ * 8. 离开房间
+ * 9. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 外部视频导入支持情况,请参考 https://developer.qiniu.com/rtc/8767/audio-and-video-collection-android#3
+ * - 外部音频导入支持情况,请参考 https://developer.qiniu.com/rtc/8767/audio-and-video-collection-android#5
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class CustomAVCaptureActivity extends AppCompatActivity {
+ private static final String TAG = "CustomAVCaptureActivity";
+
+ private QNRTCClient mClient;
+ private QNCustomVideoTrack mCustomVideoTrack;
+ private QNCustomAudioTrack mCustomAudioTrack;
+
+ private SurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+
+ private ExtAudioCapture mExtAudioCapture;
+ private ExtVideoCapture mExtVideoCapture;
+
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_custom_av_capture);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地音视频 Track
+ initLocalTracks();
+ // 初始化外部采集实例
+ initExtCapture();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mExtAudioCapture.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mExtAudioCapture.stopCapture();
+ if (isFinishing() && mClient != null) {
+ // 8. 离开房间
+ mClient.leave();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 9. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 初始化视图
+ */
+ private void initView() {
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+ }
+
+ /**
+ * 初始化外部采集音视频 Track
+ */
+ private void initLocalTracks() {
+ // 初始化外部导入视频 Track
+ QNCustomVideoTrackConfig customVideoTrackConfig = new QNCustomVideoTrackConfig(Config.TAG_CUSTOM_VIDEO_TRACK)
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)); // 设置编码参数
+ mCustomVideoTrack = QNRTC.createCustomVideoTrack(customVideoTrackConfig);
+
+ // 初始化外部导入音频 Track
+ QNCustomAudioTrackConfig customAudioTrackConfig = new QNCustomAudioTrackConfig(Config.TAG_CUSTOM_AUDIO_TRACK)
+ .setAudioQuality(new QNAudioQuality(Config.DEFAULT_AUDIO_SAMPLE_RATE, Config.DEFAULT_AUDIO_CHANNEL_COUNT, 16, Config.DEFAULT_AUDIO_BITRATE));
+ mCustomAudioTrack = QNRTC.createCustomAudioTrack(customAudioTrackConfig);
+ }
+
+ /**
+ * 初始化外部采集实例
+ */
+ private void initExtCapture() {
+ mExtVideoCapture = new ExtVideoCapture(mLocalRenderView);
+ mExtVideoCapture.setOnPreviewFrameCallback(mOnPreviewFrameCallback);
+ mExtAudioCapture = new ExtAudioCapture();
+ mExtAudioCapture.setOnAudioFrameCapturedListener(mOnAudioFrameCapturedListener);
+ }
+
+ private final ExtVideoCapture.OnPreviewFrameCallback mOnPreviewFrameCallback = new ExtVideoCapture.OnPreviewFrameCallback() {
+ @Override
+ public void onPreviewFrameCaptured(byte[] data, int width, int height, int orientation, boolean mirror, long tsInNanoTime) {
+ if (mCustomVideoTrack == null || TextUtils.isEmpty(mCustomVideoTrack.getTrackID())) {
+ return;
+ }
+ // 推送自定义视频数据
+ // 数据导入支持情况,请参考 https://developer.qiniu.com/rtc/8767/audio-and-video-collection-android#3
+ QNVideoFrame videoFrame = new QNVideoFrame();
+ videoFrame.buffer = data;
+ videoFrame.width = width;
+ videoFrame.height = height;
+ videoFrame.rotation = orientation;
+ videoFrame.type = QNVideoFrameType.YUV_NV21;
+ videoFrame.timestampNs = tsInNanoTime;
+ mCustomVideoTrack.pushVideoFrame(videoFrame);
+ }
+ };
+
+ private final ExtAudioCapture.OnAudioFrameCapturedListener mOnAudioFrameCapturedListener = new ExtAudioCapture.OnAudioFrameCapturedListener() {
+ @Override
+ public void onAudioFrameCaptured(byte[] audioData) {
+ if (mCustomAudioTrack == null || TextUtils.isEmpty(mCustomAudioTrack.getTrackID())) {
+ return;
+ }
+ // 推送自定义音频数据
+ // 数据导入支持情况,请参考 https://developer.qiniu.com/rtc/8767/audio-and-video-collection-android#5
+ QNAudioFrame audioFrame = new QNAudioFrame(ByteBuffer.wrap(audioData), audioData.length,
+ 16, Config.DEFAULT_AUDIO_SAMPLE_RATE, Config.DEFAULT_AUDIO_CHANNEL_COUNT);
+ mCustomAudioTrack.pushAudioFrame(audioFrame);
+ }
+ };
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(CustomAVCaptureActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(CustomAVCaptureActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(CustomAVCaptureActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCustomVideoTrack, mCustomAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(CustomAVCaptureActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(CustomAVCaptureActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ remoteVideoTracks.get(0).play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomAudioOnlyActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomAudioOnlyActivity.java
new file mode 100644
index 0000000..ba0526c
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomAudioOnlyActivity.java
@@ -0,0 +1,309 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioFrame;
+import com.qiniu.droid.rtc.QNAudioQuality;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomAudioTrack;
+import com.qiniu.droid.rtc.QNCustomAudioTrackConfig;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.capture.ExtAudioCapture;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 外部数据导入纯音频通话场景
+ * 本示例仅演示本地外部数据导入音频 Track 的发布和远端音频的订阅场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地外部数据导入音频 Track
+ * 5. 加入房间
+ * 6. 发布本地外部数据导入音频 Track
+ * 7. 订阅远端音频 Track
+ * 8. 离开房间
+ * 9. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class CustomAudioOnlyActivity extends AppCompatActivity {
+ private static final String TAG = "CustomAudioOnlyActivity";
+ private QNRTCClient mClient;
+ private QNCustomAudioTrack mCustomAudioTrack;
+
+ private ExtAudioCapture mExtAudioCapture;
+
+ private TextView mRemoteTrackTipsView;
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_custom_audio_only);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地麦克风音频采集 Track
+ initLocalTracks();
+ // 初始化外部采集实例
+ initExtCapture();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mExtAudioCapture.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mExtAudioCapture.stopCapture();
+ if (isFinishing() && mClient != null) {
+ // 8. 离开房间
+ mClient.leave();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 9. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 初始化本地视图
+ */
+ private void initView() {
+ // 初始化远端音频提示视图
+ mRemoteTrackTipsView = findViewById(R.id.remote_window_tips_view);
+ }
+
+ /**
+ * 初始化本地麦克风采集 Track
+ */
+ private void initLocalTracks() {
+ // 初始化外部导入音频 Track
+ QNCustomAudioTrackConfig customAudioTrackConfig = new QNCustomAudioTrackConfig(Config.TAG_CUSTOM_AUDIO_TRACK)
+ .setAudioQuality(new QNAudioQuality(Config.DEFAULT_AUDIO_SAMPLE_RATE, Config.DEFAULT_AUDIO_CHANNEL_COUNT,
+ 16, Config.DEFAULT_AUDIO_BITRATE)); // 设置外部音频数据导入的目标编码参数
+ mCustomAudioTrack = QNRTC.createCustomAudioTrack(customAudioTrackConfig);
+ }
+
+ /**
+ * 初始化外部采集实例
+ */
+ private void initExtCapture() {
+ mExtAudioCapture = new ExtAudioCapture();
+ mExtAudioCapture.setOnAudioFrameCapturedListener(mOnAudioFrameCapturedListener);
+ }
+
+ private final ExtAudioCapture.OnAudioFrameCapturedListener mOnAudioFrameCapturedListener = new ExtAudioCapture.OnAudioFrameCapturedListener() {
+ @Override
+ public void onAudioFrameCaptured(byte[] audioData) {
+ if (mCustomAudioTrack == null || TextUtils.isEmpty(mCustomAudioTrack.getTrackID())) {
+ return;
+ }
+ // 推送自定义音频数据
+ // 数据导入支持情况,请参考 https://developer.qiniu.com/rtc/8767/audio-and-video-collection-android#5
+ QNAudioFrame audioFrame = new QNAudioFrame(ByteBuffer.wrap(audioData), audioData.length,
+ 16, Config.DEFAULT_AUDIO_SAMPLE_RATE, Config.DEFAULT_AUDIO_CHANNEL_COUNT);
+ mCustomAudioTrack.pushAudioFrame(audioFrame);
+ }
+ };
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(CustomAudioOnlyActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地外部数据导入音频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(CustomAudioOnlyActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(CustomAudioOnlyActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCustomAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(CustomAudioOnlyActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音频 Track
+ for (QNRemoteTrack remoteTrack : trackList) {
+ if (remoteTrack.isAudio()) {
+ mClient.subscribe(remoteTrack);
+ }
+ }
+ } else {
+ ToastUtils.showShortToast(CustomAudioOnlyActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteTrackTipsView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (remoteUserID.equals(mFirstRemoteUserID) && !remoteAudioTracks.isEmpty()) {
+ // 成功订阅远端音频 Track 后,SDK 会默认对音频 Track 进行渲染,无需其他操作
+ mRemoteTrackTipsView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomMessageActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomMessageActivity.java
new file mode 100644
index 0000000..384a1de
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomMessageActivity.java
@@ -0,0 +1,363 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.api.examples.utils.Utils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import org.json.JSONObject;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 音视频通话 + 自定义消息发送场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地音视频 Track
+ * 5. 加入房间
+ * 6. 发布本地音视频 Track
+ * 7. 订阅远端音视频 Track
+ * 8. 发送自定义消息
+ * 9. 离开房间
+ * 10. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 发送消息接口,请参考 https://developer.qiniu.com/rtc/8684/QNRTCClient#sendMessage[1/2]
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class CustomMessageActivity extends AppCompatActivity {
+ private static final String TAG = "CameraMicrophoneActivity";
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+
+ private EditText mCustomMessageEt;
+ private String mUserID;
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_custom_message);
+
+ JSONObject roomInfo = Utils.parseRoomToken(Config.ROOM_TOKEN);
+ mUserID = roomInfo.optString(Config.KEY_USER_ID);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地音视频 Track
+ initLocalTracks();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 9. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 10. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ public void onClickSendMessage(View view) {
+ if (mClient != null) {
+ // 8. 发送自定义消息
+ String content = mCustomMessageEt.getText().toString().trim();
+ if (!TextUtils.isEmpty(content)) {
+ QNCustomMessage localMessage = new QNCustomMessage(UUID.randomUUID().toString(),
+ mUserID, content, System.currentTimeMillis() / 1000);
+ mClient.sendMessage(localMessage.getID(), localMessage.getContent());
+ }
+ // clear text
+ mCustomMessageEt.setText("");
+ }
+ }
+
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ mCustomMessageEt = findViewById(R.id.custom_message_edit_text);
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(false); // 设置是否开启大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ private void showContentArrivedDialog(QNCustomMessage message) {
+ View view = LayoutInflater.from(this).inflate(R.layout.dialog_tips, null);
+ TextView content = view.findViewById(R.id.dialog_content_text);
+ content.setText(String.format(getString(R.string.custom_message_arrived), message.getUserID(), message.getID(),
+ message.getContent(), DateFormat.getDateTimeInstance().format(new Date(message.getTimestamp() * 1000))));
+
+ final Dialog dialog = new AlertDialog.Builder(this, R.style.DialogTheme)
+ .create();
+ dialog.show();
+ dialog.setContentView(view);
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(CustomMessageActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(CustomMessageActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(CustomMessageActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(CustomMessageActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(CustomMessageActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ remoteVideoTracks.get(0).play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+ // 接收到远端自定义消息
+ showContentArrivedDialog(message);
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomTranscodingLiveStreamingActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomTranscodingLiveStreamingActivity.java
new file mode 100644
index 0000000..86bd8fa
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/CustomTranscodingLiveStreamingActivity.java
@@ -0,0 +1,646 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNLiveStreamingErrorInfo;
+import com.qiniu.droid.rtc.QNLiveStreamingListener;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNRenderMode;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNTranscodingLiveStreamingConfig;
+import com.qiniu.droid.rtc.QNTranscodingLiveStreamingImage;
+import com.qiniu.droid.rtc.QNTranscodingLiveStreamingTrack;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.api.examples.utils.Utils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.SwitchCompat;
+
+/**
+ * 1v1 音视频通话 + 自定义合流转推任务配置场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 设置 CDN 转推事件监听器
+ * 5. 创建本地音视频 Track
+ * 6. 加入房间
+ * 7. 发布本地音视频 Track
+ * 8. 订阅远端音视频 Track
+ * 9. 创建并开始自定义合流转推任务
+ * 10. 配置(新增/移除)合流布局
+ * 11. 停止自定义合流转推任务
+ * 12. 离开房间
+ * 13. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - CDN 转推任务文档,请参考 https://developer.qiniu.com/rtc/8770/turn-the-cdn-push-android
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class CustomTranscodingLiveStreamingActivity extends AppCompatActivity {
+ private static final String TAG = "CustomTranscodingLiveStreamingActivity";
+
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNRemoteVideoTrack mRemoteVideoTrack;
+ private QNRemoteAudioTrack mRemoteAudioTrack;
+ private QNTranscodingLiveStreamingConfig mTranscodingLiveStreamingConfig;
+ private QNRenderMode mRenderMode = QNRenderMode.ASPECT_FILL;
+
+ private final List mTranscodingLiveStreamingTracks = new ArrayList<>();
+
+ private EditText mPublishUrlEditText;
+ private EditText mConfigWidthEditText;
+ private EditText mConfigHeightEditText;
+ private EditText mConfigBitrateEditText;
+ private EditText mConfigFrameRateEditText;
+ private EditText mXEditText;
+ private EditText mYEditText;
+ private EditText mWidthEditText;
+ private EditText mHeightEditText;
+ private EditText mZOrderEditText;
+ private SwitchCompat mWatermarkSwitch;
+ private SwitchCompat mBackgroundSwitch;
+
+ private String mUserID;
+ private String mRoomName;
+ private String mFirstRemoteUserID = null;
+ private boolean mIsLocalUserConfig = true;
+ private boolean mIsLocalPublished;
+ private volatile LiveStreamingState mCurrentStreamingState = LiveStreamingState.IDLE;
+
+ private enum LiveStreamingState {
+ IDLE,
+ CONNECTING,
+ STREAMING
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_custom_transcoding_live_streaming);
+
+ JSONObject roomInfo = Utils.parseRoomToken(Config.ROOM_TOKEN);
+ mUserID = roomInfo.optString(Config.KEY_USER_ID);
+ mRoomName = roomInfo.optString(Config.KEY_ROOM_NAME);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 4. 设置 CDN 转推事件监听器
+ mClient.setLiveStreamingListener(mLiveStreamingListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 5. 创建本地音视频 Track
+ initLocalTracks();
+ // 6. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 12. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 13. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 开始自定义合流转推任务
+ */
+ public void onClickStartLiveStreaming(View view) {
+ if (TextUtils.isEmpty(mPublishUrlEditText.getText().toString()) || !mPublishUrlEditText.getText().toString().startsWith("rtmp")) {
+ ToastUtils.showShortToast(this, getString(R.string.invalid_publish_url_toast));
+ return;
+ }
+ if (!mIsLocalPublished) {
+ ToastUtils.showShortToast(this, getString(R.string.publish_first_toast));
+ return;
+ }
+ if (TextUtils.isEmpty(mConfigWidthEditText.getText()) || TextUtils.isEmpty(mConfigHeightEditText.getText())
+ || TextUtils.isEmpty(mConfigBitrateEditText.getText()) || TextUtils.isEmpty(mConfigFrameRateEditText.getText())) {
+ ToastUtils.showShortToast(this, getString(R.string.invalid_parameters_toast));
+ return;
+ }
+ if (mCurrentStreamingState != LiveStreamingState.IDLE) {
+ ToastUtils.showShortToast(this, getString(R.string.live_streaming_not_idle_toast));
+ return;
+ }
+ mCurrentStreamingState = LiveStreamingState.CONNECTING;
+ // 9. 创建并开始自定义合流转推任务
+ // 创建合流转推配置类实例
+ mTranscodingLiveStreamingConfig = new QNTranscodingLiveStreamingConfig();
+ mTranscodingLiveStreamingConfig.setStreamID(mRoomName + "-" + mUserID); // 设置合流转推任务的 streamID,streamID 为一个转推任务的唯一标识
+ mTranscodingLiveStreamingConfig.setUrl(mPublishUrlEditText.getText().toString()); // 设置推流地址
+ mTranscodingLiveStreamingConfig.setWidth(Integer.parseInt(mConfigWidthEditText.getText().toString())); // 设置合流画布的宽
+ mTranscodingLiveStreamingConfig.setHeight(Integer.parseInt(mConfigHeightEditText.getText().toString())); // 设置合流画布的高
+ mTranscodingLiveStreamingConfig.setBitrate(Integer.parseInt(mConfigBitrateEditText.getText().toString())); // 设置合流画布的码率
+ mTranscodingLiveStreamingConfig.setVideoFrameRate(Integer.parseInt(mConfigFrameRateEditText.getText().toString())); // 设置合流画布的帧率
+ mTranscodingLiveStreamingConfig.setRenderMode(mRenderMode);
+ if (mWatermarkSwitch.isChecked()) {
+ QNTranscodingLiveStreamingImage watermarkImage = new QNTranscodingLiveStreamingImage();
+ watermarkImage.setUrl("http://pili-playback.qnsdk.com/qiniu-logo-110-34.png"); // 设置水印图片地址,仅支持 HTTP 链接
+ watermarkImage.setX(0); // 设置水印位置的 x 坐标,默认为 0
+ watermarkImage.setY(0); // 设置水印位置的 y 坐标,默认为 0
+ watermarkImage.setWidth(100); // 设置水印的宽度,需自行指定
+ watermarkImage.setHeight(30); // 设置水印的高度,需自行指定
+ mTranscodingLiveStreamingConfig.setWatermarks(Collections.singletonList(watermarkImage));
+ }
+ if (mBackgroundSwitch.isChecked()) {
+ QNTranscodingLiveStreamingImage backgroundImage = new QNTranscodingLiveStreamingImage();
+ backgroundImage.setUrl("http://pili-playback.qnsdk.com/ivs_background_1280x720.png"); // 设置背景图片的地址,仅支持 HTTP 链接
+ backgroundImage.setX(0); // 设置背景图片的 x 坐标,默认为 0
+ backgroundImage.setY(0); // 设置背景图片的 y 坐标,默认为 0
+ backgroundImage.setWidth(Integer.parseInt(mConfigWidthEditText.getText().toString())); // 设置背景图片的宽度,需自行指定
+ backgroundImage.setHeight(Integer.parseInt(mConfigHeightEditText.getText().toString())); // 设置背景图片的高度,需自行指定
+ mTranscodingLiveStreamingConfig.setBackground(backgroundImage);
+ }
+ mClient.startLiveStreaming(mTranscodingLiveStreamingConfig);
+ }
+
+ /**
+ * 停止自定义合流转推任务
+ */
+ public void onClickStopLiveStreaming(View view) {
+ // 需要在转推任务成功接收到 QNLiveStreamingListener.onStarted 时,转推任务开始之后,才可以停止该任务
+ if (mCurrentStreamingState != LiveStreamingState.STREAMING) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, getString(R.string.no_live_streaming_toast));
+ return;
+ }
+ // 11. 停止自定义合流转推任务
+ mClient.stopLiveStreaming(mTranscodingLiveStreamingConfig);
+ }
+
+ /**
+ * 新增合流布局
+ */
+ public void onClickAddTranscodingTracks(View view) {
+ if (mTranscodingLiveStreamingConfig == null) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, getString(R.string.no_live_streaming_toast));
+ return;
+ }
+ if (TextUtils.isEmpty(mXEditText.getText()) || TextUtils.isEmpty(mYEditText.getText())
+ || TextUtils.isEmpty(mWidthEditText.getText()) || TextUtils.isEmpty(mHeightEditText.getText())
+ || TextUtils.isEmpty(mZOrderEditText.getText())) {
+ ToastUtils.showShortToast(this, getString(R.string.invalid_parameters_toast));
+ return;
+ }
+ if (mIsLocalUserConfig && !mIsLocalPublished) {
+ ToastUtils.showShortToast(this, getString(R.string.publish_first_toast));
+ return;
+ }
+ if (!mIsLocalUserConfig && mFirstRemoteUserID == null) {
+ ToastUtils.showShortToast(this, getString(R.string.subscribe_first_toast));
+ return;
+ }
+ List addLiveStreamingTracks = new ArrayList<>();
+ if (mIsLocalUserConfig || mRemoteVideoTrack != null) {
+ QNTranscodingLiveStreamingTrack videoTranscodingTrack = null;
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ if ((mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mCameraVideoTrack.getTrackID()))
+ || (!mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mRemoteVideoTrack.getTrackID()))) {
+ videoTranscodingTrack = liveStreamingTrack;
+ break;
+ }
+ }
+ // 设置视频 Track 的合流布局
+ if (videoTranscodingTrack == null) {
+ videoTranscodingTrack = new QNTranscodingLiveStreamingTrack();
+ videoTranscodingTrack.setTrackID(mIsLocalUserConfig ? mCameraVideoTrack.getTrackID() : mRemoteVideoTrack.getTrackID()); // 设置 TrackID
+ mTranscodingLiveStreamingTracks.add(videoTranscodingTrack);
+ }
+ videoTranscodingTrack.setX(Integer.parseInt(mXEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的 x 坐标
+ videoTranscodingTrack.setY(Integer.parseInt(mYEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的 y 坐标
+ videoTranscodingTrack.setWidth(Integer.parseInt(mWidthEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的宽度
+ videoTranscodingTrack.setHeight(Integer.parseInt(mHeightEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的高度
+ videoTranscodingTrack.setZOrder(Integer.parseInt(mZOrderEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的层级
+ videoTranscodingTrack.setRenderMode(mRenderMode); // 设置视频画面的渲染模式
+ addLiveStreamingTracks.add(videoTranscodingTrack);
+ }
+
+ if (mIsLocalUserConfig || mRemoteAudioTrack != null) {
+ QNTranscodingLiveStreamingTrack audioTranscodingTrack = null;
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ if ((mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mMicrophoneAudioTrack.getTrackID()))
+ || (!mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mRemoteAudioTrack.getTrackID()))) {
+ audioTranscodingTrack = liveStreamingTrack;
+ break;
+ }
+ }
+ if (audioTranscodingTrack == null) {
+ // 将音频 Track 添加到合流任务中,仅需配置 TrackID 即可
+ audioTranscodingTrack = new QNTranscodingLiveStreamingTrack();
+ audioTranscodingTrack.setTrackID(mIsLocalUserConfig ? mMicrophoneAudioTrack.getTrackID() : mRemoteAudioTrack.getTrackID()); // 设置 TrackID
+ mTranscodingLiveStreamingTracks.add(audioTranscodingTrack);
+ }
+ addLiveStreamingTracks.add(audioTranscodingTrack);
+ }
+ // 10. 配置(新增)合流布局,streamID 为 null 代表设置默认合流任务的合流布局
+ mClient.setTranscodingLiveStreamingTracks(mTranscodingLiveStreamingConfig.getStreamID(), addLiveStreamingTracks);
+ }
+
+ /**
+ * 移除合流布局
+ */
+ public void onClickRemoveTranscodingTracks(View view) {
+ if (mTranscodingLiveStreamingConfig == null) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, getString(R.string.no_live_streaming_toast));
+ return;
+ }
+ List removeTranscodingTracks = new ArrayList<>();
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ if (mIsLocalUserConfig &&
+ (liveStreamingTrack.getTrackID().equals(mCameraVideoTrack.getTrackID())
+ || liveStreamingTrack.getTrackID().equals(mMicrophoneAudioTrack.getTrackID()))) {
+ removeTranscodingTracks.add(liveStreamingTrack);
+ }
+ if (!mIsLocalUserConfig &&
+ (liveStreamingTrack.getTrackID().equals(mRemoteVideoTrack.getTrackID())
+ || liveStreamingTrack.getTrackID().equals(mRemoteAudioTrack.getTrackID()))) {
+ removeTranscodingTracks.add(liveStreamingTrack);
+ }
+ }
+ if (!removeTranscodingTracks.isEmpty()) {
+ // 10. 配置(移除)合流布局,streamID 为 null 代表设置默认合流任务的合流布局
+ mClient.removeTranscodingLiveStreamingTracks(mTranscodingLiveStreamingConfig.getStreamID(), removeTranscodingTracks);
+ }
+ }
+
+ /**
+ * 初始化视图
+ */
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ mPublishUrlEditText = findViewById(R.id.publish_url_edit_text);
+ mConfigWidthEditText = findViewById(R.id.transcoding_config_width_edit_text);
+ mConfigHeightEditText = findViewById(R.id.transcoding_config_height_edit_text);
+ mConfigBitrateEditText = findViewById(R.id.transcoding_config_bitrate_edit_text);
+ mConfigFrameRateEditText = findViewById(R.id.transcoding_config_frame_rate_edit_text);
+ mWatermarkSwitch = findViewById(R.id.watermark_switch);
+ mBackgroundSwitch = findViewById(R.id.background_image_switch);
+ mXEditText = findViewById(R.id.layout_x_edit_text);
+ mYEditText = findViewById(R.id.layout_y_edit_text);
+ mWidthEditText = findViewById(R.id.layout_width_edit_text);
+ mHeightEditText = findViewById(R.id.layout_height_edit_text);
+ mZOrderEditText = findViewById(R.id.layout_z_order_edit_text);
+
+ RadioGroup roleSelectRadioGroup = findViewById(R.id.role_select_radio_group);
+ roleSelectRadioGroup.setOnCheckedChangeListener((group, checkedId) -> mIsLocalUserConfig = checkedId == R.id.local_user_setting);
+ RadioGroup renderModeRadioGroup = findViewById(R.id.render_mode_group);
+ renderModeRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (checkedId == R.id.aspect_fill) {
+ mRenderMode = QNRenderMode.ASPECT_FILL;
+ } else if (checkedId == R.id.aspect_fit) {
+ mRenderMode = QNRenderMode.ASPECT_FIT;
+ } else {
+ mRenderMode = QNRenderMode.FILL;
+ }
+ });
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(false); // 设置是否开启大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+ // 初始化并配置美颜
+ mCameraVideoTrack.setBeauty(new QNBeautySetting(0.5f, 0.5f, 0.5f));
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ /**
+ * CDN 转推事件监听
+ * CDN 转推使用指南可参考 https://developer.qiniu.com/rtc/8770/turn-the-cdn-push-android
+ */
+ private final QNLiveStreamingListener mLiveStreamingListener = new QNLiveStreamingListener() {
+ /**
+ * 转推任务开始后会触发该回调
+ *
+ * @param streamID 已开始的转推任务的 streamID
+ */
+ @Override
+ public void onStarted(String streamID) {
+ mCurrentStreamingState = LiveStreamingState.STREAMING;
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, String.format(getString(R.string.start_live_streaming_success), streamID));
+ }
+
+ /**
+ * 转推任务停止后会触发该回调
+ *
+ * @param streamID 已停止的转推任务的 streamID
+ */
+ @Override
+ public void onStopped(String streamID) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.stop_live_streaming_success), streamID));
+ if (mTranscodingLiveStreamingConfig != null && streamID.equals(mTranscodingLiveStreamingConfig.getStreamID())) {
+ mTranscodingLiveStreamingConfig = null;
+ mTranscodingLiveStreamingTracks.clear();
+ }
+ mCurrentStreamingState = LiveStreamingState.IDLE;
+ }
+
+ /**
+ * 合流转推任务布局更新时会触发该回调
+ *
+ * @param streamID 布局更新的转推任务的 streamID
+ */
+ @Override
+ public void onTranscodingTracksUpdated(String streamID) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, getString(R.string.transcoding_layout_updated_toast));
+ }
+
+ /**
+ * 转推任务发生错误是会触发该回调
+ * CDN 转推任务错误码可参考 https://developer.qiniu.com/rtc/9904/rtc-error-code-android#5
+ *
+ * @param streamID 出错的转推任务的 streamID
+ * @param errorInfo 错误信息
+ */
+ @Override
+ public void onError(String streamID, QNLiveStreamingErrorInfo errorInfo) {
+ if (errorInfo == null) {
+ return;
+ }
+ switch (errorInfo.type) {
+ case START:
+ // 开始单路转推场景下出错
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.live_streaming_error), "开始", errorInfo.code));
+ mTranscodingLiveStreamingConfig = null;
+ mCurrentStreamingState = LiveStreamingState.IDLE;
+ break;
+ case STOP:
+ // 停止单路转推场景下出错
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.live_streaming_error), "停止", errorInfo.code));
+ break;
+ }
+ }
+ };
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 7. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ mIsLocalPublished = true;
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(CustomTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 8. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(CustomTranscodingLiveStreamingActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteAudioTrack = null;
+ mRemoteVideoTrack = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ List unpublishedTrack = new ArrayList<>();
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ for (QNRemoteTrack remoteTrack : trackList) {
+ if (liveStreamingTrack.getTrackID().equals(remoteTrack.getTrackID())) {
+ unpublishedTrack.add(liveStreamingTrack);
+ }
+ }
+ }
+ mTranscodingLiveStreamingTracks.removeAll(unpublishedTrack);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ mRemoteVideoTrack = remoteVideoTracks.get(0);
+ mRemoteVideoTrack.play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ if (!remoteAudioTracks.isEmpty()) {
+ mRemoteAudioTrack = remoteAudioTracks.get(0);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/DefaultTranscodingLiveStreamingActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/DefaultTranscodingLiveStreamingActivity.java
new file mode 100644
index 0000000..fb937ad
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/DefaultTranscodingLiveStreamingActivity.java
@@ -0,0 +1,513 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNLiveStreamingErrorInfo;
+import com.qiniu.droid.rtc.QNLiveStreamingListener;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNTranscodingLiveStreamingTrack;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 音视频通话 + 默认合流转推任务配置场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 设置 CDN 转推事件监听器
+ * 5. 创建本地音视频 Track
+ * 6. 加入房间
+ * 7. 发布本地音视频 Track
+ * 8. 订阅远端音视频 Track
+ * 9. 配置(新增/移除)合流布局
+ * 10. 离开房间
+ * 11. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - CDN 转推任务文档,请参考 https://developer.qiniu.com/rtc/8770/turn-the-cdn-push-android
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class DefaultTranscodingLiveStreamingActivity extends AppCompatActivity {
+ private static final String TAG = "DefaultTranscodingLiveStreamingActivity";
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNRemoteVideoTrack mRemoteVideoTrack;
+ private QNRemoteAudioTrack mRemoteAudioTrack;
+
+ private final List mTranscodingLiveStreamingTracks = new ArrayList<>();
+
+ private EditText mXEditText;
+ private EditText mYEditText;
+ private EditText mWidthEditText;
+ private EditText mHeightEditText;
+ private EditText mZOrderEditText;
+
+ private String mFirstRemoteUserID = null;
+ private boolean mIsLocalUserConfig = true;
+ private boolean mIsLocalPublished;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_default_transcoding_live_streaming);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 4. 设置 CDN 转推事件监听器
+ mClient.setLiveStreamingListener(mLiveStreamingListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 5. 创建本地音视频 Track
+ initLocalTracks();
+ // 6. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 10. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 11. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 新增合流布局
+ */
+ public void onClickAddTranscodingTracks(View view) {
+ if (TextUtils.isEmpty(mXEditText.getText()) || TextUtils.isEmpty(mYEditText.getText())
+ || TextUtils.isEmpty(mWidthEditText.getText()) || TextUtils.isEmpty(mHeightEditText.getText())
+ || TextUtils.isEmpty(mZOrderEditText.getText())) {
+ ToastUtils.showShortToast(this, getString(R.string.invalid_parameters_toast));
+ return;
+ }
+ if (mIsLocalUserConfig && !mIsLocalPublished) {
+ ToastUtils.showShortToast(this, getString(R.string.publish_first_toast));
+ return;
+ }
+ if (!mIsLocalUserConfig && mFirstRemoteUserID == null) {
+ ToastUtils.showShortToast(this, getString(R.string.subscribe_first_toast));
+ return;
+ }
+ List addLiveStreamingTracks = new ArrayList<>();
+ if (mIsLocalUserConfig || mRemoteVideoTrack != null) {
+ QNTranscodingLiveStreamingTrack videoTranscodingTrack = null;
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ if ((mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mCameraVideoTrack.getTrackID()))
+ || (!mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mRemoteVideoTrack.getTrackID()))) {
+ videoTranscodingTrack = liveStreamingTrack;
+ break;
+ }
+ }
+ // 设置视频 Track 的合流布局
+ if (videoTranscodingTrack == null) {
+ videoTranscodingTrack = new QNTranscodingLiveStreamingTrack();
+ videoTranscodingTrack.setTrackID(mIsLocalUserConfig ? mCameraVideoTrack.getTrackID() : mRemoteVideoTrack.getTrackID()); // 设置 TrackID
+ mTranscodingLiveStreamingTracks.add(videoTranscodingTrack);
+ }
+ videoTranscodingTrack.setX(Integer.parseInt(mXEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的 x 坐标
+ videoTranscodingTrack.setY(Integer.parseInt(mYEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的 y 坐标
+ videoTranscodingTrack.setWidth(Integer.parseInt(mWidthEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的宽度
+ videoTranscodingTrack.setHeight(Integer.parseInt(mHeightEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的高度
+ videoTranscodingTrack.setZOrder(Integer.parseInt(mZOrderEditText.getText().toString())); // 设置视频 Track 在合流布局中位置的层级
+ addLiveStreamingTracks.add(videoTranscodingTrack);
+ }
+
+ if (mIsLocalUserConfig || mRemoteAudioTrack != null) {
+ QNTranscodingLiveStreamingTrack audioTranscodingTrack = null;
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ if ((mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mMicrophoneAudioTrack.getTrackID()))
+ || (!mIsLocalUserConfig && liveStreamingTrack.getTrackID().equals(mRemoteAudioTrack.getTrackID()))) {
+ audioTranscodingTrack = liveStreamingTrack;
+ break;
+ }
+ }
+ if (audioTranscodingTrack == null) {
+ // 将音频 Track 添加到合流任务中,仅需配置 TrackID 即可
+ audioTranscodingTrack = new QNTranscodingLiveStreamingTrack();
+ audioTranscodingTrack.setTrackID(mIsLocalUserConfig ? mMicrophoneAudioTrack.getTrackID() : mRemoteAudioTrack.getTrackID());; // 设置 TrackID
+ mTranscodingLiveStreamingTracks.add(audioTranscodingTrack);
+ }
+ addLiveStreamingTracks.add(audioTranscodingTrack);
+ }
+ // 9. 配置(新增)合流布局,streamID 为 null 代表设置默认合流任务的合流布局
+ mClient.setTranscodingLiveStreamingTracks(null, addLiveStreamingTracks);
+ }
+
+ /**
+ * 移除合流布局
+ */
+ public void onClickRemoveTranscodingTracks(View view) {
+ List removeTranscodingTracks = new ArrayList<>();
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ if (mIsLocalUserConfig &&
+ (liveStreamingTrack.getTrackID().equals(mCameraVideoTrack.getTrackID())
+ || liveStreamingTrack.getTrackID().equals(mMicrophoneAudioTrack.getTrackID()))) {
+ removeTranscodingTracks.add(liveStreamingTrack);
+ }
+ if (!mIsLocalUserConfig &&
+ (liveStreamingTrack.getTrackID().equals(mRemoteVideoTrack.getTrackID())
+ || liveStreamingTrack.getTrackID().equals(mRemoteAudioTrack.getTrackID()))) {
+ removeTranscodingTracks.add(liveStreamingTrack);
+ }
+ }
+ if (!removeTranscodingTracks.isEmpty()) {
+ // 9. 配置(移除)合流布局,streamID 为 null 代表设置默认合流任务的合流布局
+ mClient.removeTranscodingLiveStreamingTracks(null, removeTranscodingTracks);
+ }
+ }
+
+ /**
+ * 初始化视图
+ */
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ mXEditText = findViewById(R.id.layout_x_edit_text);
+ mYEditText = findViewById(R.id.layout_y_edit_text);
+ mWidthEditText = findViewById(R.id.layout_width_edit_text);
+ mHeightEditText = findViewById(R.id.layout_height_edit_text);
+ mZOrderEditText = findViewById(R.id.layout_z_order_edit_text);
+
+ RadioGroup roleSelectRadioGroup = findViewById(R.id.role_select_radio_group);
+ roleSelectRadioGroup.setOnCheckedChangeListener((group, checkedId) -> mIsLocalUserConfig = checkedId == R.id.local_user_setting);
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(false); // 设置是否开启大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+ // 初始化并配置美颜
+ mCameraVideoTrack.setBeauty(new QNBeautySetting(0.5f, 0.5f, 0.5f));
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ /**
+ * CDN 转推事件监听
+ * CDN 转推使用指南可参考 https://developer.qiniu.com/rtc/8770/turn-the-cdn-push-android
+ */
+ private final QNLiveStreamingListener mLiveStreamingListener = new QNLiveStreamingListener() {
+ /**
+ * 转推任务开始后会触发该回调
+ *
+ * @param streamID 已开始的转推任务的 streamID
+ */
+ @Override
+ public void onStarted(String streamID) {
+
+ }
+
+ /**
+ * 转推任务停止后会触发该回调
+ *
+ * @param streamID 已停止的转推任务的 streamID
+ */
+ @Override
+ public void onStopped(String streamID) {
+
+ }
+
+ /**
+ * 合流转推任务布局更新时会触发该回调
+ *
+ * @param streamID 布局更新的转推任务的 streamID
+ */
+ @Override
+ public void onTranscodingTracksUpdated(String streamID) {
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this, getString(R.string.transcoding_layout_updated_toast));
+ }
+
+ /**
+ * 转推任务发生错误是会触发该回调
+ * CDN 转推任务错误码可参考 https://developer.qiniu.com/rtc/9904/rtc-error-code-android#5
+ *
+ * @param streamID 出错的转推任务的 streamID
+ * @param errorInfo 错误信息
+ */
+ @Override
+ public void onError(String streamID, QNLiveStreamingErrorInfo errorInfo) {
+ if (errorInfo == null) {
+ return;
+ }
+ switch (errorInfo.type) {
+ case START:
+ // 开始单路转推场景下出错
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.live_streaming_error), "开始", errorInfo.code));
+ break;
+ case STOP:
+ // 停止单路转推场景下出错
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.live_streaming_error), "停止", errorInfo.code));
+ break;
+ }
+ }
+ };
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 7. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ mIsLocalPublished = true;
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(DefaultTranscodingLiveStreamingActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 8. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(DefaultTranscodingLiveStreamingActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteAudioTrack = null;
+ mRemoteVideoTrack = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ List unpublishedTrack = new ArrayList<>();
+ for (QNTranscodingLiveStreamingTrack liveStreamingTrack : mTranscodingLiveStreamingTracks) {
+ for (QNRemoteTrack remoteTrack : trackList) {
+ if (liveStreamingTrack.getTrackID().equals(remoteTrack.getTrackID())) {
+ unpublishedTrack.add(liveStreamingTrack);
+ }
+ }
+ }
+ mTranscodingLiveStreamingTracks.removeAll(unpublishedTrack);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ mRemoteVideoTrack = remoteVideoTracks.get(0);
+ mRemoteVideoTrack.play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ if (!remoteAudioTracks.isEmpty()) {
+ mRemoteAudioTrack = remoteAudioTracks.get(0);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/DirectLiveStreamingActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/DirectLiveStreamingActivity.java
new file mode 100644
index 0000000..ba0e1e7
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/DirectLiveStreamingActivity.java
@@ -0,0 +1,434 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNDirectLiveStreamingConfig;
+import com.qiniu.droid.rtc.QNLiveStreamingErrorInfo;
+import com.qiniu.droid.rtc.QNLiveStreamingListener;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRTCSetting;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.api.examples.utils.Utils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import org.json.JSONObject;
+
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 音视频通话 + 单路转推场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 设置 CDN 转推事件监听器
+ * 5. 创建本地音视频 Track
+ * 6. 加入房间
+ * 7. 发布本地音视频 Track(发布后可创建基于本地音视频 Track 的转推任务)
+ * 8. 订阅远端音视频 Track(订阅后可创建基于远端音视频 Track 的转推任务)
+ * 9. 离开房间
+ *10. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - CDN 转推使用指南,请参考 https://developer.qiniu.com/rtc/8770/turn-the-cdn-push-android
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class DirectLiveStreamingActivity extends AppCompatActivity {
+ private static final String TAG = "DirectLiveStreamingActivity";
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNDirectLiveStreamingConfig mDirectLiveStreamingConfig;
+
+ private TextView mPublishUrlEditText;
+
+ private String mUserID;
+ private String mRoomName;
+ private String mFirstRemoteUserID = null;
+ private boolean mIsLocalPublished;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_direct_live_streaming);
+
+ JSONObject roomInfo = Utils.parseRoomToken(Config.ROOM_TOKEN);
+ mUserID = roomInfo.optString(Config.KEY_USER_ID);
+ mRoomName = roomInfo.optString(Config.KEY_ROOM_NAME);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTCSetting setting = new QNRTCSetting()
+ .setMaintainResolution(true); // 开启固定分辨率,以避免单路转推场景下,动态分辨率造成的非预期问题
+ QNRTC.init(this, setting, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 设置 CDN 转推事件监听器
+ mClient.setLiveStreamingListener(mLiveStreamingListener);
+ // 5. 创建本地音视频 Track
+ initLocalTracks();
+ // 6. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 9. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 10. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ public void onClickStartLiveStreaming(View view) {
+ if ("".equals(mPublishUrlEditText.getText().toString()) || !mPublishUrlEditText.getText().toString().startsWith("rtmp")) {
+ ToastUtils.showShortToast(this, getString(R.string.invalid_publish_url_toast));
+ return;
+ }
+ if (mDirectLiveStreamingConfig != null) {
+ ToastUtils.showShortToast(this, getString(R.string.already_exist_live_streaming_toast));
+ return;
+ }
+ if (!mIsLocalPublished) {
+ ToastUtils.showShortToast(this, getString(R.string.publish_first_toast));
+ return;
+ }
+ // 创建单路转推配置类实例
+ mDirectLiveStreamingConfig = new QNDirectLiveStreamingConfig();
+ mDirectLiveStreamingConfig.setStreamID(mRoomName + "-" + mUserID); // 设置单路转推 streamID,streamID 为一个转推任务的唯一标识
+ mDirectLiveStreamingConfig.setUrl(mPublishUrlEditText.getText().toString()); // 设置推流地址
+ mDirectLiveStreamingConfig.setAudioTrack(mMicrophoneAudioTrack); // 设置待转推的音频 Track
+ mDirectLiveStreamingConfig.setVideoTrack(mCameraVideoTrack); // 设置待转推的视频 Track
+ mClient.startLiveStreaming(mDirectLiveStreamingConfig);
+ }
+
+ public void onClickStopLiveStreaming(View view) {
+ if (mDirectLiveStreamingConfig != null) {
+ mClient.stopLiveStreaming(mDirectLiveStreamingConfig);
+ }
+ }
+
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ mPublishUrlEditText = findViewById(R.id.publish_url_edit_text);
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(false); // 设置是否开启大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+ // 初始化并配置美颜
+ mCameraVideoTrack.setBeauty(new QNBeautySetting(0.5f, 0.5f, 0.5f));
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ /**
+ * CDN 转推事件监听
+ * CDN 转推使用指南可参考 https://developer.qiniu.com/rtc/8770/turn-the-cdn-push-android
+ */
+ private final QNLiveStreamingListener mLiveStreamingListener = new QNLiveStreamingListener() {
+ /**
+ * 转推任务开始后会触发该回调
+ *
+ * @param streamID 已开始的转推任务的 streamID
+ */
+ @Override
+ public void onStarted(String streamID) {
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this, String.format(getString(R.string.start_live_streaming_success), streamID));
+ }
+
+ /**
+ * 转推任务停止后会触发该回调
+ *
+ * @param streamID 已停止的转推任务的 streamID
+ */
+ @Override
+ public void onStopped(String streamID) {
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this, String.format(getString(R.string.stop_live_streaming_success), streamID));
+ if (mDirectLiveStreamingConfig != null && streamID.equals(mDirectLiveStreamingConfig.getStreamID())) {
+ mDirectLiveStreamingConfig = null;
+ }
+ }
+
+ /**
+ * 合流转推任务布局更新时会触发该回调
+ *
+ * @param streamID 布局更新的转推任务的 streamID
+ */
+ @Override
+ public void onTranscodingTracksUpdated(String streamID) {
+
+ }
+
+ /**
+ * 转推任务发生错误是会触发该回调
+ * CDN 转推任务错误码可参考 https://developer.qiniu.com/rtc/9904/rtc-error-code-android#5
+ *
+ * @param streamID 出错的转推任务的 streamID
+ * @param errorInfo 错误信息
+ */
+ @Override
+ public void onError(String streamID, QNLiveStreamingErrorInfo errorInfo) {
+ if (errorInfo == null) {
+ return;
+ }
+ switch (errorInfo.type) {
+ case START:
+ // 开始单路转推场景下出错
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this,
+ String.format(getString(R.string.live_streaming_error), "开始", errorInfo.code));
+ break;
+ case STOP:
+ // 停止单路转推场景下出错
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this,
+ String.format(getString(R.string.live_streaming_error), "停止", errorInfo.code));
+ break;
+ }
+ }
+ };
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 7. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ mIsLocalPublished = true;
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(DirectLiveStreamingActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(DirectLiveStreamingActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ remoteVideoTracks.get(0).play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MainActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MainActivity.java
new file mode 100644
index 0000000..c81f066
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MainActivity.java
@@ -0,0 +1,72 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Toast;
+
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.PermissionChecker;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.api.examples.utils.Utils;
+
+public class MainActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+
+ public void onClickScenes(View v) {
+ if (v.getId() != R.id.screen_microphone && !isPermissionOK()) {
+ return;
+ }
+ if (Utils.parseRoomToken(Config.ROOM_TOKEN) == null) {
+ ToastUtils.showShortToast(this, getString(R.string.invalid_token_toast));
+ return;
+ }
+ Intent intent = null;
+ if (v.getId() == R.id.camera_microphone) {
+ intent = new Intent(this, CameraMicrophoneActivity.class);
+ } else if (v.getId() == R.id.screen_microphone) {
+ intent = new Intent(this, ScreenCaptureActivity.class);
+ } else if (v.getId() == R.id.custom_video_audio) {
+ intent = new Intent(this, CustomAVCaptureActivity.class);
+ } else if (v.getId() == R.id.microphone_only) {
+ intent = new Intent(this, MicrophoneOnlyActivity.class);
+ } else if (v.getId() == R.id.custom_audio_only) {
+ intent = new Intent(this, CustomAudioOnlyActivity.class);
+ } else if (v.getId() == R.id.direct_streaming) {
+ intent = new Intent(this, DirectLiveStreamingActivity.class);
+ } else if (v.getId() == R.id.custom_message) {
+ intent = new Intent(this, CustomMessageActivity.class);
+ } else if (v.getId() == R.id.multi_profile) {
+ intent = new Intent(this, MultiProfileActivity.class);
+ } else if (v.getId() == R.id.media_statistics) {
+ intent = new Intent(this, MediaStatisticsActivity.class);
+ } else if (v.getId() == R.id.audio_mixing) {
+ intent = new Intent(this, AudioMixerActivity.class);
+ } else if (v.getId() == R.id.default_transcoding_streaming) {
+ intent = new Intent(this, DefaultTranscodingLiveStreamingActivity.class);
+ } else if (v.getId() == R.id.custom_transcoding_streaming) {
+ intent = new Intent(this, CustomTranscodingLiveStreamingActivity.class);
+ }
+ if (intent != null) {
+ startActivity(intent);
+ }
+ }
+
+ private boolean isPermissionOK() {
+ PermissionChecker checker = new PermissionChecker(this);
+ boolean isPermissionOK = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checker.checkPermission();
+ if (!isPermissionOK) {
+ Toast.makeText(this, "Some permissions is not approved !!!", Toast.LENGTH_SHORT).show();
+ }
+ return isPermissionOK;
+ }
+}
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MediaStatisticsActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MediaStatisticsActivity.java
new file mode 100644
index 0000000..a607149
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MediaStatisticsActivity.java
@@ -0,0 +1,483 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNLocalAudioTrackStats;
+import com.qiniu.droid.rtc.QNLocalVideoTrackStats;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNNetworkQuality;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteAudioTrackStats;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrackStats;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 音视频通话 + 通话质量信息统计场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地音视频 Track
+ * 5. 加入房间
+ * 6. 发布本地音视频 Track
+ * 7. 订阅远端音视频 Track
+ * 8. 开启通话质量统计
+ * 9. 离开房间
+ * 10. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 通话信息统计文档,请参考 https://developer.qiniu.com/rtc/9860/quality-statistics-android
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class MediaStatisticsActivity extends AppCompatActivity {
+ private static final String TAG = "MediaStatisticsActivity";
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+
+ private String mFirstRemoteUserID = null;
+
+ private Timer mStatsTimer;
+ private TextView mLocalUplinkNetworkQualityText;
+ private TextView mLocalDownlinkNetworkQualityText;
+ private TextView mLocalAudioUplinkBitrateText;
+ private TextView mLocalAudioUplinkRttText;
+ private TextView mLocalAudioUplinkLostRateText;
+ private TextView mLocalVideoProfileText;
+ private TextView mLocalVideoUplinkFrameRateText;
+ private TextView mLocalVideoUplinkBitrateText;
+ private TextView mLocalVideoUplinkRttText;
+ private TextView mLocalVideoUplinkLostRateText;
+
+ private TextView mRemoteUplinkNetworkQualityText;
+ private TextView mRemoteDownlinkNetworkQualityText;
+ private TextView mRemoteAudioDownlinkBitrateText;
+ private TextView mRemoteAudioDownlinkLostRateText;
+ private TextView mRemoteAudioUplinkRttText;
+ private TextView mRemoteAudioUplinkLostRateText;
+ private TextView mRemoteVideoProfileText;
+ private TextView mRemoteVideoDownlinkFrameRateText;
+ private TextView mRemoteVideoDownlinkBitrateText;
+ private TextView mRemoteVideoDownlinkLostRateText;
+ private TextView mRemoteVideoUplinkRttText;
+ private TextView mRemoteVideoUplinkLostRateText;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_media_statistics);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地音视频 Track
+ initLocalTracks();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 9. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 10. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 初始化视图控件
+ */
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ mLocalDownlinkNetworkQualityText = findViewById(R.id.downlink_network_quality);
+ mLocalUplinkNetworkQualityText = findViewById(R.id.upload_network_quality);
+ mLocalAudioUplinkBitrateText = findViewById(R.id.local_audio_bitrate);
+ mLocalAudioUplinkRttText = findViewById(R.id.local_audio_rtt);
+ mLocalAudioUplinkLostRateText = findViewById(R.id.local_audio_lost_rate);
+ mLocalVideoProfileText = findViewById(R.id.local_video_profile);
+ mLocalVideoUplinkFrameRateText = findViewById(R.id.local_video_frame_rate);
+ mLocalVideoUplinkBitrateText = findViewById(R.id.local_video_bitrate);
+ mLocalVideoUplinkRttText = findViewById(R.id.local_video_rtt);
+ mLocalVideoUplinkLostRateText = findViewById(R.id.local_video_lost_rate);
+
+ mRemoteDownlinkNetworkQualityText = findViewById(R.id.remote_downlink_network_quality);
+ mRemoteUplinkNetworkQualityText = findViewById(R.id.remote_upload_network_quality);
+ mRemoteAudioDownlinkBitrateText = findViewById(R.id.remote_audio_downlink_bitrate);
+ mRemoteAudioDownlinkLostRateText = findViewById(R.id.remote_audio_downlink_lost_rate);
+ mRemoteAudioUplinkRttText = findViewById(R.id.remote_audio_uplink_rtt);
+ mRemoteAudioUplinkLostRateText = findViewById(R.id.remote_audio_uplink_lost_rate);
+ mRemoteVideoProfileText = findViewById(R.id.remote_video_profile);
+ mRemoteVideoDownlinkFrameRateText = findViewById(R.id.remote_video_downlink_frame_rate);
+ mRemoteVideoDownlinkBitrateText = findViewById(R.id.remote_video_downlink_bitrate);
+ mRemoteVideoDownlinkLostRateText = findViewById(R.id.remote_video_downlink_lost_rate);
+ mRemoteVideoUplinkRttText = findViewById(R.id.remote_video_uplink_rtt);
+ mRemoteVideoUplinkLostRateText = findViewById(R.id.remote_video_uplink_lost_rate);
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(true); // 设置是否开启大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+ // 初始化并配置美颜
+ mCameraVideoTrack.setBeauty(new QNBeautySetting(0.5f, 0.5f, 0.5f));
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ /**
+ * 开始通话质量统计
+ */
+ private synchronized void startStatisticsScheduler() {
+ mStatsTimer = new Timer();
+ mStatsTimer.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ runOnUiThread(() -> {
+ if (mClient != null) {
+ // 本地视频 Track 质量统计
+ Map> localVideoTrackStats = mClient.getLocalVideoTrackStats();
+ for (Map.Entry> entry : localVideoTrackStats.entrySet()) {
+ for (QNLocalVideoTrackStats stats : entry.getValue()) {
+ mLocalVideoProfileText.setText(stats.profile.name());
+ mLocalVideoUplinkBitrateText.setText(String.format(getString(R.string.bitrate), stats.uplinkBitrate / 1000));
+ mLocalVideoUplinkLostRateText.setText(String.format(getString(R.string.lost_rate), stats.uplinkLostRate));
+ mLocalVideoUplinkFrameRateText.setText(String.format(getString(R.string.fps), stats.uplinkFrameRate));
+ mLocalVideoUplinkRttText.setText(String.format(getString(R.string.rtt), stats.uplinkRTT));
+ }
+ }
+ // 本地音频 Track 质量统计
+ Map localAudioTrackStats = mClient.getLocalAudioTrackStats();
+ for (Map.Entry entry : localAudioTrackStats.entrySet()) {
+ QNLocalAudioTrackStats stats = entry.getValue();
+ mLocalAudioUplinkBitrateText.setText(String.format(getString(R.string.bitrate), stats.uplinkBitrate / 1000));
+ mLocalAudioUplinkLostRateText.setText(String.format(getString(R.string.lost_rate), stats.uplinkLostRate));
+ mLocalAudioUplinkRttText.setText(String.format(getString(R.string.rtt), stats.uplinkRTT));
+ }
+ // 远端视频 Track 质量统计
+ Map remoteVideoTrackStats = mClient.getRemoteVideoTrackStats();
+ for (Map.Entry entry : remoteVideoTrackStats.entrySet()) {
+ QNRemoteVideoTrackStats stats = entry.getValue();
+ if (stats.profile != null) {
+ mRemoteVideoProfileText.setText(stats.profile.name());
+ }
+ mRemoteVideoUplinkLostRateText.setText(String.format(getString(R.string.lost_rate), stats.uplinkLostRate));
+ mRemoteVideoUplinkRttText.setText(String.format(getString(R.string.rtt), stats.uplinkRTT));
+ mRemoteVideoDownlinkBitrateText.setText(String.format(getString(R.string.bitrate), stats.downlinkBitrate / 1000));
+ mRemoteVideoDownlinkFrameRateText.setText(String.format(getString(R.string.fps), stats.downlinkFrameRate));
+ mRemoteVideoDownlinkLostRateText.setText(String.format(getString(R.string.lost_rate), stats.downlinkLostRate));
+ }
+ // 远端音频 Track 质量统计
+ Map remoteAudioTrackStats = mClient.getRemoteAudioTrackStats();
+ for (Map.Entry entry : remoteAudioTrackStats.entrySet()) {
+ QNRemoteAudioTrackStats stats = entry.getValue();
+ mRemoteAudioUplinkLostRateText.setText(String.format(getString(R.string.lost_rate), stats.uplinkLostRate));
+ mRemoteAudioUplinkRttText.setText(String.format(getString(R.string.rtt), stats.uplinkRTT));
+ mRemoteAudioDownlinkBitrateText.setText(String.format(getString(R.string.bitrate), stats.downlinkBitrate / 1000));
+ mRemoteAudioDownlinkLostRateText.setText(String.format(getString(R.string.lost_rate), stats.downlinkLostRate));
+ }
+ // 远端用户网络质量统计
+ Map userNetworkQuality = mClient.getUserNetworkQuality();
+ for (Map.Entry entry : userNetworkQuality.entrySet()) {
+ mRemoteDownlinkNetworkQualityText.setText(entry.getValue().downlinkNetworkGrade.name());
+ mRemoteUplinkNetworkQualityText.setText(entry.getValue().uplinkNetworkGrade.name());
+ }
+ }
+ });
+ }
+ }, 0, 5000);
+ if (mClient != null) {
+ // 设置本地网络质量统计
+ mClient.setNetworkQualityListener(networkQuality -> runOnUiThread(() -> {
+ mLocalDownlinkNetworkQualityText.setText(networkQuality.downlinkNetworkGrade.name());
+ mLocalUplinkNetworkQualityText.setText(networkQuality.uplinkNetworkGrade.name());
+ }));
+ }
+ }
+
+ /**
+ * 停止通话质量统计
+ */
+ public synchronized void stopStatisticsScheduler() {
+ if (mClient != null) {
+ mClient.setNetworkQualityListener(null);
+ }
+ if (mStatsTimer != null) {
+ mStatsTimer.cancel();
+ mStatsTimer = null;
+ }
+ }
+
+ /**
+ * 远端用户离开后,重置 UI
+ */
+ private void resetRemoteStatisticsView() {
+ mRemoteDownlinkNetworkQualityText.setText(getString(R.string.none));
+ mRemoteUplinkNetworkQualityText.setText(getString(R.string.none));
+ mRemoteAudioUplinkLostRateText.setText(String.format(getString(R.string.lost_rate), 0));
+ mRemoteAudioUplinkRttText.setText(String.format(getString(R.string.rtt), 0));
+ mRemoteAudioDownlinkBitrateText.setText(String.format(getString(R.string.bitrate), 0));
+ mRemoteAudioDownlinkLostRateText.setText(String.format(getString(R.string.lost_rate), 0));
+ mRemoteVideoProfileText.setText(getString(R.string.none));
+ mRemoteVideoUplinkLostRateText.setText(String.format(getString(R.string.lost_rate), 0));
+ mRemoteVideoUplinkRttText.setText(String.format(getString(R.string.rtt), 0));
+ mRemoteVideoDownlinkBitrateText.setText(String.format(getString(R.string.bitrate), 0));
+ mRemoteVideoDownlinkFrameRateText.setText(String.format(getString(R.string.fps), 0));
+ mRemoteVideoDownlinkLostRateText.setText(String.format(getString(R.string.lost_rate), 0));
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(MediaStatisticsActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(MediaStatisticsActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(MediaStatisticsActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ // 8. 开启通话质量统计
+ startStatisticsScheduler();
+ } else if (state == QNConnectionState.DISCONNECTED) {
+ // 停止通话质量统计
+ stopStatisticsScheduler();
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(MediaStatisticsActivity.this, getString(R.string.remote_user_left_toast));
+ resetRemoteStatisticsView();
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(MediaStatisticsActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ remoteVideoTracks.get(0).play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MicrophoneOnlyActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MicrophoneOnlyActivity.java
new file mode 100644
index 0000000..468d523
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MicrophoneOnlyActivity.java
@@ -0,0 +1,327 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 麦克风纯音频通话场景
+ * 本示例仅演示本地麦克风音频 Track 的发布和远端音频的订阅场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地麦克风音频采集 Track
+ * 5. 加入房间
+ * 6. 发布本地麦克风音频 Track
+ * 7. 订阅远端音频 Track
+ * 8. 离开房间
+ * 9. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class MicrophoneOnlyActivity extends AppCompatActivity {
+ private static final String TAG = "MicrophoneOnlyActivity";
+ private QNRTCClient mClient;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNRemoteAudioTrack mRemoteAudioTrack;
+
+ private TextView mRemoteTrackTipsView;
+ private SeekBar mRemoteAudioVolumeSeekBar;
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_microphone_audio_only);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地麦克风音频采集 Track
+ initLocalTracks();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing() && mClient != null) {
+ // 8. 离开房间
+ mClient.leave();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 9. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ /**
+ * 初始化本地视图
+ */
+ private void initView() {
+ // 初始化远端音频提示视图
+ mRemoteTrackTipsView = findViewById(R.id.remote_window_tips_view);
+
+ // 初始化本地音频采集音量设置控件
+ SeekBar localAudioVolumeSeekBar = findViewById(R.id.local_audio_volume_seek_bar);
+ localAudioVolumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // 设置音频采集后的音量,该接口可用于适度对采集音量做放大或者缩小
+ // 音量值在 0.0 - 1.0f 之间为软件缩小;1.0f 为原始音量播放;大于 1.0f 且小于 10.0f 为软件放大,
+ // 在需要放大时,应从 1.x 开始设置用最小的放大值来取得合适的播放效果,过大将可能出现音频失真的现象
+ mMicrophoneAudioTrack.setVolume((double) seekBar.getProgress() / 10.0);
+ }
+ });
+
+ // 初始化远端音频播放音量设置控件
+ mRemoteAudioVolumeSeekBar = findViewById(R.id.remote_audio_volume_seek_bar);
+ mRemoteAudioVolumeSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mRemoteAudioTrack != null) {
+ // 设置远端音频播放音量,该接口可用于适度对播放音量做放大或者缩小
+ // 音量值在 0.0 - 1.0f 之间为软件缩小;1.0f 为原始音量播放;大于 1.0f 且小于 10.0f 为软件放大,
+ // 在需要放大时,应从 1.x 开始设置用最小的放大值来取得合适的播放效果,过大将可能出现音频失真的现象
+ mRemoteAudioTrack.setVolume((double) seekBar.getProgress() / 10.0);
+ }
+ }
+ });
+ mRemoteAudioVolumeSeekBar.setEnabled(false);
+ }
+
+ /**
+ * 初始化本地麦克风采集 Track
+ */
+ private void initLocalTracks() {
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(MicrophoneOnlyActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地麦克风音频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(MicrophoneOnlyActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(MicrophoneOnlyActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(MicrophoneOnlyActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音频 Track
+ for (QNRemoteTrack remoteTrack : trackList) {
+ if (remoteTrack.isAudio()) {
+ mClient.subscribe(remoteTrack);
+ }
+ }
+ } else {
+ ToastUtils.showShortToast(MicrophoneOnlyActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteAudioTrack = null;
+ mRemoteTrackTipsView.setVisibility(View.INVISIBLE);
+ mRemoteAudioVolumeSeekBar.setProgress(10);
+ mRemoteAudioVolumeSeekBar.setEnabled(false);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (remoteUserID.equals(mFirstRemoteUserID) && !remoteAudioTracks.isEmpty()) {
+ // 成功订阅远端音频 Track 后,SDK 会默认对音频 Track 进行渲染,无需其他操作
+ mRemoteAudioTrack = remoteAudioTracks.get(0);
+ mRemoteAudioVolumeSeekBar.setEnabled(true);
+ mRemoteTrackTipsView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MultiProfileActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MultiProfileActivity.java
new file mode 100644
index 0000000..a362815
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/MultiProfileActivity.java
@@ -0,0 +1,369 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.RadioGroup;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNBeautySetting;
+import com.qiniu.droid.rtc.QNCameraFacing;
+import com.qiniu.droid.rtc.QNCameraVideoTrack;
+import com.qiniu.droid.rtc.QNCameraVideoTrackConfig;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNTrackInfoChangedListener;
+import com.qiniu.droid.rtc.QNTrackProfile;
+import com.qiniu.droid.rtc.QNVideoCaptureConfigPreset;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.util.List;
+import java.util.Random;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 摄像头开启大小流 + 麦克风音视频通话场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 初始化 RTC
+ * 3. 创建 QNRTCClient 对象
+ * 4. 创建本地音视频 Track (开启大小流配置)
+ * 5. 加入房间
+ * 6. 发布本地音视频 Track
+ * 7. 订阅远端音视频 Track
+ * 8. 设置对远端视频 Track Profile 改变的监听
+ * 9. 更改预期订阅的远端视频 Track Profile
+ * 10. 离开房间
+ * 11. 反初始化 RTC 释放资源
+ *
+ * 文档参考:
+ * - 视频大小流文档指南,请参考 https://developer.qiniu.com/rtc/8772/video-size-flow-android
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class MultiProfileActivity extends AppCompatActivity {
+ private static final String TAG = "MultiProfileActivity";
+ private QNRTCClient mClient;
+ private QNSurfaceView mLocalRenderView;
+ private QNSurfaceView mRemoteRenderView;
+ private QNCameraVideoTrack mCameraVideoTrack;
+ private QNRemoteVideoTrack mRemoteVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+ private QNTrackProfile mPreferredProfile = null;
+
+ private TextView mCurrentProfileText;
+ private RadioGroup mProfileRadioGroup;
+
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_multi_profile);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 初始化 RTC
+ QNRTC.init(this, mRTCEventListener);
+ // 3. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 4. 创建本地音视频 Track
+ initLocalTracks();
+ // 5. 加入房间
+ mClient.join(Config.ROOM_TOKEN);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // 退后台会关闭采集,回到前台重新开启采集
+ mCameraVideoTrack.startCapture();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing()) {
+ if (mClient != null) {
+ // 10. 离开房间
+ mClient.leave();
+ }
+ } else {
+ // 从 Android 9 开始,设备将无法在后台访问相机,本示例不做后台采集的演示
+ // 详情可参考 https://developer.qiniu.com/rtc/kb/10074/FAQ-Android?category=kb#3
+ if (mCameraVideoTrack != null) {
+ mCameraVideoTrack.stopCapture();
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 11. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ public void onClickSetProfile(View view) {
+ if (mRemoteVideoTrack != null && mPreferredProfile != null) {
+ // 9. 更改预期订阅的远端视频 Track Profile
+ mRemoteVideoTrack.setProfile(mPreferredProfile);
+ }
+ }
+
+ private void initView() {
+ // 初始化本地预览视图
+ mLocalRenderView = findViewById(R.id.local_render_view);
+ mLocalRenderView.setZOrderOnTop(true);
+ // 初始化远端预览视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+
+ mCurrentProfileText = findViewById(R.id.current_profile);
+ mProfileRadioGroup = findViewById(R.id.set_profile_group);
+ mProfileRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ if (checkedId == R.id.low_profile) {
+ mPreferredProfile = QNTrackProfile.LOW;
+ } else if (checkedId == R.id.medium_profile) {
+ mPreferredProfile = QNTrackProfile.MEDIUM;
+ } else if (checkedId == R.id.high_profile) {
+ mPreferredProfile = QNTrackProfile.HIGH;
+ }
+ });
+ }
+
+ /**
+ * 创建音视频采集 Track
+ *
+ * 摄像头采集 Track 创建方式:
+ * 1. 创建 QNCameraVideoTrackConfig 对象,指定采集编码相关的配置
+ * 2. 通过 QNCameraVideoTrackConfig 对象创建摄像头采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(640x480, 20fps, 800kbps)
+ *
+ * 麦克风采集 Track 创建方式:
+ * 1. 创建 QNMicrophoneAudioTrackConfig 对象指定音频参数,亦可使用 SDK 的预设值,预设值可见 {@link QNAudioQualityPreset}
+ * 2. 通过 QNMicrophoneAudioTrackConfig 对象创建麦克风采集 Track,若使用无参的方法创建则会使用 SDK 默认配置参数(16kHz, 单声道, 24kbps)
+ */
+ private void initLocalTracks() {
+ // 创建摄像头采集 Track
+ QNCameraVideoTrackConfig cameraVideoTrackConfig = new QNCameraVideoTrackConfig(Config.TAG_CAMERA_TRACK)
+ .setVideoCaptureConfig(QNVideoCaptureConfigPreset.CAPTURE_1280x720) // 设置采集参数
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_WIDTH, Config.DEFAULT_HEIGHT, Config.DEFAULT_FPS, Config.DEFAULT_VIDEO_BITRATE)) // 设置编码参数
+ .setCameraFacing(QNCameraFacing.FRONT) // 设置摄像头方向
+ .setMultiProfileEnabled(true); // 开启本地发布大小流
+ mCameraVideoTrack = QNRTC.createCameraVideoTrack(cameraVideoTrackConfig);
+ // 设置本地预览视图
+ mCameraVideoTrack.play(mLocalRenderView);
+ // 初始化并配置美颜
+ mCameraVideoTrack.setBeauty(new QNBeautySetting(0.5f, 0.5f, 0.5f));
+
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+ }
+
+ private void setProfileChangeEnabled(boolean enabled) {
+ for (int i = 0; i < mProfileRadioGroup.getChildCount(); i++) {
+ mProfileRadioGroup.getChildAt(i).setEnabled(enabled);
+ }
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ ToastUtils.showShortToast(MultiProfileActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 6. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(MultiProfileActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(MultiProfileActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mCameraVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(MultiProfileActivity.this, getString(R.string.remote_user_left_toast));
+ mCurrentProfileText.setText(getString(R.string.none));
+ setProfileChangeEnabled(false);
+ mPreferredProfile = null;
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 7. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(MultiProfileActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ mRemoteVideoTrack = remoteVideoTracks.get(0);
+ mRemoteVideoTrack.play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ setProfileChangeEnabled(mRemoteVideoTrack.isMultiProfileEnabled());
+ // 8. 设置对远端视频 Track Profile 改变的监听
+ mRemoteVideoTrack.setTrackInfoChangedListener(new QNTrackInfoChangedListener() {
+ @Override
+ public void onVideoProfileChanged(QNTrackProfile profile) {
+ mCurrentProfileText.setText(profile.name());
+ ToastUtils.showShortToast(MultiProfileActivity.this, String.format(getString(R.string.profile_changed_toast), profile.name()));
+ }
+ });
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/ScreenCaptureActivity.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/ScreenCaptureActivity.java
new file mode 100644
index 0000000..1652d64
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/activity/ScreenCaptureActivity.java
@@ -0,0 +1,354 @@
+package com.qiniu.droid.rtc.api.examples.activity;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.qiniu.droid.rtc.QNAudioQualityPreset;
+import com.qiniu.droid.rtc.QNClientEventListener;
+import com.qiniu.droid.rtc.QNConnectionDisconnectedInfo;
+import com.qiniu.droid.rtc.QNConnectionState;
+import com.qiniu.droid.rtc.QNCustomMessage;
+import com.qiniu.droid.rtc.QNMediaRelayState;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrack;
+import com.qiniu.droid.rtc.QNMicrophoneAudioTrackConfig;
+import com.qiniu.droid.rtc.QNPublishResultCallback;
+import com.qiniu.droid.rtc.QNRTC;
+import com.qiniu.droid.rtc.QNRTCClient;
+import com.qiniu.droid.rtc.QNRTCEventListener;
+import com.qiniu.droid.rtc.QNRTCSetting;
+import com.qiniu.droid.rtc.QNRemoteAudioTrack;
+import com.qiniu.droid.rtc.QNRemoteTrack;
+import com.qiniu.droid.rtc.QNRemoteVideoTrack;
+import com.qiniu.droid.rtc.QNScreenVideoTrack;
+import com.qiniu.droid.rtc.QNScreenVideoTrackConfig;
+import com.qiniu.droid.rtc.QNSourceType;
+import com.qiniu.droid.rtc.QNSurfaceView;
+import com.qiniu.droid.rtc.QNVideoEncoderConfig;
+import com.qiniu.droid.rtc.api.examples.R;
+import com.qiniu.droid.rtc.api.examples.service.ForegroundService;
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+import com.qiniu.droid.rtc.api.examples.utils.ToastUtils;
+import com.qiniu.droid.rtc.model.QNAudioDevice;
+
+import java.util.Collections;
+import java.util.List;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * 1v1 屏幕录制音视频通话场景
+ *
+ * 主要步骤如下:
+ * 1. 初始化视图
+ * 2. 确认设备是否支持屏幕录制
+ * 3. 初始化 RTC
+ * 4. 创建 QNRTCClient 对象
+ * 5. 获取屏幕录制权限
+ * 6. 创建本地音视频 Track
+ * 7. 加入房间
+ * 8. 发布本地音视频 Track
+ * 9. 订阅远端音视频 Track
+ * 10. 离开房间
+ * 11. 反初始化 RTC 释放资源
+ *
+ * 注意:Android Q 之后,屏幕录制必须要在前台服务中进行,详细的实现方式可参考本示例
+ *
+ * 文档参考:
+ * - 屏幕录制实现步骤,请参考 https://developer.qiniu.com/rtc/8767/audio-and-video-collection-android#2
+ * - 音视频通话中的基本概念,请参考 https://developer.qiniu.com/rtc/9909/the-rtc-basic-concept
+ * - 接口文档,请参考 https://developer.qiniu.com/rtc/8773/API%20%E6%A6%82%E8%A7%88
+ */
+public class ScreenCaptureActivity extends AppCompatActivity {
+ private static final String TAG = "ScreenCaptureActivity";
+
+ private QNRTCClient mClient;
+ private QNScreenVideoTrack mScreenVideoTrack;
+ private QNMicrophoneAudioTrack mMicrophoneAudioTrack;
+
+ private QNSurfaceView mRemoteRenderView;
+ private String mFirstRemoteUserID = null;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.activity_screen_capture);
+
+ // 1. 初始化视图
+ initView();
+ // 2. 确认设备是否支持屏幕录制
+ if (!QNScreenVideoTrack.isScreenCaptureSupported()) {
+ ToastUtils.showShortToast(this, "当前设备不支持屏幕录制");
+ finish();
+ }
+ // 3. 初始化 RTC
+ QNRTCSetting setting = new QNRTCSetting()
+ .setMaintainResolution(true) // 设置开启固定分辨率
+ .setHWCodecEnabled(false); // 为保证编码质量,开启软编
+ QNRTC.init(this, setting, mRTCEventListener);
+ // 4. 创建 QNRTCClient 对象
+ mClient = QNRTC.createClient(mClientEventListener);
+ // 本示例仅针对 1v1 连麦场景,因此,关闭自动订阅选项。关于自动订阅的配置,可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android#3
+ mClient.setAutoSubscribe(false);
+ // 5. 获取屏幕录制权限
+ QNScreenVideoTrack.requestPermission(this);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (isFinishing() && mClient != null) {
+ // 10. 离开房间
+ mClient.leave();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ // 11. 反初始化 RTC 释放资源
+ QNRTC.deinit();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == QNScreenVideoTrack.SCREEN_CAPTURE_PERMISSION_REQUEST_CODE) {
+ if (QNScreenVideoTrack.checkActivityResult(requestCode, resultCode, data)) {
+ // 6. 创建本地音视频 Track
+ initLocalTracks();
+ // 7. 加入房间,Android Q 之后屏幕录制需要 foreground service,因此可等待 service 回调后再加入房间
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ mClient.join(Config.ROOM_TOKEN);
+ }
+ } else {
+ ToastUtils.showShortToast(this, "用户未授予录屏权限");
+ finish();
+ }
+ }
+ }
+
+ private void initView() {
+ // 初始化远端视频渲染视图
+ mRemoteRenderView = findViewById(R.id.remote_render_view);
+ mRemoteRenderView.setZOrderOnTop(true);
+ }
+
+ private void initLocalTracks() {
+ // 创建麦克风采集 Track
+ QNMicrophoneAudioTrackConfig microphoneAudioTrackConfig = new QNMicrophoneAudioTrackConfig(Config.TAG_MICROPHONE_TRACK)
+ .setAudioQuality(QNAudioQualityPreset.HIGH_STEREO) // 设置音频参数
+ .setCommunicationModeOn(true); // 设置是否开启通话模式,开启后会启用硬件回声消除等处理
+ mMicrophoneAudioTrack = QNRTC.createMicrophoneAudioTrack(microphoneAudioTrackConfig);
+
+ // 创建屏幕录制采集 Track
+ createScreenTrack();
+ }
+
+ // 处理 Build.VERSION_CODES.Q 及以上的兼容问题
+ private void createScreenTrack() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ Intent intent = new Intent(this, ForegroundService.class);
+ startForegroundService(intent);
+ bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ Log.i(TAG, "start service for Q");
+ } else {
+ // 创建屏幕录制采集 Track
+ // 编码分辨率约贴近屏幕分辨率,码率越大,画面越清晰,但是考虑到编码性能,需根据您的场景选择特定的编码分辨率
+ QNScreenVideoTrackConfig screenVideoTrackConfig = new QNScreenVideoTrackConfig(Config.TAG_SCREEN_TRACK)
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_SCREEN_VIDEO_TRACK_WIDTH, Config.DEFAULT_SCREEN_VIDEO_TRACK_HEIGHT,
+ Config.DEFAULT_FPS, Config.DEFAULT_SCREEN_VIDEO_TRACK_BITRATE));
+ mScreenVideoTrack = QNRTC.createScreenVideoTrack(screenVideoTrackConfig);
+ }
+ }
+
+ private final QNRTCEventListener mRTCEventListener = new QNRTCEventListener() {
+ /**
+ * 当音频路由发生变化时会回调此方法
+ *
+ * @param device 音频设备, 详情请参考{@link QNAudioDevice}
+ */
+ @Override
+ public void onAudioRouteChanged(QNAudioDevice device) {
+
+ }
+ };
+
+ private final QNClientEventListener mClientEventListener = new QNClientEventListener() {
+ /**
+ * 连接状态改变时会回调此方法
+ * 连接状态回调只需要做提示用户,或者更新相关 UI; 不需要再做加入房间或者重新发布等其他操作!
+ * @param state 连接状态,可参考 {@link QNConnectionState}
+ */
+ @Override
+ public void onConnectionStateChanged(QNConnectionState state, @Nullable QNConnectionDisconnectedInfo info) {
+ Log.i(TAG, "onConnectionStateChanged : " + state.name());
+ ToastUtils.showShortToast(ScreenCaptureActivity.this,
+ String.format(getString(R.string.connection_state_changed), state.name()));
+ if (state == QNConnectionState.CONNECTED) {
+ // 8. 发布本地音视频 Track
+ // 发布订阅场景注意事项可参考 https://developer.qiniu.com/rtc/8769/publish-and-subscribe-android
+ mClient.publish(new QNPublishResultCallback() {
+ @Override
+ public void onPublished() { // 发布成功
+ ToastUtils.showShortToast(ScreenCaptureActivity.this,
+ getString(R.string.publish_success));
+ }
+
+ @Override
+ public void onError(int errorCode, String errorMessage) { // 发布失败
+ ToastUtils.showLongToast(ScreenCaptureActivity.this,
+ String.format(getString(R.string.publish_failed), errorCode, errorMessage));
+ }
+ }, mScreenVideoTrack, mMicrophoneAudioTrack);
+ }
+ }
+
+ /**
+ * 远端用户加入房间时会回调此方法
+ * @see QNRTCClient#join(String, String) 可指定 userData 字段
+ *
+ * @param remoteUserID 远端用户的 userID
+ * @param userData 透传字段,用户自定义内容
+ */
+ @Override
+ public void onUserJoined(String remoteUserID, String userData) {
+
+ }
+
+ /**
+ * 远端用户重连时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnecting(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户重连成功时会回调此方法
+ *
+ * @param remoteUserID 远端用户的 userID
+ */
+ @Override
+ public void onUserReconnected(String remoteUserID) {
+
+ }
+
+ /**
+ * 远端用户离开房间时会回调此方法
+ *
+ * @param remoteUserID 远端离开用户的 userID
+ */
+ @Override
+ public void onUserLeft(String remoteUserID) {
+ ToastUtils.showShortToast(ScreenCaptureActivity.this, getString(R.string.remote_user_left_toast));
+ }
+
+ /**
+ * 远端用户成功发布 tracks 时会回调此方法
+ *
+ * 手动订阅场景下,可以在该回调中选择待订阅的 Track,并通过 {@link QNRTCClient#subscribe(QNRemoteTrack...)}
+ * 接口进行订阅的操作。
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户发布的 tracks 列表
+ */
+ @Override
+ public void onUserPublished(String remoteUserID, List trackList) {
+ if (mFirstRemoteUserID == null || remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = remoteUserID;
+ // 9. 手动订阅远端音视频 Track
+ mClient.subscribe(trackList);
+ } else {
+ ToastUtils.showShortToast(ScreenCaptureActivity.this, getString(R.string.toast_other_user_published));
+ }
+ }
+
+ /**
+ * 远端用户成功取消发布 tracks 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param trackList 远端用户取消发布的 tracks 列表
+ */
+ @Override
+ public void onUserUnpublished(String remoteUserID, List trackList) {
+ if (remoteUserID.equals(mFirstRemoteUserID)) {
+ mFirstRemoteUserID = null;
+ mRemoteRenderView.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * 成功订阅远端用户 Track 时会回调此方法
+ *
+ * @param remoteUserID 远端用户 userID
+ * @param remoteAudioTracks 订阅的远端用户音频 tracks 列表
+ * @param remoteVideoTracks 订阅的远端用户视频 tracks 列表
+ */
+ @Override
+ public void onSubscribed(String remoteUserID, List remoteAudioTracks, List remoteVideoTracks) {
+ if (!remoteVideoTracks.isEmpty()) {
+ // 成功订阅远端音视频 Track 后,对视频 Track 进行渲染,由于本示例仅实现一对一的连麦,因此,直接渲染即可
+ remoteVideoTracks.get(0).play(mRemoteRenderView);
+ mRemoteRenderView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * 当收到自定义消息时回调此方法
+ *
+ * @param message 自定义信息,详情请参考 {@link QNCustomMessage}
+ */
+ @Override
+ public void onMessageReceived(QNCustomMessage message) {
+
+ }
+
+ /**
+ * 跨房媒体转发状态改变时会回调此方法
+ *
+ * @param relayRoom 媒体转发的房间名
+ * @param state 媒体转发的状态
+ */
+ @Override
+ public void onMediaRelayStateChanged(String relayRoom, QNMediaRelayState state) {
+
+ }
+ };
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ // 创建屏幕录制采集 Track
+ // 编码分辨率约贴近屏幕分辨率,码率越大,画面越清晰,但是考虑到编码性能,需根据您的场景选择特定的编码分辨率
+ QNScreenVideoTrackConfig screenVideoTrackConfig = new QNScreenVideoTrackConfig(Config.TAG_SCREEN_TRACK)
+ .setVideoEncoderConfig(new QNVideoEncoderConfig(
+ Config.DEFAULT_SCREEN_VIDEO_TRACK_WIDTH, Config.DEFAULT_SCREEN_VIDEO_TRACK_HEIGHT,
+ Config.DEFAULT_FPS, Config.DEFAULT_SCREEN_VIDEO_TRACK_BITRATE));
+ mScreenVideoTrack = QNRTC.createScreenVideoTrack(screenVideoTrackConfig);
+
+ // 7. 加入房间,Android Q 之后屏幕录制需要 foreground service,因此可等待 service 回调后再加入房间
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ mClient.join(Config.ROOM_TOKEN);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+
+ }
+ };
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/capture/ExtAudioCapture.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/capture/ExtAudioCapture.java
new file mode 100644
index 0000000..7b7dc95
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/capture/ExtAudioCapture.java
@@ -0,0 +1,123 @@
+package com.qiniu.droid.rtc.api.examples.capture;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.content.pm.PackageManager;
+import android.media.AudioFormat;
+import android.media.AudioRecord;
+import android.media.MediaRecorder;
+import android.util.Log;
+
+import androidx.core.app.ActivityCompat;
+
+public final class ExtAudioCapture {
+ private static final String TAG = "ExtAudioCapture";
+
+ public static final int DEFAULT_SOURCE = MediaRecorder.AudioSource.MIC;
+ public static final int DEFAULT_SAMPLE_RATE = 44100;
+ public static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
+ public static final int DEFAULT_DATA_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
+
+ // Make sure the sample size is the same in different devices
+ private static final int SAMPLES_PER_FRAME = 1024;
+
+ private AudioRecord mAudioRecord;
+
+ private Thread mCaptureThread;
+ private boolean mIsCaptureStarted = false;
+ private volatile boolean mIsLoopExit = false;
+ private byte[] mAudioSrcBuffer = new byte[SAMPLES_PER_FRAME * 2];
+
+ private OnAudioFrameCapturedListener mOnAudioFrameCapturedListener;
+
+ public interface OnAudioFrameCapturedListener {
+ void onAudioFrameCaptured(byte[] audioData);
+ }
+
+ public void setOnAudioFrameCapturedListener(OnAudioFrameCapturedListener listener) {
+ mOnAudioFrameCapturedListener = listener;
+ }
+
+ public boolean isCaptureStarted() {
+ return mIsCaptureStarted;
+ }
+
+ public boolean startCapture() {
+ return startCapture(DEFAULT_SOURCE, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG, DEFAULT_DATA_FORMAT);
+ }
+
+ @SuppressLint("MissingPermission")
+ public boolean startCapture(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat) {
+ if (mIsCaptureStarted) {
+ Log.e(TAG, "Capture already started !");
+ return false;
+ }
+
+ int minBufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
+ if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
+ Log.e(TAG, "Invalid parameter !");
+ return false;
+ }
+
+ mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, minBufferSize * 4);
+ if (mAudioRecord.getState() == AudioRecord.STATE_UNINITIALIZED) {
+ Log.e(TAG, "AudioRecord initialize fail !");
+ return false;
+ }
+
+ mAudioRecord.startRecording();
+
+ mIsLoopExit = false;
+ mCaptureThread = new Thread(new AudioCaptureRunnable());
+ mCaptureThread.start();
+
+ mIsCaptureStarted = true;
+
+ Log.d(TAG, "Start audio capture success !");
+
+ return true;
+ }
+
+ public void stopCapture() {
+ if (!mIsCaptureStarted) {
+ return;
+ }
+
+ mIsLoopExit = true;
+ try {
+ mCaptureThread.interrupt();
+ mCaptureThread.join(1000);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ if (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
+ mAudioRecord.stop();
+ }
+
+ mAudioRecord.release();
+
+ mIsCaptureStarted = false;
+ mOnAudioFrameCapturedListener = null;
+
+ Log.d(TAG, "Stop audio capture success !");
+ }
+
+ private class AudioCaptureRunnable implements Runnable {
+ @Override
+ public void run() {
+ while (!mIsLoopExit) {
+ int ret = mAudioRecord.read(mAudioSrcBuffer, 0, mAudioSrcBuffer.length);
+ if (ret == AudioRecord.ERROR_INVALID_OPERATION) {
+ Log.e(TAG, "Error ERROR_INVALID_OPERATION");
+ } else if (ret == AudioRecord.ERROR_BAD_VALUE) {
+ Log.e(TAG, "Error ERROR_BAD_VALUE");
+ } else {
+ if (mOnAudioFrameCapturedListener != null) {
+ mOnAudioFrameCapturedListener.onAudioFrameCaptured(mAudioSrcBuffer);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/capture/ExtVideoCapture.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/capture/ExtVideoCapture.java
new file mode 100644
index 0000000..90d6e3e
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/capture/ExtVideoCapture.java
@@ -0,0 +1,197 @@
+package com.qiniu.droid.rtc.api.examples.capture;
+
+import android.content.Context;
+import android.graphics.ImageFormat;
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.WindowManager;
+
+import com.qiniu.droid.rtc.api.examples.utils.Config;
+
+import java.io.IOException;
+import java.util.List;
+
+public final class ExtVideoCapture implements SurfaceHolder.Callback, Camera.PreviewCallback {
+ private static final String TAG = "ExtVideoCapture";
+
+ public interface OnPreviewFrameCallback {
+ void onPreviewFrameCaptured(byte[] data, int width, int height, int orientation, boolean mirror, long tsInNanoTime);
+ }
+
+ public static final int MAX_CALLBACK_BUFFER_NUM = 2;
+
+ private Camera mCamera;
+ private int mCurrentFacingId = Camera.CameraInfo.CAMERA_FACING_BACK;
+
+ private OnPreviewFrameCallback mOnPreviewFrameCallback;
+
+ private int mPreviewWidth = 0;
+ private int mPreviewHeight = 0;
+
+ private Context mContext;
+ private int mCameraPreviewDegree;
+
+ public ExtVideoCapture(SurfaceView sv) {
+ sv.getHolder().addCallback(this);
+ mContext = sv.getContext();
+ }
+
+ public void setOnPreviewFrameCallback(OnPreviewFrameCallback callback) {
+ mOnPreviewFrameCallback = callback;
+ }
+
+ public void switchCamera() {
+ stopPreviewAndFreeCamera();
+
+ if (mCurrentFacingId == Camera.CameraInfo.CAMERA_FACING_BACK) {
+ mCurrentFacingId = Camera.CameraInfo.CAMERA_FACING_FRONT;
+ } else {
+ mCurrentFacingId = Camera.CameraInfo.CAMERA_FACING_BACK;
+ }
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ if (safeCameraOpen(mCurrentFacingId)) {
+ try {
+ mCamera.setPreviewDisplay(holder);
+
+ int degree = getDeviceRotationDegree(mContext);
+ Camera.CameraInfo camInfo = new Camera.CameraInfo();
+ Camera.getCameraInfo(mCurrentFacingId, camInfo);
+ int orientation;
+ if (mCurrentFacingId == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ orientation = (camInfo.orientation + degree) % 360;
+ mCameraPreviewDegree = orientation;
+ orientation = (360 - orientation) % 360; // compensate the mirror
+ } else { // back-facing
+ orientation = (camInfo.orientation - degree + 360) % 360;
+ mCameraPreviewDegree = orientation;
+ }
+ mCamera.setDisplayOrientation(orientation);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ Camera.Parameters params = mCamera.getParameters();
+ Log.i(TAG, "current preview size : " + params.getPreviewSize().width + ", " + params.getPreviewSize().height);
+ List supportedSize = params.getSupportedPreviewSizes();
+ for(Camera.Size size : supportedSize) {
+ if (size.width == Config.DEFAULT_WIDTH && size.height == Config.DEFAULT_HEIGHT) {
+ Log.i(TAG, "Choose expected preview size");
+ params.setPreviewSize(size.width, size.height);
+ break;
+ }
+ }
+ params.setPreviewFormat(ImageFormat.NV21);
+ mCamera.setParameters(params);
+
+ final Camera.Size previewSize = params.getPreviewSize();
+ Log.i(TAG, "final preview size : " + previewSize.width + ", " + previewSize.height);
+ final int bitsPerPixel = ImageFormat.getBitsPerPixel(params.getPreviewFormat());
+ final int previewBufferSize = (previewSize.width * previewSize.height * bitsPerPixel) / 8;
+ for (int i = 0; i < MAX_CALLBACK_BUFFER_NUM; i++) {
+ mCamera.addCallbackBuffer(new byte[previewBufferSize]);
+ }
+
+ mPreviewWidth = previewSize.width;
+ mPreviewHeight = previewSize.height;
+
+ mCamera.setPreviewCallbackWithBuffer(this);
+ mCamera.startPreview();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ stopPreviewAndFreeCamera();
+ }
+
+ private boolean safeCameraOpen(int id) {
+ boolean qOpened = false;
+
+ try {
+ releaseCameraAndPreview();
+ mCamera = Camera.open(id);
+ qOpened = (mCamera != null);
+ } catch (Exception e) {
+ Log.e(TAG, "failed to open Camera");
+ e.printStackTrace();
+ }
+
+ if (qOpened) {
+ mCurrentFacingId = id;
+ }
+
+ return qOpened;
+ }
+
+ private void releaseCameraAndPreview() {
+ if (mCamera != null) {
+ mCamera.release();
+ mCamera = null;
+ }
+ }
+
+ /**
+ * When this function returns, mCamera will be null.
+ */
+ private void stopPreviewAndFreeCamera() {
+ if (mCamera != null) {
+ // Call stopPreview() to stop updating the preview surface.
+ mCamera.stopPreview();
+
+ // Important: Call release() to release the camera for use by other
+ // applications. Applications should release the camera immediately
+ // during onPause() and re-open() it during onResume()).
+ mCamera.release();
+
+ mCamera = null;
+ }
+ }
+
+ @Override
+ public void onPreviewFrame(byte[] data, Camera camera) {
+ if (camera == null || data == null) {
+ return;
+ }
+
+ if (mOnPreviewFrameCallback != null) {
+ mOnPreviewFrameCallback.onPreviewFrameCaptured(data, mPreviewWidth, mPreviewHeight, mCameraPreviewDegree, false, System.nanoTime());
+ }
+
+ if (mCamera != null) {
+ mCamera.addCallbackBuffer(data);
+ }
+ }
+
+ private static int getDisplayDefaultRotation(Context ctx) {
+ WindowManager windowManager = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
+ return windowManager.getDefaultDisplay().getRotation();
+ }
+
+ private static int getDeviceRotationDegree(Context ctx) {
+ switch (getDisplayDefaultRotation(ctx)) {
+ // normal portrait
+ case Surface.ROTATION_0:
+ return 0;
+ // expected landscape
+ case Surface.ROTATION_90:
+ return 90;
+ // upside down portrait
+ case Surface.ROTATION_180:
+ return 180;
+ // "upside down" landscape
+ case Surface.ROTATION_270:
+ return 270;
+ }
+ return 0;
+ }
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/service/ForegroundService.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/service/ForegroundService.java
new file mode 100644
index 0000000..e8b9a4a
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/service/ForegroundService.java
@@ -0,0 +1,62 @@
+package com.qiniu.droid.rtc.api.examples.service;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+public class ForegroundService extends Service {
+
+ private static final String TAG = "ForegroundService";
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ ForegroundService getService() {
+ return ForegroundService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ String CHANNEL_ID = "screen share";
+ NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
+ "screen share",
+ NotificationManager.IMPORTANCE_DEFAULT);
+
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
+ Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("")
+ .setContentText("").build();
+ startForeground(1, notification);
+ Log.i(TAG, "start foreground");
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ stopForeground(true);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(final Intent intent, int flags, int startId) {
+ return super.onStartCommand(intent, flags, startId);
+ }
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/Config.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/Config.java
new file mode 100644
index 0000000..e87c5a6
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/Config.java
@@ -0,0 +1,28 @@
+package com.qiniu.droid.rtc.api.examples.utils;
+
+public class Config {
+ // 输入您的 RoomToken,生成方式可参考 https://developer.qiniu.com/rtc/9858/applist#4
+ public static final String ROOM_TOKEN = "";
+ // CDN 转推场景下需要配置推流的 rtmp 地址,获取方式可参考 https://developer.qiniu.com/pili/1221/the-console-quick-start
+ public static final String PUBLISH_URL = "自定义转推 rtmp 地址";
+
+ public static final String KEY_ROOM_NAME = "roomName";
+ public static final String KEY_USER_ID = "userId";
+
+ public static final String TAG_CAMERA_TRACK = "camera";
+ public static final String TAG_MICROPHONE_TRACK = "microphone";
+ public static final String TAG_SCREEN_TRACK = "screen";
+ public static final String TAG_CUSTOM_VIDEO_TRACK = "custom_video";
+ public static final String TAG_CUSTOM_AUDIO_TRACK = "custom_audio";
+
+ public static final int DEFAULT_WIDTH = 1280;
+ public static final int DEFAULT_HEIGHT = 720;
+ public static final int DEFAULT_FPS = 24;
+ public static final int DEFAULT_VIDEO_BITRATE = 1600;
+ public static final int DEFAULT_AUDIO_SAMPLE_RATE = 44100;
+ public static final int DEFAULT_AUDIO_CHANNEL_COUNT = 1;
+ public static final int DEFAULT_AUDIO_BITRATE = 64;
+ public static final int DEFAULT_SCREEN_VIDEO_TRACK_WIDTH = 1080;
+ public static final int DEFAULT_SCREEN_VIDEO_TRACK_HEIGHT = 1920;
+ public static final int DEFAULT_SCREEN_VIDEO_TRACK_BITRATE = 3000;
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/PermissionChecker.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/PermissionChecker.java
new file mode 100644
index 0000000..c8e8821
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/PermissionChecker.java
@@ -0,0 +1,119 @@
+package com.qiniu.droid.rtc.api.examples.utils;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionChecker {
+ private static final int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
+
+ private Activity mActivity;
+
+ public PermissionChecker(Activity activity) {
+ mActivity = activity;
+ }
+
+ /**
+ * Check that all given permissions have been granted by verifying that each entry in the
+ * given array is of the value {@link PackageManager#PERMISSION_GRANTED}.
+ *
+ * @see Activity#onRequestPermissionsResult(int, String[], int[])
+ */
+ private boolean verifyPermissions(int[] grantResults) {
+ // At least one result must be checked.
+ if (grantResults.length < 1){
+ return false;
+ }
+
+ // Verify that each required permission has been granted, otherwise return false.
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ public boolean checkPermission() {
+ boolean ret = true;
+
+ List permissionsNeeded = new ArrayList();
+ final List permissionsList = new ArrayList();
+ if (!addPermission(permissionsList, Manifest.permission.CAMERA)) {
+ permissionsNeeded.add("CAMERA");
+ }
+ if (!addPermission(permissionsList, Manifest.permission.RECORD_AUDIO)) {
+ permissionsNeeded.add("MICROPHONE");
+ }
+ if (!addPermission(permissionsList, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+ permissionsNeeded.add("Write external storage");
+
+ } if (!addPermission(permissionsList, Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ permissionsNeeded.add("read external storage");
+ }
+
+ if (permissionsNeeded.size() > 0) {
+ // Need Rationale
+ String message = "You need to grant access to " + permissionsNeeded.get(0);
+ for (int i = 1; i < permissionsNeeded.size(); i++) {
+ message = message + ", " + permissionsNeeded.get(i);
+ }
+ // Check for Rationale Option
+ if (!mActivity.shouldShowRequestPermissionRationale(permissionsList.get(0))) {
+ showMessageOKCancel(message,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mActivity.requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
+ REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
+ }
+ });
+ }
+ else {
+ mActivity.requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
+ REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
+ }
+ ret = false;
+ }
+
+ return ret;
+ }
+
+ private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
+ new AlertDialog.Builder(mActivity)
+ .setMessage(message)
+ .setPositiveButton("OK", okListener)
+ .setNegativeButton("Cancel", null)
+ .create()
+ .show();
+ }
+
+ @TargetApi(Build.VERSION_CODES.M)
+ private boolean addPermission(List permissionsList, String permission) {
+ boolean ret = true;
+ if (mActivity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+ permissionsList.add(permission);
+ ret = false;
+ }
+ return ret;
+ }
+
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (requestCode == REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS) {
+ if (verifyPermissions(grantResults)) {
+ // all permissions granted
+ } else {
+ // some permissions denied
+ //ToastUtils.s(mActivity, "some permissions denied");
+ }
+ }
+ }
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/ToastUtils.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/ToastUtils.java
new file mode 100644
index 0000000..b0fee29
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/ToastUtils.java
@@ -0,0 +1,31 @@
+package com.qiniu.droid.rtc.api.examples.utils;
+
+import android.content.Context;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.qiniu.droid.rtc.api.examples.R;
+
+public class ToastUtils {
+ public static void showShortToast(Context context, String content) {
+ showToast(context, content, Toast.LENGTH_SHORT);
+ }
+
+ public static void showLongToast(Context context, String content) {
+ showToast(context, content, Toast.LENGTH_LONG);
+ }
+
+ private static void showToast(Context context, String content, int duration) {
+ Toast toast = new Toast(context.getApplicationContext());
+ View view = LayoutInflater.from(context.getApplicationContext()).inflate(R.layout.toast_message, null);
+ TextView contentView = view.findViewById(R.id.toast_content);
+ contentView.setText(content);
+ toast.setView(view);
+ toast.setDuration(duration);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ }
+}
diff --git a/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/Utils.java b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/Utils.java
new file mode 100644
index 0000000..2ceff30
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/java/com/qiniu/droid/rtc/api/examples/utils/Utils.java
@@ -0,0 +1,133 @@
+package com.qiniu.droid.rtc.api.examples.utils;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Base64;
+import android.view.View;
+
+import com.qiniu.android.dns.DnsManager;
+import com.qiniu.android.dns.IResolver;
+import com.qiniu.android.dns.NetworkInfo;
+import com.qiniu.android.dns.http.DnspodFree;
+import com.qiniu.android.dns.local.AndroidDnsServer;
+import com.qiniu.android.dns.local.Resolver;
+import com.qiniu.droid.rtc.QNErrorCode;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.InetAddress;
+
+public final class Utils {
+
+ public static String packageName(Context context) {
+ PackageInfo info;
+ try {
+ info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ return info.packageName;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ public static int appVersion(Context context) {
+ PackageInfo info;
+ try {
+ info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ return info.versionCode;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return 0;
+ }
+
+ public static void showAlertDialog(Context context, String message) {
+ new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setCancelable(false)
+ .setPositiveButton("确定", (dialog, which) -> dialog.dismiss())
+ .create()
+ .show();
+ }
+
+ public static String base64Decode(String msg) {
+ try {
+ return new String(Base64.decode(msg.getBytes(), Base64.DEFAULT));
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ public static JSONObject parseRoomToken(String roomToken) {
+ if (roomToken == null) {
+ return null;
+ }
+ String[] tokens = roomToken.split(":");
+ if (tokens.length != 3) {
+ return null;
+ }
+ String roomInfo = Utils.base64Decode(tokens[2]);
+ if (roomInfo == null) {
+ return null;
+ }
+ try {
+ return new JSONObject(roomInfo);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ @TargetApi(19)
+ public static int getSystemUiVisibility() {
+ int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+ }
+ return flags;
+ }
+
+ /**
+ * 创建自定义 dns manager
+ *
+ * 注意:该方法需要在子线程中调用,否则可能会抛异常
+ *
+ * @param context 上下文
+ * @return dns manager
+ */
+ public static DnsManager getDefaultDnsManager(Context context) {
+ IResolver r0 = null;
+ try {
+ // 默认使用阿里云公共 DNS 服务,避免系统 DNS 解析可能出现的跨运营商、重定向等问题,详情可参考 https://www.alidns.com/
+ // 超时时间参数可选,不指定默认为 10s 的超时
+ // 超时时间单位:s
+ r0 = new Resolver(InetAddress.getByName("223.5.5.5"), 3);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // 默认 Dnspod 服务,使用腾讯公共 DNS 服务,详情可参考 https://www.dnspod.cn/Products/Public.DNS
+ // 超时时间参数可选,不指定默认为 10s 的超时
+ // 超时时间单位:s
+ IResolver r1 = new DnspodFree("119.29.29.29", 3);
+ // 系统默认 DNS 解析,可能会出现解析跨运营商等问题
+ IResolver r2 = AndroidDnsServer.defaultResolver(context);
+ return new DnsManager(NetworkInfo.normal, new IResolver[]{r0, r1, r2});
+ }
+
+ public static String getVersion(Context context) {
+ PackageManager packageManager = context.getPackageManager();
+ try {
+ PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ return packageInfo.versionName;
+
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+}
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_amix.so b/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_amix.so
new file mode 100755
index 0000000..7d3bc84
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_amix.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_beauty.so b/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_beauty.so
new file mode 100755
index 0000000..565b072
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_beauty.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_rtc.so b/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_rtc.so
new file mode 100755
index 0000000..d4e4f2b
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/arm64-v8a/libqndroid_rtc.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_amix.so b/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_amix.so
new file mode 100755
index 0000000..a75f5e1
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_amix.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_beauty.so b/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_beauty.so
new file mode 100755
index 0000000..4ef4e89
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_beauty.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_rtc.so b/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_rtc.so
new file mode 100755
index 0000000..e8dbee8
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/armeabi-v7a/libqndroid_rtc.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_amix.so b/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_amix.so
new file mode 100755
index 0000000..432aaa5
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_amix.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_beauty.so b/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_beauty.so
new file mode 100755
index 0000000..4d9b5e6
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_beauty.so differ
diff --git a/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_rtc.so b/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_rtc.so
new file mode 100755
index 0000000..6940e1f
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/jniLibs/x86/libqndroid_rtc.so differ
diff --git a/QNRTC-API-Examples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/QNRTC-API-Examples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/drawable/blue_round_rect_background.xml b/QNRTC-API-Examples/app/src/main/res/drawable/blue_round_rect_background.xml
new file mode 100644
index 0000000..dfa8bea
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/drawable/blue_round_rect_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/drawable/grey_round_rect_background.xml b/QNRTC-API-Examples/app/src/main/res/drawable/grey_round_rect_background.xml
new file mode 100644
index 0000000..0d0dbd1
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/drawable/grey_round_rect_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/drawable/ic_launcher_background.xml b/QNRTC-API-Examples/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/QNRTC-API-Examples/app/src/main/res/drawable/screen_capture.gif b/QNRTC-API-Examples/app/src/main/res/drawable/screen_capture.gif
new file mode 100644
index 0000000..0b120cf
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/drawable/screen_capture.gif differ
diff --git a/QNRTC-API-Examples/app/src/main/res/drawable/white_round_rect_background.xml b/QNRTC-API-Examples/app/src/main/res/drawable/white_round_rect_background.xml
new file mode 100644
index 0000000..5facb31
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/drawable/white_round_rect_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_audio_mixer.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_audio_mixer.xml
new file mode 100644
index 0000000..2c3193d
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_audio_mixer.xml
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_camera_microphone.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_camera_microphone.xml
new file mode 100644
index 0000000..f52ae7f
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_camera_microphone.xml
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_audio_only.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_audio_only.xml
new file mode 100644
index 0000000..f07e354
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_audio_only.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_av_capture.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_av_capture.xml
new file mode 100644
index 0000000..b3ebd30
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_av_capture.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_message.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_message.xml
new file mode 100644
index 0000000..1cd6a53
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_message.xml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_transcoding_live_streaming.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_transcoding_live_streaming.xml
new file mode 100644
index 0000000..5b005e2
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_custom_transcoding_live_streaming.xml
@@ -0,0 +1,675 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_default_transcoding_live_streaming.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_default_transcoding_live_streaming.xml
new file mode 100644
index 0000000..5fb9da8
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_default_transcoding_live_streaming.xml
@@ -0,0 +1,393 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_direct_live_streaming.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_direct_live_streaming.xml
new file mode 100644
index 0000000..7319a33
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_direct_live_streaming.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_main.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..e5bde44
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_media_statistics.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_media_statistics.xml
new file mode 100644
index 0000000..66536c4
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_media_statistics.xml
@@ -0,0 +1,639 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_microphone_audio_only.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_microphone_audio_only.xml
new file mode 100644
index 0000000..6986d6b
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_microphone_audio_only.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_multi_profile.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_multi_profile.xml
new file mode 100644
index 0000000..4e8092f
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_multi_profile.xml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/activity_screen_capture.xml b/QNRTC-API-Examples/app/src/main/res/layout/activity_screen_capture.xml
new file mode 100644
index 0000000..2ebccb7
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/activity_screen_capture.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/dialog_tips.xml b/QNRTC-API-Examples/app/src/main/res/layout/dialog_tips.xml
new file mode 100644
index 0000000..3a41aa3
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/dialog_tips.xml
@@ -0,0 +1,14 @@
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/layout/toast_message.xml b/QNRTC-API-Examples/app/src/main/res/layout/toast_message.xml
new file mode 100644
index 0000000..dfe05e6
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/layout/toast_message.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/QNRTC-API-Examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/QNRTC-API-Examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-hdpi/ic_launcher.png b/QNRTC-API-Examples/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/QNRTC-API-Examples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-mdpi/ic_launcher.png b/QNRTC-API-Examples/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/QNRTC-API-Examples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/QNRTC-API-Examples/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/QNRTC-API-Examples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/QNRTC-API-Examples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/QNRTC-API-Examples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/QNRTC-API-Examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/QNRTC-API-Examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/QNRTC-API-Examples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/QNRTC-API-Examples/app/src/main/res/values-night/themes.xml b/QNRTC-API-Examples/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..4ebd210
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/values/colors.xml b/QNRTC-API-Examples/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..0eab9a8
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/values/colors.xml
@@ -0,0 +1,16 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF1E90FF
+ #FF1E8BFF
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
+ #FFF2F2F7
+ #FFDCDCDC
+ #FF808080
+ #979797
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/values/strings.xml b/QNRTC-API-Examples/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..778eadf
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/values/strings.xml
@@ -0,0 +1,152 @@
+
+ QNRTC-API-Examples
+ 视频通话相关
+ 纯音频通话相关
+ CDN 转推相关
+ 其他功能
+ 摄像头采集音视频通话
+ 自定义采集音视频通话
+ 屏幕录制采集音视频通话
+ 麦克风采集纯音频通话
+ 自定义采集纯音频通话
+ CDN 单人转推
+ CDN 合流转推(默认合流任务)
+ CDN 合流转推(自定义合流任务)
+ 音频文件混音
+ 发送房间消息
+ 大小流
+ 通话质量统计
+ 非法 token
+ 美颜开关
+ 美颜强度
+ 红润强度
+ 白皙强度
+ Tips:本示例仅展示一对一场景下 SDK 内置摄像头采集视频 Track 和麦克风采集音频 Track 的发布和订阅,以及基于摄像头视频 Track 的美颜功能。
+ 音频路由改变:%1$s
+ 房间状态改变:%1$s
+ 发布成功
+ 发布失败:[%1$d, %2$s]
+ Tips:本示例仅展示一对一场景下自定义音视频 Track 的发布和订阅功能,音视频数据源采集均使用系统采集实现。
+ 录屏本端无预览
+ Tips:本示例仅展示一对一场景下 SDK 内置录屏采集视频 Track 和麦克风采集音频 Track 的发布和订阅功能。
+ 存在已订阅用户,不再订阅其他用户
+ 本地视图
+ 远端视图
+ 本地音频 Track
+ 远端音频 Track
+ Tips:本示例仅展示一对一场景下 SDK 内置麦克风采集音频 Track 的发布和订阅,以及基于音频 Track 的相关功能。
+ 注:远端音频的可调节范围是 0–10,调节过大可能会影响音质和损坏音频设备
+ 本地音频采集音量
+ 远端音频播放音量
+ 远端用户离开房间
+ Tips:本示例仅展示一对一场景下自定义音频采集 Track 的发布和订阅功能,音频数据源采集使用系统采集实现。
+ 转推地址:
+ 转推角色(转推该用户的音视频 Track):
+ 本地用户
+ 远端用户
+ 开始单人转推
+ 停止单人转推
+ 请输入推流地址
+ 非法推流地址
+ 未发布本地音视频 Track
+ 未订阅远端音视频 Track
+ 请先停止转推任务
+ 请先开始转推任务
+ %1$s 开始转推成功
+ %1$s 停止转推成功
+ %1$s转推出错: %2$d
+ Tips:本示例仅展示一对一场景下本地或远端音视频 Track 的单路转推功能,使用转推功能需要在七牛后台开启对应 AppId 的转推功能开关。
+ Tips:本示例仅展示一对一场景下消息的发送和接收功能。
+ 消息文本
+ 请输入自定义信息
+ 发送
+ 收到消息:\nUserID: %1$s\nMessageID: %2$s\nContent: %3$s\nTime: %4$s
+ 取消
+ Tips:本示例仅展示一对一场景下相机 Track 的大小流发布和订阅,请注意:\n1. 开启大小流功能设置编码宽高最低为 1280 x 720。\n2. 建议发送端在仅发布单路视频 Track 的场景下,使用大小流功能。\n"3. 对于开启大小流的用户,建议保证有良好的网络环境,保证多流发送质量。
+ 当前订阅的 Profile:
+ 设置预期订阅的 Profile:
+ Low
+ Medium
+ High
+ None
+ 切换
+ 订阅视频 Profile 更新 : %1$s
+ Tips:本示例仅展示一对一场景下发布订阅后获取本地和远端单路音视频 Track 状态统计的功能。
+ 本地上行网络质量:
+ 本地下行网络质量:
+ 本地音频 Track 质量:
+ 本地视频 Track 质量:
+ 远端上行网络质量:
+ 远端下行网络质量:
+ 远端音频 Track 质量:
+ 远端视频 Track 质量:
+ 上行码率:
+ 上行网络延时:
+ 上行丢包率:
+ 上行帧率:
+ 下行码率:
+ 下行网络延时:
+ 下行丢包率:
+ 下行帧率:
+ Profile 等级:
+ 0 kbps
+ %1$d kbps
+ 0 ms
+ %1$d ms
+ 0.0%
+ %1$d%%
+ 0 fps
+ %1$d fps
+ Tips:本示例仅展示一对一场景下的麦克风音频的发布和订阅,以及麦克风音频 Track 的混音功能。返听场景建议佩戴耳机。
+ 音乐地址:
+ 循环次数:
+ Start
+ Stop
+ Pause
+ Resume
+ 返听:
+ 播放进度:
+ %1$s/%2$s
+ 麦克风混音音量:
+ 音乐音频混音音量:
+ 音乐音频播放音量:
+ 请先开始混音
+ 非法音乐地址
+ 非法循环次数
+ 准备资源中…
+ 混音错误: %1$d
+ Tips:\n1. 本示例仅展示一对一场景下使用默认合流配置创建合流任务的功能。\n2. 使用转推功能需要在七牛后台开启对应 AppId 的转推功能开关。\n3. 默认合流任务使用七牛控制台下该 AppId 的合流配置,无需客户端创建。\n4. 添加布局即可触发合流转推,可以到 AppId 绑定的直播空间下查看活跃流。
+ px
+ 合流布局设置:
+ 待添加到合流布局的角色:
+ 视频 Track 布局:
+ x:
+ y:
+ 宽:
+ 高:
+ 层级(0 为最底层):
+ 音频 Track 布局:
+ 音频 Track 仅需配置 trackID 即可,详情可参考代码实现
+ 添加合流布局
+ 移除合流布局
+ 注意:请保证此处的宽高设置是基于您七牛控制台上配置的合流宽高计算的,否则合流画面将被剪裁
+ 非法参数
+ 合流任务已存在
+ 合流布局已更新
+ Tips:\n1. 本示例仅展示一对一场景下使用自定义合流配置创建合流任务的功能。\n2. 使用转推功能需要在七牛后台开启对应 AppId 的转推功能开关。\n3. 开启转推后即可用转推地址对应的拉流地址观看合流效果。
+ 自定义合流配置:
+ 转推地址:
+ 帧率:
+ fps
+ 码率:
+ kbps
+ 填充方式:
+ ASPECT_FILL
+ ASPECT_FIT
+ SCALE_FIT
+ 水印:
+ 背景图:
+ 开启合流转推
+ 停止合流转推
+ 扫描
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/values/styles.xml b/QNRTC-API-Examples/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..1578f6d
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/values/styles.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/app/src/main/res/values/themes.xml b/QNRTC-API-Examples/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..c574e44
--- /dev/null
+++ b/QNRTC-API-Examples/app/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/QNRTC-API-Examples/build.gradle b/QNRTC-API-Examples/build.gradle
new file mode 100644
index 0000000..4d47cbd
--- /dev/null
+++ b/QNRTC-API-Examples/build.gradle
@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:4.2.2"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ jcenter() // Warning: this repository is going to shut down soon
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
+
+ext {
+ buildWithQNDroidRTCLibrary = false
+}
\ No newline at end of file
diff --git a/QNRTC-API-Examples/gradle.properties b/QNRTC-API-Examples/gradle.properties
new file mode 100644
index 0000000..1d75b56
--- /dev/null
+++ b/QNRTC-API-Examples/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+android.injected.testOnly=false
\ No newline at end of file
diff --git a/QNRTC-API-Examples/gradle/wrapper/gradle-wrapper.jar b/QNRTC-API-Examples/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/QNRTC-API-Examples/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/QNRTC-API-Examples/gradle/wrapper/gradle-wrapper.properties b/QNRTC-API-Examples/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..6d8bbb8
--- /dev/null
+++ b/QNRTC-API-Examples/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Jan 29 14:40:13 CST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/QNRTC-API-Examples/gradlew b/QNRTC-API-Examples/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/QNRTC-API-Examples/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/QNRTC-API-Examples/gradlew.bat b/QNRTC-API-Examples/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/QNRTC-API-Examples/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/QNRTC-API-Examples/settings.gradle b/QNRTC-API-Examples/settings.gradle
new file mode 100644
index 0000000..7893c49
--- /dev/null
+++ b/QNRTC-API-Examples/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = "QNRTC-API-Examples"
+include ':app'