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 @@ + + + + + + + + + + + + + + + + + + + + + + + +