diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 000000000..d662cda96
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,38 @@
+#!/bin/sh
+#
+# An example hook script to verify what is about to be committed.
+# Called by "git commit" with no arguments. The hook should
+# exit with non-zero status after issuing an appropriate message if
+# it wants to stop the commit.
+#
+# To enable this hook, rename this file to "pre-commit".
+
+if git rev-parse --verify HEAD >/dev/null 2>&1
+then
+ against=HEAD
+else
+ # Initial commit: diff against an empty tree object
+ against=$(git hash-object -t tree /dev/null)
+fi
+
+SCRIPT_DIR=$(dirname "$0")
+SCRIPT_ABS_PATH=`cd "$SCRIPT_DIR"; pwd`
+
+
+ANDROID_DIFF_FILES=`git diff --cached --name-only --diff-filter=ACM -- '*' | grep 'Android'`
+if [[ "$ANDROID_DIFF_FILES" != "" ]]
+then
+ cd Android/APIExample
+ echo "precommit >> current paht = $(pwd), diff files = $ANDROID_DIFF_FILES"
+ ./gradlew -Dorg.gradle.project.commit_diff_files="$ANDROID_DIFF_FILES" checkstyle detekt
+ if [ $? -eq 0 ]; then
+ echo "precommit >> checkstyle detekt OK."
+ else
+ echo "precommit >> checkstyle detekt Failed."
+ exit 1
+ fi
+else
+ echo "precommit >> No changing android files."
+fi
+
+
diff --git a/.github/ci/Jenkinsfile_bitbucket.groovy b/.github/ci/Jenkinsfile_bitbucket.groovy
new file mode 100644
index 000000000..28df4bfe9
--- /dev/null
+++ b/.github/ci/Jenkinsfile_bitbucket.groovy
@@ -0,0 +1,3 @@
+@Library('agora-build-pipeline-library') _
+
+pipelineLoad(this, "ApiExample", "workflow", "", "", "api-examples")
diff --git a/.github/ci/build/build_android.groovy b/.github/ci/build/build_android.groovy
new file mode 100644
index 000000000..d9cfdc887
--- /dev/null
+++ b/.github/ci/build/build_android.groovy
@@ -0,0 +1,56 @@
+// -*- mode: groovy -*-
+// vim: set filetype=groovy :
+@Library('agora-build-pipeline-library') _
+import groovy.transform.Field
+
+buildUtils = new agora.build.BuildUtils()
+
+compileConfig = [
+ "sourceDir": "api-examples",
+ "docker": "hub.agoralab.co/server/apiexample_build_android:latest",
+ "non-publish": [
+ "command": "./.github/ci/build/build_android.sh",
+ "extraArgs": "",
+ ],
+ "publish": [
+ "command": "./.github/ci/build/build_android.sh",
+ "extraArgs": "",
+ ]
+]
+
+def doBuild(buildVariables) {
+ type = params.Package_Publish ? "publish" : "non-publish"
+ command = compileConfig.get(type).command
+ preCommand = compileConfig.get(type).get("preCommand", "")
+ postCommand = compileConfig.get(type).get("postCommand", "")
+ docker = compileConfig.docker
+ extraArgs = compileConfig.get(type).extraArgs
+ extraArgs += " " + params.getOrDefault("extra_args", "")
+ commandConfig = [
+ "command": command,
+ "sourceRoot": "${compileConfig.sourceDir}",
+ "extraArgs": extraArgs,
+ "docker": docker,
+ ]
+ loadResources(["config.json", "artifactory_utils.py"])
+ buildUtils.customBuild(commandConfig, preCommand, postCommand)
+}
+
+def doPublish(buildVariables) {
+ if (!params.Package_Publish) {
+ return
+ }
+ (shortVersion, releaseVersion) = buildUtils.getBranchVersion()
+ def archiveInfos = [
+ [
+ "type": "ARTIFACTORY",
+ "archivePattern": "*.zip",
+ "serverPath": "ApiExample/${shortVersion}/${buildVariables.buildDate}/${env.platform}",
+ "serverRepo": "SDK_repo"
+ ]
+ ]
+ archive.archiveFiles(archiveInfos)
+ sh "rm -rf *.zip || true"
+}
+
+pipelineLoad(this, "ApiExample", "build", "android", "apiexample_linux")
diff --git a/.github/ci/build/build_android.sh b/.github/ci/build/build_android.sh
new file mode 100644
index 000000000..053f65542
--- /dev/null
+++ b/.github/ci/build/build_android.sh
@@ -0,0 +1,121 @@
+##################################
+# --- Guidelines: ---
+#
+# Common Environment Variable Injected:
+# 'Package_Publish:boolean:true',
+# 'Clean_Clone:boolean:false',
+# 'is_tag_fetch:boolean:false',
+# 'is_offical_build:boolean:false',
+# 'repo:string',
+# 'base:string',
+# 'arch:string'
+# 'output:string'
+# 'short_version:string'
+# 'release_version:string'
+# 'build_date:string(yyyyMMdd)',
+# 'build_timestamp:string (yyyyMMdd_hhmm)',
+# 'platform: string',
+# 'BUILD_NUMBER: string',
+# 'WORKSPACE: string'
+#
+# --- Test Related: ---
+# PR build, zip test related to test.zip
+# Package build, zip package related to package.zip
+# --- Artifactory Related: ---
+# download artifactory:
+# python3 ${WORKSPACE}/artifactory_utils.py --action=download_file --file=ARTIFACTORY_URL
+# upload file to artifactory:
+# python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_pattern
+# for example: python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_pattern
+# upload folder to artifactory
+# python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_folder
+# for example: python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_folder
+# --- Input: ----
+# sourcePath: see jenkins console for details.
+# WORKSPACE: ${WORKSPACE}
+# --- Output: ----
+# pr: output test.zip to workspace dir
+# others: Rename the zip package name yourself, But need copy it to workspace dir
+##################################
+
+echo Package_Publish: $Package_Publish
+echo is_tag_fetch: $is_tag_fetch
+echo arch: $arch
+echo source_root: %source_root%
+echo output: /tmp/jenkins/${project}_out
+echo build_date: $build_date
+echo build_time: $build_time
+echo release_version: $release_version
+echo short_version: $short_version
+echo pwd: `pwd`
+echo sdk_url: $sdk_url
+
+ls ~/.gradle || (mkdir -p /tmp/.gradle && ln -s /tmp/.gradle ~/.gradle && touch ~/.gradle/ln_$(date "+%y%m%d%H") && ls ~/.gradle)
+
+zip_name=${sdk_url##*/}
+echo zip_name: $zip_name
+
+# env LC_ALL=en_US.UTF-8 python3 $WORKSPACE/artifactory_utils.py --action=download_file --file=$sdk_url || exit 1
+curl -o $zip_name $sdk_url || exit 1
+7za x ./$zip_name -y > log.txt
+
+unzip_name=`ls -S -d */ | grep Agora | sed 's/\///g'`
+echo unzip_name: $unzip_name
+
+rm -rf ./$unzip_name/rtc/bin
+rm -rf ./$unzip_name/rtc/demo
+rm ./$unzip_name/rtc/commits
+rm ./$unzip_name/rtc/package_size_report.txt
+mkdir ./$unzip_name/rtc/samples
+mkdir ./$unzip_name/rtc/samples/API-example
+
+if [ ! -z "$(echo $sdk_url | grep 'audio')" ] || [ ! -z "$(echo $sdk_url | grep 'VOICE')" ]
+then
+audio_suffix=-Audio
+else
+audio_suffix=
+fi
+echo audio_suffix: $audio_suffix
+
+cp -rf ./Android/APIExample${audio_suffix}/** ./$unzip_name/rtc/samples/API-example
+7za a -tzip result.zip -r $unzip_name > log.txt
+mv result.zip $WORKSPACE/withAPIExample_$(date "+%d%H%M")_$zip_name
+
+# install android sdk
+which java
+java --version
+source ~/.bashrc
+export ANDROID_HOME=/usr/lib/android_sdk
+echo ANDROID_HOME: $ANDROID_HOME
+
+# compile apk
+cd ./$unzip_name/rtc/samples/API-example
+pwd
+
+## config appId
+sed -i -e "s#YOUR APP ID#${APP_ID}#g" app/src/main/res/values/string_configs.xml
+sed -i -e "s#YOUR APP CERTIFICATE##g" app/src/main/res/values/string_configs.xml
+sed -i -e "s#YOUR ACCESS TOKEN##g" app/src/main/res/values/string_configs.xml
+rm -f app/src/main/res/values/string_configs.xml-e
+cat app/src/main/res/values/string_configs.xml
+
+## config simple filter
+sed -i -e "s#simpleFilter = false#simpleFilter = true#g" gradle.properties
+mkdir -p agora-simple-filter/src/main/agoraLibs
+cp -r ../../sdk/arm64-v8a agora-simple-filter/src/main/agoraLibs/
+cp -r ../../sdk/armeabi-v7a agora-simple-filter/src/main/agoraLibs/
+curl -o opencv4.zip https://agora-adc-artifacts.s3.cn-north-1.amazonaws.com.cn/androidLibs/opencv4.zip
+unzip opencv4.zip
+mkdir -p agora-simple-filter/src/main/libs
+mv arm64-v8a agora-simple-filter/src/main/libs
+mv armeabi-v7a agora-simple-filter/src/main/libs
+sed -i -e "s#jniLibs/#libs/#g" agora-simple-filter/src/main/cpp/CMakeLists.txt
+
+./gradlew clean || exit 1
+./gradlew :app:assembleDebug || exit 1
+cp app/build/outputs/apk/debug/app-debug.apk ./APIExample_Android_$(date "+%y%m%d%H").apk
+7za a -tzip result.zip -r *.apk > log.txt
+mv result.zip $WORKSPACE/APIExample_Android${audio_suffix}_$(date "+%y%m%d%H%M")_apk.zip
+ls $WORKSPACE
+cd -
+
diff --git a/.github/ci/build/build_ios.groovy b/.github/ci/build/build_ios.groovy
new file mode 100644
index 000000000..b1faf2479
--- /dev/null
+++ b/.github/ci/build/build_ios.groovy
@@ -0,0 +1,53 @@
+// -*- mode: groovy -*-
+// vim: set filetype=groovy :
+@Library('agora-build-pipeline-library') _
+import groovy.transform.Field
+
+buildUtils = new agora.build.BuildUtils()
+
+compileConfig = [
+ "sourceDir": "api-examples",
+ "non-publish": [
+ "command": "./.github/ci/build/build_ios.sh",
+ "extraArgs": "",
+ ],
+ "publish": [
+ "command": "./.github/ci/build/build_ios.sh",
+ "extraArgs": "",
+ ]
+]
+
+def doBuild(buildVariables) {
+ type = params.Package_Publish ? "publish" : "non-publish"
+ command = compileConfig.get(type).command
+ preCommand = compileConfig.get(type).get("preCommand", "")
+ postCommand = compileConfig.get(type).get("postCommand", "")
+ extraArgs = compileConfig.get(type).extraArgs
+ extraArgs += " " + params.getOrDefault("extra_args", "")
+ commandConfig = [
+ "command": command,
+ "sourceRoot": "${compileConfig.sourceDir}",
+ "extraArgs": extraArgs
+ ]
+ loadResources(["config.json", "artifactory_utils.py"])
+ buildUtils.customBuild(commandConfig, preCommand, postCommand)
+}
+
+def doPublish(buildVariables) {
+ if (!params.Package_Publish) {
+ return
+ }
+ (shortVersion, releaseVersion) = buildUtils.getBranchVersion()
+ def archiveInfos = [
+ [
+ "type": "ARTIFACTORY",
+ "archivePattern": "*.zip",
+ "serverPath": "ApiExample/${shortVersion}/${buildVariables.buildDate}/${env.platform}",
+ "serverRepo": "SDK_repo" // ATTENTIONS: Update the artifactoryRepo if needed.
+ ]
+ ]
+ archive.archiveFiles(archiveInfos)
+ sh "rm -rf *.zip || true"
+}
+
+pipelineLoad(this, "ApiExample", "build", "ios", "apiexample_mac")
\ No newline at end of file
diff --git a/.github/ci/build/build_ios.sh b/.github/ci/build/build_ios.sh
new file mode 100644
index 000000000..e39a1f792
--- /dev/null
+++ b/.github/ci/build/build_ios.sh
@@ -0,0 +1,129 @@
+#!/bin/bash -ilex
+##################################
+# --- Guidelines: ---
+#
+# Common Environment Variable:
+# 'Package_Publish:boolean:true',
+# 'Clean_Clone:boolean:false',
+# 'is_tag_fetch:boolean:false',
+# 'is_offical_build:boolean:false',
+# 'repo:string',
+# 'base:string',
+# 'arch:string'
+# 'output:string'
+# 'short_version:string'
+# 'release_version:string'
+# 'build_date:string(yyyyMMdd)',
+# 'build_timestamp:string (yyyyMMdd_hhmm)',
+# 'platform: string',
+# 'BUILD_NUMBER: string',
+# 'WORKSPACE: string'
+#
+# --- Test Related: ---
+# PR build, zip test related to test.zip
+# Package build, zip package related to package.zip
+# --- Artifactory Related: ---
+# download artifactory:
+# python3 ${WORKSPACE}/artifactory_utils.py --action=download_file --file=ARTIFACTORY_URL
+# upload file to artifactory:
+# python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_pattern
+# for example: python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_pattern
+# upload folder to artifactory
+# python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_folder
+# for example: python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_folder
+# --- Input: ----
+# sourcePath: see jenkins console for details.
+# WORKSPACE: ${WORKSPACE}
+# --- Output: ----
+# pr: output test.zip to workspace dir
+# others: Rename the zip package name yourself, But need copy it to workspace dir
+##################################
+
+echo is_generate_validate_app: $is_generate_validate_app
+echo Package_Publish: $Package_Publish
+echo is_tag_fetch: $is_tag_fetch
+echo arch: $arch
+echo source_root: %source_root%
+echo output: /tmp/jenkins/${project}_out
+echo build_date: $build_date
+echo build_time: $build_time
+echo release_version: $release_version
+echo short_version: $short_version
+echo pwd: `pwd`
+echo sdk_url: $sdk_url
+
+zip_name=${sdk_url##*/}
+echo zip_name: $zip_name
+
+python3 $WORKSPACE/artifactory_utils.py --action=download_file --file=$sdk_url
+7za x ./$zip_name -y
+
+unzip_name=`ls -S -d */ | grep Agora`
+echo unzip_name: $unzip_name
+
+rm -rf ./$unzip_name/bin
+rm ./$unzip_name/commits
+rm ./$unzip_name/package_size_report.txt
+mkdir ./$unzip_name/samples
+mkdir ./$unzip_name/samples/API-Example
+if [ $? -eq 0 ]; then
+ echo "success"
+else
+ echo "failed"
+ exit 1
+fi
+
+cp -rf ./iOS/** ./$unzip_name/samples/API-Example
+
+result=$(echo $sdk_url | grep "VOICE")
+if [ ! -z "$result" ]
+then
+ echo "包含"
+ rm -rf ./$unzip_name/samples/API-Example/APIExample
+ rm -rf ./$unzip_name/samples/API-Example/APIExample-OC
+ mv ./$unzip_name/samples/API-Example/APIExample-Audio ./$unzip_name/samples/APIExample-Audio
+ mv ./$unzip_name/samples/APIExample-Audio/sdk.podspec ./$unzip_name/
+ python3 ./.github/ci/build/modify_podfile.py ./$unzip_name/samples/APIExample-Audio/Podfile
+ if [ $? -eq 0 ]; then
+ echo "success"
+ else
+ echo "failed"
+ exit 1
+ fi
+ if [ $is_generate_validate_app = true ]; then
+ ./.github/ci/build/build_ios_ipa.sh ./$unzip_name/samples/APIExample-Audio
+ fi
+
+else
+ echo "不包含"
+ rm -rf ./$unzip_name/samples/API-Example/APIExample-Audio
+ if [ $is_objective_c = true ]; then
+ rm -rf ./$unzip_name/samples/API-Example/APIExample
+ mv ./$unzip_name/samples/API-Example/APIExample-OC ./$unzip_name/samples/APIExample-OC
+ mv ./$unzip_name/samples/APIExample-OC/sdk.podspec ./$unzip_name/
+ python3 ./.github/ci/build/modify_podfile.py ./$unzip_name/samples/APIExample-OC/Podfile
+ else
+ rm -rf ./$unzip_name/samples/API-Example/APIExample-OC
+ mv ./$unzip_name/samples/API-Example/APIExample ./$unzip_name/samples/APIExample
+ mv ./$unzip_name/samples/APIExample/sdk.podspec ./$unzip_name/
+ python3 ./.github/ci/build/modify_podfile.py ./$unzip_name/samples/APIExample/Podfile
+ fi
+
+ if [ $? -eq 0 ]; then
+ echo "success"
+ else
+ echo "failed"
+ exit 1
+ fi
+ if [ $is_generate_validate_app = true ]; then
+ if [ $is_objective_c = true ]; then
+ ./.github/ci/build/build_ios_ipa.sh ./$unzip_name/samples/APIExample-OC
+ else
+ ./.github/ci/build/build_ios_ipa.sh ./$unzip_name/samples/APIExample
+ fi
+ fi
+fi
+
+rm -rf ./$unzip_name/samples/API-Example
+7za a -tzip result.zip -r $unzip_name
+cp result.zip $WORKSPACE/withAPIExample_${BUILD_NUMBER}_$zip_name
diff --git a/.github/ci/build/build_ios_ipa.sh b/.github/ci/build/build_ios_ipa.sh
new file mode 100755
index 000000000..7badd7a4d
--- /dev/null
+++ b/.github/ci/build/build_ios_ipa.sh
@@ -0,0 +1,165 @@
+CURRENT_PATH=$PWD
+
+# 获取项目目录
+PROJECT_PATH="$( cd "$1" && pwd )"
+
+cd ${PROJECT_PATH} && pod install
+
+if [ $? -eq 0 ]; then
+ echo "success"
+else
+ echo "failed"
+ exit 1
+fi
+
+# 项目target名
+TARGET_NAME=${PROJECT_PATH##*/}
+
+KEYCENTER_PATH=${PROJECT_PATH}"/"${TARGET_NAME}"/Common/KeyCenter.swift"
+if [ $is_objective_c = true ]; then
+ KEYCENTER_PATH=${PROJECT_PATH}"/"${TARGET_NAME}"/Common/KeyCenter.m"
+fi
+
+# 打包环境
+CONFIGURATION="Debug"
+
+#工程文件路径
+APP_PATH="${PROJECT_PATH}/${TARGET_NAME}.xcworkspace"
+
+#工程配置路径
+PBXPROJ_PATH="${PROJECT_PATH}/${TARGET_NAME}.xcodeproj/project.pbxproj"
+echo PBXPROJ_PATH: $PBXPROJ_PATH
+
+# 主项目工程配置
+if [ $is_objective_c = true ]; then
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE062A5D0050009947CF:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE062A5D0050009947CF:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE062A5D0050009947CF:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE072A5D0050009947CF:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE072A5D0050009947CF:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE072A5D0050009947CF:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+
+ # 屏幕共享Extension
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:E72F61D42A7256D500C963D2:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E72F61D42A7256D500C963D2:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E72F61D42A7256D500C963D2:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:E72F61D52A7256D500C963D2:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E72F61D52A7256D500C963D2:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E72F61D52A7256D500C963D2:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+
+ # SimpleFilter
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:E7361F932A6E6E7100925BD6:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E7361F932A6E6E7100925BD6:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E7361F932A6E6E7100925BD6:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:E7361F942A6E6E7100925BD6:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E7361F942A6E6E7100925BD6:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:E7361F942A6E6E7100925BD6:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH
+
+ #修改build number
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE062A5D0050009947CF:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:E70ADE072A5D0050009947CF:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH
+else
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+
+ # 屏幕共享Extension
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH
+
+ # SimpleFilter
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH
+ /usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH
+
+ #修改build number
+ # Debug
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH
+ # Release
+ /usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH
+fi
+
+# 读取APPID环境变量
+echo AGORA_APP_ID:$APP_ID
+echo $AGORA_APP_ID
+
+echo PROJECT_PATH: $PROJECT_PATH
+echo TARGET_NAME: $TARGET_NAME
+echo KEYCENTER_PATH: $KEYCENTER_PATH
+echo APP_PATH: $APP_PATH
+
+#修改Keycenter文件
+python3 /tmp/jenkins/api-examples/.github/ci/build/modify_ios_keycenter.py $KEYCENTER_PATH 0
+
+# Xcode clean
+xcodebuild clean -workspace "${APP_PATH}" -configuration "${CONFIGURATION}" -scheme "${TARGET_NAME}"
+
+# 时间戳
+CURRENT_TIME=$(date "+%Y-%m-%d %H-%M-%S")
+
+# 归档路径
+ARCHIVE_PATH="${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}.xcarchive"
+# 编译环境
+
+# plist路径
+PLIST_PATH="${PROJECT_PATH}/ExportOptions.plist"
+
+echo PLIST_PATH: $PLIST_PATH
+
+# archive 这边使用的工作区间 也可以使用project
+xcodebuild CODE_SIGN_STYLE="Manual" archive -workspace "${APP_PATH}" -scheme "${TARGET_NAME}" clean CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -configuration "${CONFIGURATION}" -archivePath "${ARCHIVE_PATH}" -destination 'generic/platform=iOS' -quiet || exit
+
+cd ${WORKSPACE}
+
+# 压缩archive
+7za a -tzip "${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" "${ARCHIVE_PATH}"
+
+# 签名
+# sh sign "${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" --type xcarchive --plist "${PLIST_PATH}"
+sh export "${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" --plist "${PLIST_PATH}"
+
+SDK_VERSION=$(echo $sdk_url | cut -d "/" -f 5)
+# 上传IPA
+PAYLOAD_PATH="${TARGET_NAME}_SDK_${SDK_VERSION}_CI_${BUILD_NUMBER}_Payload"
+mkdir "${PAYLOAD_PATH}"
+# mv "${TARGET_NAME}_${BUILD_NUMBER}_iOS.ipa" "${PAYLOAD_PATH}"
+mv "${TARGET_NAME}_${BUILD_NUMBER}.ipa" "${PAYLOAD_PATH}"
+
+7za a "${TARGET_NAME}_SDK_${SDK_VERSION}_CI_${BUILD_NUMBER}_IPA.zip" -r "${PAYLOAD_PATH}"
+python3 artifactory_utils.py --action=upload_file --file="${TARGET_NAME}_SDK_${SDK_VERSION}_CI_${BUILD_NUMBER}_IPA.zip" --project
+
+# 删除IPA文件夹
+rm -rf ${TARGET_NAME}_SDK_${SDK_VERSION}_CI_${BUILD_NUMBER}.xcarchive
+rm -rf *.zip
+rm -rf ${PAYLOAD_PATH}
+
+#复原Keycenter文件
+python3 /tmp/jenkins/api-examples/.github/ci/build/modify_ios_keycenter.py $KEYCENTER_PATH 1
+
+
+
+
diff --git a/.github/ci/build/build_mac.groovy b/.github/ci/build/build_mac.groovy
new file mode 100644
index 000000000..7760a16f8
--- /dev/null
+++ b/.github/ci/build/build_mac.groovy
@@ -0,0 +1,53 @@
+// -*- mode: groovy -*-
+// vim: set filetype=groovy :
+@Library('agora-build-pipeline-library') _
+import groovy.transform.Field
+
+buildUtils = new agora.build.BuildUtils()
+
+compileConfig = [
+ "sourceDir": "api-examples",
+ "non-publish": [
+ "command": "./.github/ci/build/build_mac.sh",
+ "extraArgs": "",
+ ],
+ "publish": [
+ "command": "./.github/ci/build/build_mac.sh",
+ "extraArgs": "",
+ ]
+]
+
+def doBuild(buildVariables) {
+ type = params.Package_Publish ? "publish" : "non-publish"
+ command = compileConfig.get(type).command
+ preCommand = compileConfig.get(type).get("preCommand", "")
+ postCommand = compileConfig.get(type).get("postCommand", "")
+ extraArgs = compileConfig.get(type).extraArgs
+ extraArgs += " " + params.getOrDefault("extra_args", "")
+ commandConfig = [
+ "command": command,
+ "sourceRoot": "${compileConfig.sourceDir}",
+ "extraArgs": extraArgs
+ ]
+ loadResources(["config.json", "artifactory_utils.py"])
+ buildUtils.customBuild(commandConfig, preCommand, postCommand)
+}
+
+def doPublish(buildVariables) {
+ if (!params.Package_Publish) {
+ return
+ }
+ (shortVersion, releaseVersion) = buildUtils.getBranchVersion()
+ def archiveInfos = [
+ [
+ "type": "ARTIFACTORY",
+ "archivePattern": "*.zip",
+ "serverPath": "ApiExample/${shortVersion}/${buildVariables.buildDate}/${env.platform}",
+ "serverRepo": "SDK_repo" // ATTENTIONS: Update the artifactoryRepo if needed.
+ ]
+ ]
+ archive.archiveFiles(archiveInfos)
+ sh "rm -rf *.zip || true"
+}
+
+pipelineLoad(this, "ApiExample", "build", "mac", "apiexample_mac")
\ No newline at end of file
diff --git a/.github/ci/build/build_mac.sh b/.github/ci/build/build_mac.sh
new file mode 100644
index 000000000..9a2e0deeb
--- /dev/null
+++ b/.github/ci/build/build_mac.sh
@@ -0,0 +1,84 @@
+##################################
+# --- Guidelines: ---
+#
+# Common Environment Variable:
+# 'Package_Publish:boolean:true',
+# 'Clean_Clone:boolean:false',
+# 'is_tag_fetch:boolean:false',
+# 'is_offical_build:boolean:false',
+# 'repo:string',
+# 'base:string',
+# 'arch:string'
+# 'output:string'
+# 'short_version:string'
+# 'release_version:string'
+# 'build_date:string(yyyyMMdd)',
+# 'build_timestamp:string (yyyyMMdd_hhmm)',
+# 'platform: string',
+# 'BUILD_NUMBER: string',
+# 'WORKSPACE: string'
+#
+# --- Test Related: ---
+# PR build, zip test related to test.zip
+# Package build, zip package related to package.zip
+# --- Artifactory Related: ---
+# download artifactory:
+# python3 ${WORKSPACE}/artifactory_utils.py --action=download_file --file=ARTIFACTORY_URL
+# upload file to artifactory:
+# python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_pattern
+# for example: python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_pattern
+# upload folder to artifactory
+# python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_folder
+# for example: python3 ${WORKSPACE}/artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_folder
+# --- Input: ----
+# sourcePath: see jenkins console for details.
+# WORKSPACE: ${WORKSPACE}
+# --- Output: ----
+# pr: output test.zip to workspace dir
+# others: Rename the zip package name yourself, But need copy it to workspace dir
+##################################
+
+echo is_generate_validate_app:$is_generate_validate_app
+echo Package_Publish: $Package_Publish
+echo is_tag_fetch: $is_tag_fetch
+echo arch: $arch
+echo source_root: %source_root%
+echo output: /tmp/jenkins/${project}_out
+echo build_date: $build_date
+echo build_time: $build_time
+echo release_version: $release_version
+echo short_version: $short_version
+echo pwd: `pwd`
+echo sdk_url: $sdk_url
+
+zip_name=${sdk_url##*/}
+echo zip_name: $zip_name
+
+python3 $WORKSPACE/artifactory_utils.py --action=download_file --file=$sdk_url
+7za x ./$zip_name -y
+
+unzip_name=`ls -S -d */ | grep Agora`
+echo unzip_name: $unzip_name
+
+rm -rf ./$unzip_name/bin
+rm ./$unzip_name/commits
+rm ./$unzip_name/package_size_report.txt
+mkdir ./$unzip_name/samples
+mkdir ./$unzip_name/samples/APIExample
+if [ $? -eq 0 ]; then
+ echo "success"
+else
+ echo "failed"
+ exit 1
+fi
+cp -a ./macOS/** ./$unzip_name/samples/APIExample
+mv ./$unzip_name/samples/APIExample/sdk.podspec ./$unzip_name/
+python3 ./.github/ci/build/modify_podfile.py ./$unzip_name/samples/APIExample/Podfile
+
+
+if [ $is_generate_validate_app = true ]; then
+ ./.github/ci/build/build_mac_ipa.sh ./$unzip_name/samples/APIExample
+fi
+
+7za a -tzip result.zip -r $unzip_name
+cp result.zip $WORKSPACE/withAPIExample_${BUILD_NUMBER}_$zip_name
diff --git a/.github/ci/build/build_mac_ipa.sh b/.github/ci/build/build_mac_ipa.sh
new file mode 100755
index 000000000..7c295439c
--- /dev/null
+++ b/.github/ci/build/build_mac_ipa.sh
@@ -0,0 +1,108 @@
+CURRENT_PATH=$PWD
+
+# 获取项目目录
+PROJECT_PATH="$( cd "$1" && pwd )"
+
+cd ${PROJECT_PATH} && pod install
+if [ $? -eq 0 ]; then
+ echo " pod install success"
+else
+ echo " pod install failed"
+ exit 1
+fi
+
+# 项目target名
+TARGET_NAME=${PROJECT_PATH##*/}
+
+KEYCENTER_PATH=${PROJECT_PATH}"/"${TARGET_NAME}"/Common/KeyCenter.swift"
+
+# 打包环境
+CONFIGURATION=Release
+
+#工程文件路径
+APP_PATH="${PROJECT_PATH}/${TARGET_NAME}.xcworkspace"
+
+#工程配置路径
+PBXPROJ_PATH="${PROJECT_PATH}/${TARGET_NAME}.xcodeproj/project.pbxproj"
+echo PBXPROJ_PATH: $PBXPROJ_PATH
+
+# 主项目工程配置
+# Debug
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:CODE_SIGN_IDENTITY 'Developer ID Application'" $PBXPROJ_PATH
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:DEVELOPMENT_TEAM 'YS397FG5PA'" $PBXPROJ_PATH
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'apiexamplemac'" $PBXPROJ_PATH
+# Release
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:CODE_SIGN_IDENTITY 'Developer ID Application'" $PBXPROJ_PATH
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:DEVELOPMENT_TEAM 'YS397FG5PA'" $PBXPROJ_PATH
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'apiexamplemac'" $PBXPROJ_PATH
+
+#修改build number
+# Debug
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH
+# Release
+/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH
+
+# 读取APPID环境变量
+echo AGORA_APP_ID:$APP_ID
+echo $AGORA_APP_ID
+
+echo PROJECT_PATH: $PROJECT_PATH
+echo TARGET_NAME: $TARGET_NAME
+echo KEYCENTER_PATH: $KEYCENTER_PATH
+echo APP_PATH: $APP_PATH
+
+#修改Keycenter文件
+
+python3 /tmp/jenkins/api-examples/.github/ci/build/modify_ios_keycenter.py $KEYCENTER_PATH 0
+if [ $? -eq 0 ]; then
+ echo "修改Keycenter文件 success"
+else
+ echo "修改Keycenter文件 failed"
+ exit 1
+fi
+# Xcode clean
+xcodebuild clean -workspace "${APP_PATH}" -configuration "${CONFIGURATION}" -scheme "${TARGET_NAME}"
+
+# 时间戳
+CURRENT_TIME=$(date "+%Y-%m-%d %H-%M-%S")
+
+SDK_VERSION=$(echo $sdk_url | cut -d "/" -f 5)
+
+# 归档路径
+ARCHIVE_PATH="${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}.xcarchive"
+# 编译环境
+
+# plist路径
+PLIST_PATH="${PROJECT_PATH}/ExportOptions.plist"
+
+echo PLIST_PATH: $PLIST_PATH
+
+# archive 这边使用的工作区间 也可以使用project
+xcodebuild archive -workspace "${APP_PATH}" -scheme "${TARGET_NAME}" -configuration "${CONFIGURATION}" -archivePath "${ARCHIVE_PATH}"
+
+cd ${WORKSPACE}
+
+# 压缩archive
+7za a -slp "${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" "${ARCHIVE_PATH}"
+
+# 签名
+sh sign "${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" --type xcarchive --plist "${PLIST_PATH}" --application macApp
+
+# 重命名
+cp "${TARGET_NAME}_${BUILD_NUMBER}.app.zip" "${TARGET_NAME}_SDK_${SDK_VERSION}_CI_${BUILD_NUMBER}.app.zip"
+
+# 上传IPA
+python3 artifactory_utils.py --action=upload_file --file="${TARGET_NAME}_SDK_${SDK_VERSION}_CI_${BUILD_NUMBER}.app.zip" --project
+
+# 删除archive文件
+rm -rf ${TARGET_NAME}_${BUILD_NUMBER}.xcarchive
+rm -rf *.zip
+
+#复原Keycenter文件
+python3 /tmp/jenkins/api-examples/.github/ci/build/modify_ios_keycenter.py $KEYCENTER_PATH 1
+
+
+
+
diff --git a/.github/ci/build/build_windows.bat b/.github/ci/build/build_windows.bat
new file mode 100644
index 000000000..7bdbb98f3
--- /dev/null
+++ b/.github/ci/build/build_windows.bat
@@ -0,0 +1,101 @@
+REM ##################################
+REM --- Guidelines: ---
+REM
+REM Common Environment Variable:
+REM 'Package_Publish:boolean:true',
+REM 'Clean_Clone:boolean:false',
+REM 'is_tag_fetch:boolean:false',
+REM 'is_offical_build:boolean:false',
+REM 'repo:string',
+REM 'base:string',
+REM 'arch:string'
+REM 'output:string'
+REM 'short_version:string'
+REM 'release_version:string'
+REM 'build_date:string(yyyyMMdd)',
+REM 'build_timestamp:string (yyyyMMdd_hhmm)',
+REM 'platform: string',
+REM 'BUILD_NUMBER: string',
+REM 'WORKSPACE: string'
+REM
+REM --- Test Related: ---
+REM For PR build, zip test related to test.zip
+REM For Package build, zip package related to package.zip
+REM --- Artifactory Related: ---
+REM download artifactory:
+REM python %WORKSPACE%\\artifactory_utils.py --action=download_file --file=ARTIFACTORY_URL
+REM upload file to artifactory:
+REM python %WORKSPACE%\\artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_pattern
+REM for example: python %WORKSPACE%\\artifactory_utils.py --action=upload_file --file=*.zip --server_path=windows/ --server_repo=ACCS_repo --with_pattern
+REM upload folder to artifactory
+REM python %WORKSPACE%\\artifactory_utils.py --action=upload_file --file=FILEPATTERN --server_path=SERVERPATH --server_repo=SERVER_REPO --with_folder
+REM for example: python %WORKSPACE%\\artifactory_utils.py --action=upload_file --file=publish --server_path=windows/ --server_repo=ACCS_repo --with_folder
+REM --- Input: ----
+REM sourcePath: see jenkins console for details.
+REM WORKSPACE: %WORKSPACE%
+REM --- Output: ----
+REM pr: output test.zip to workspace dir
+REM others: Rename the zip package name yourself, But need copy it to workspace dir
+REM ##################################
+
+echo Package_Publish: %Package_Publish%
+echo is_tag_fetch: %is_tag_fetch%
+echo arch: %arch%
+echo source_root: %source_root%
+echo output: C:\\tmp\\%project%_out
+echo build_date: %build_date%
+echo build_time: %build_time%
+echo release_version: %release_version%
+echo short_version: %short_version%
+echo pwd: %cd%
+echo sdk_url: %sdk_url%
+
+echo off
+set zip_name=%sdk_url%
+:LOOP
+for /f "tokens=1* delims=>" %%a in ("%zip_name%") do (
+ set zip_name=%%a
+ set part2=%%b
+)
+if "%part2%" EQU "" goto END
+set zip_name=%part2%
+goto LOOP
+:END
+echo on
+echo zip_name: %zip_name%
+
+dir
+
+echo off
+REM curl --silent %sdk_url% ./
+python %WORKSPACE%\\artifactory_utils.py --action=download_file --file=%sdk_url%
+7z x ./%zip_name% -y
+echo on
+
+dir
+
+rmdir /S /Q Agora_Native_SDK_for_Windows_FULL\demo
+del /F /Q Agora_Native_SDK_for_Windows_FULL\commits
+del /F /Q Agora_Native_SDK_for_Windows_FULL\package_size_report.txt
+mkdir Agora_Native_SDK_for_Windows_FULL\samples
+mkdir Agora_Native_SDK_for_Windows_FULL\samples\API-example
+rmdir /S /Q windows\cicd
+del /F /Q windows\APIExample\ci.py
+xcopy /Y /E windows\APIExample Agora_Native_SDK_for_Windows_FULL\samples\API-example
+xcopy /Y /E windows\README.md Agora_Native_SDK_for_Windows_FULL\samples\API-example
+xcopy /Y /E windows\README.zh.md Agora_Native_SDK_for_Windows_FULL\samples\API-example
+rmdir /S /Q Agora_Native_SDK_for_Windows_FULL\samples\API-example\APIExample\APIExample
+dir Agora_Native_SDK_for_Windows_FULL\samples\API-example\APIExample
+7z a -tzip result.zip -r Agora_Native_SDK_for_Windows_FULL
+copy result.zip %WORKSPACE%\\withAPIExample_%date:~4,2%%date:~7,2%%time:~0,2%%time:~3,2%_%zip_name%
+del /F result.zip
+del /F %WORKSPACE%\\%zip_name%
+
+cd Agora_Native_SDK_for_Windows_FULL\samples\API-example
+echo "compile start..."
+call installThirdParty.bat
+"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" "APIExample.sln" /p:platform="Win32" /p:configuration="Release"
+7z a -tzip result.zip -r Release
+copy result.zip %WORKSPACE%\\APIExample_windows_%date:~4,2%%date:~7,2%%time:~0,2%%time:~3,2%_Release_exe.zip
+del /F result.zip
+echo "compile done."
diff --git a/.github/ci/build/build_windows.groovy b/.github/ci/build/build_windows.groovy
new file mode 100644
index 000000000..1f7463e05
--- /dev/null
+++ b/.github/ci/build/build_windows.groovy
@@ -0,0 +1,53 @@
+// -*- mode: groovy -*-
+// vim: set filetype=groovy :
+@Library('agora-build-pipeline-library') _
+import groovy.transform.Field
+
+buildUtils = new agora.build.BuildUtils()
+
+compileConfig = [
+ "sourceDir": "api-examples",
+ "non-publish": [
+ "command": "./.github/ci/build/build_windows.bat",
+ "extraArgs": "",
+ ],
+ "publish": [
+ "command": "./.github/ci/build/build_windows.bat",
+ "extraArgs": "",
+ ]
+]
+
+def doBuild(buildVariables) {
+ type = params.Package_Publish ? "publish" : "non-publish"
+ command = compileConfig.get(type).command
+ preCommand = compileConfig.get(type).get("preCommand", "")
+ postCommand = compileConfig.get(type).get("postCommand", "")
+ extraArgs = compileConfig.get(type).extraArgs
+ extraArgs += " " + params.getOrDefault("extra_args", "")
+ commandConfig = [
+ "command": command,
+ "sourceRoot": "${compileConfig.sourceDir}",
+ "extraArgs": extraArgs
+ ]
+ loadResources(["config.json", "artifactory_utils.py"])
+ buildUtils.customBuild(commandConfig, preCommand, postCommand)
+}
+
+def doPublish(buildVariables) {
+ if (!params.Package_Publish) {
+ return
+ }
+ (shortVersion, releaseVersion) = buildUtils.getBranchVersion()
+ def archiveInfos = [
+ [
+ "type": "ARTIFACTORY",
+ "archivePattern": "*.zip",
+ "serverPath": "ApiExample/${shortVersion}/${buildVariables.buildDate}/${env.platform}",
+ "serverRepo": "SDK_repo" // ATTENTIONS: Update the artifactoryRepo if needed.
+ ]
+ ]
+ archive.archiveFiles(archiveInfos)
+ bat "del /f /Q *.zip"
+}
+
+pipelineLoad(this, "ApiExample", "build", "windows", "apiexample_windows")
\ No newline at end of file
diff --git a/.github/ci/build/modify_ios_keycenter.py b/.github/ci/build/modify_ios_keycenter.py
new file mode 100644
index 000000000..784833a4d
--- /dev/null
+++ b/.github/ci/build/modify_ios_keycenter.py
@@ -0,0 +1,45 @@
+import os, sys
+
+def modfiy(path, isReset):
+ appId = os.environ.get('APP_ID')
+ with open(path, 'r', encoding='utf-8') as file:
+ contents = []
+ for num, line in enumerate(file):
+ line = line.strip()
+ if "static let AppId" in line:
+ if isReset:
+ line = "static let AppId: String = <#YOUR APPID#>"
+ else:
+ line = f'static let AppId: String = "{appId}"'
+ elif "static let Certificate" in line:
+ if isReset:
+ line = "static let Certificate: String? = <#YOUR Certificate#>"
+ else:
+ line = 'static let Certificate: String? = nil'
+ elif "static NSString * const APPID" in line:
+ if isReset:
+ line = "static NSString * const APPID = <#YOUR APPID#>"
+ else:
+ line = f'static NSString * const APPID = @"{appId}";'
+ elif "static NSString * const Certificate" in line:
+ if isReset:
+ line = "static NSString * const Certificate = <#YOUR Certificate#>"
+ else:
+ line = 'static NSString * const Certificate = nil;'
+ contents.append(line)
+ file.close()
+
+ with open(path, 'w', encoding='utf-8') as fw:
+ for content in contents:
+ if "{" in content or "}" in content:
+ fw.write(content + "\n")
+ else:
+ fw.write('\t'+content + "\n")
+ fw.close()
+
+
+if __name__ == '__main__':
+ print(f'argv === {sys.argv[1:]}')
+ path = sys.argv[1:][0]
+ isReset = eval(sys.argv[1:][1])
+ modfiy(path.strip(), isReset)
diff --git a/.github/ci/build/modify_podfile.py b/.github/ci/build/modify_podfile.py
new file mode 100644
index 000000000..1f63ee796
--- /dev/null
+++ b/.github/ci/build/modify_podfile.py
@@ -0,0 +1,22 @@
+import os, sys
+
+def modfiy(path):
+ with open(path, 'r', encoding='utf-8') as file:
+ contents = []
+ for num, line in enumerate(file):
+ if "pod 'Agora" in line:
+ line = '\t'+"pod 'sdk', :path => '../../sdk.podspec'"
+ elif "pod 'sdk" in line:
+ line = ""
+ contents.append(line)
+ file.close()
+
+ with open(path, 'w', encoding='utf-8') as fw:
+ for content in contents:
+ fw.write(content + "\n")
+ fw.close()
+
+
+if __name__ == '__main__':
+ path = sys.argv[1:][0]
+ modfiy(path.strip())
\ No newline at end of file
diff --git a/.github/workflows/gitee-sync-shell.sh b/.github/workflows/gitee-sync-shell.sh
new file mode 100755
index 000000000..1a7f4f437
--- /dev/null
+++ b/.github/workflows/gitee-sync-shell.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+git config --global user.email "sync2gitee@example.com"
+git config --global user.name "sync2gitee"
+
+pwd
+git remote -v
+
+# change android maven to china repos
+sed -ie "s#google()#maven { url \"https\://maven.aliyun.com/repository/public\" }\n google()#g" Android/APIExample/settings.gradle
+sed -ie "s#https://services.gradle.org/distributions#https://mirrors.cloud.tencent.com/gradle#g" Android/APIExample/gradle/wrapper/gradle-wrapper.properties
+sed -ie "s#google()#maven { url \"https\://maven.aliyun.com/repository/public\" }\n google()#g" Android/APIExample-Audio/settings.gradle
+sed -ie "s#https://services.gradle.org/distributions#https://mirrors.cloud.tencent.com/gradle#g" Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.properties
+git add Android/APIExample/settings.gradle Android/APIExample/gradle/wrapper/gradle-wrapper.properties Android/APIExample-Audio/settings.gradle Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.properties
+git commit -m '[Android] gitee sync >> use china repos.'
+
+git branch
+git status
+git push gitee
+
+
diff --git a/.github/workflows/gitee-sync.yml b/.github/workflows/gitee-sync.yml
new file mode 100644
index 000000000..02c9462fe
--- /dev/null
+++ b/.github/workflows/gitee-sync.yml
@@ -0,0 +1,28 @@
+name: gitee-sync
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ build:
+ name: gitee-sync
+ runs-on: ubuntu-latest
+
+ concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+ if: github.actor != 'dependabot[bot]'
+ steps:
+ - name: Gitee sync repo
+ uses: xgfd3/hub-mirror-action@v1.0
+ with:
+ src: github/AgoraIO
+ dst: gitee/agoraio-community
+ white_list: "API-Examples"
+ static_list: "API-Examples"
+ cache_path: "./cache"
+ dst_key: ${{ secrets.GITEE_PI_SSH }}
+ dst_token: ${{ secrets.GITEE_PRIVATE_TOKEN }}
+ force_update: true
+ account_type: org
+ shell_path: ./.github/workflows/gitee-sync-shell.sh
\ No newline at end of file
diff --git a/Android/APIExample-Audio/.gitignore b/Android/APIExample-Audio/.gitignore
new file mode 100644
index 000000000..b0f139bf0
--- /dev/null
+++ b/Android/APIExample-Audio/.gitignore
@@ -0,0 +1,24 @@
+*.so
+*.iml
+.gradle
+/local.properties
+/.idea
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+androidTest/
+Test/
+
+# sdk files
+*.so
+agora-rtc-sdk.jar
+AgoraScreenShareExtension.aar
+/release
\ No newline at end of file
diff --git a/Android/APIExample-Audio/README.md b/Android/APIExample-Audio/README.md
new file mode 100644
index 000000000..23f880c8a
--- /dev/null
+++ b/Android/APIExample-Audio/README.md
@@ -0,0 +1,52 @@
+# API Example Android
+
+*English | [中文](README.zh.md)*
+
+This project presents you a set of API examples to help you understand how to use Agora APIs.
+
+## Prerequisites
+
+- Android Studio 3.0+
+- Physical Android device
+- Android simulator is supported
+
+## Quick Start
+
+This section shows you how to prepare, build, and run the sample application.
+
+### Obtain an App Id
+
+To build and run the sample application, get an App Id:
+
+1. Create a developer account at [agora.io](https://dashboard.agora.io/signin/). Once you finish the signup process, you will be redirected to the Dashboard.
+2. Navigate in the Dashboard tree on the left to **Projects** > **Project List**.
+3. Save the **App Id** from the Dashboard for later use.
+4. Save the **App Certificate** from the Dashboard for later use.
+5. Generate a temp **Access Token** (valid for 24 hours) from dashboard page with given channel name, save for later use.
+
+6. Open `Android/APIExample` and edit the `app/src/main/res/values/string-config.xml` file. Update `YOUR APP ID` with your App Id, update `YOUR APP CERTIFICATE` with the main app certificate from dashboard, and update `YOUR ACCESS TOKEN` with the temp Access Token generated from dashboard. Note you can leave the token and certificate variable `null` if your project has not turned on security token.
+
+ ```
+ YOUR APP ID
+ // assign token and certificate to null if you have not enabled app certificate
+ YOUR APP CERTIFICATE
+ // assign token and certificate to null if you have not enabled app certificate or you have set the certificate above.
+ YOUR ACCESS TOKEN
+ ```
+
+You are all set. Now connect your Android device and run the project.
+
+
+## Contact Us
+
+- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first
+- Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials
+- Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case
+- Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community)
+- You can find full API documentation at [Document Center](https://docs.agora.io/en/)
+- If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io)
+- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues)
+
+## License
+
+The MIT License (MIT)
diff --git a/Android/APIExample-Audio/README.zh.md b/Android/APIExample-Audio/README.zh.md
new file mode 100644
index 000000000..84cc668ee
--- /dev/null
+++ b/Android/APIExample-Audio/README.zh.md
@@ -0,0 +1,53 @@
+# API Example Android
+
+*[English](README.md) | 中文*
+
+这个开源示例项目演示了Agora视频SDK的部分API使用示例,以帮助开发者更好地理解和运用Agora视频SDK的API。
+
+## 环境准备
+
+- Android Studio 3.0+
+- Android 真机设备
+- 支持模拟器
+
+## 运行示例程序
+
+这个段落主要讲解了如何编译和运行实例程序。
+
+### 创建Agora账号并获取AppId
+
+在编译和启动实例程序前,你需要首先获取一个可用的App Id:
+
+1. 在[agora.io](https://dashboard.agora.io/signin/)创建一个开发者账号
+2. 前往后台页面,点击左部导航栏的 **项目 > 项目列表** 菜单
+3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它
+4. 复制后台的 **App Certificate** 并备注,稍后启动应用时会用到它
+5. 在项目页面生成临时 **Access Token** (24小时内有效)并备注,注意生成的Token只能适用于对应的频道名。
+
+6. 打开 `Android/APIExample` 并编辑 `app/src/main/res/values/string-config.xml`,将你的 AppID 、App主证书、 临时Token 分别替换到 `Your App Id` 、 `YOUR ACCESS TOKEN` 和 `YOUR APP CERTIFICATE`
+
+ ```
+ YOUR APP ID
+ // 如果你没有打开Token功能,certificate可以直接不填
+ YOUR APP CERTIFICATE
+ // 如果你没有打开Token功能或者已经配置了certificate,token可以直接不填
+ YOUR ACCESS TOKEN
+
+ ```
+
+然后你就可以编译并运行项目了。
+
+## 联系我们
+
+- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq)
+- 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO)
+- 如果你想了解声网SDK在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase)
+- 如果你想了解声网的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community)
+- 完整的 API 文档见 [文档中心](https://docs.agora.io/cn/)
+- 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问
+- 如果需要售后技术支持, 你可以在 [Agora Dashboard](https://dashboard.agora.io) 提交工单
+- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/API-Examples/issues)
+
+## 代码许可
+
+The MIT License (MIT)
diff --git a/Android/APIExample-Audio/app/.gitignore b/Android/APIExample-Audio/app/.gitignore
new file mode 100644
index 000000000..da62f148c
--- /dev/null
+++ b/Android/APIExample-Audio/app/.gitignore
@@ -0,0 +1,20 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+gradle
+gradlew
+gradlew.bat
+.externalNativeBuild
+.cxx
+androidTest/
+Test/
diff --git a/Android/APIExample-Audio/app/build.gradle b/Android/APIExample-Audio/app/build.gradle
new file mode 100644
index 000000000..c61ffa95d
--- /dev/null
+++ b/Android/APIExample-Audio/app/build.gradle
@@ -0,0 +1,82 @@
+apply plugin: 'com.android.application'
+
+def localSdkPath= "${rootProject.projectDir.absolutePath}/../../sdk"
+
+android {
+ compileSdkVersion 32
+ buildToolsVersion "32.0.0"
+
+ defaultConfig {
+ applicationId "io.agora.api.example.audio"
+ minSdkVersion 21
+ targetSdkVersion 32
+ versionCode 1
+ versionName "1.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
+ }
+
+ sourceSets {
+ main {
+ jniLibs.srcDirs += 'src/main/jniLibs'
+ if(new File("${localSdkPath}").exists()){
+ jniLibs.srcDirs += "${localSdkPath}"
+ }
+ }
+ }
+
+ buildFeatures{
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
+ if(new File("${localSdkPath}").exists()){
+ implementation fileTree(dir: "${localSdkPath}", include: ['*.jar', '*.aar'])
+ }
+ else{
+ def agora_sdk_version = "4.3.0"
+ // case 1: full single lib with voice only
+ implementation "io.agora.rtc:voice-sdk:${agora_sdk_version}"
+ // case 2: partial libs with voice only
+ // implementation "io.agora.rtc:voice-rtc-basic:${agora_sdk_version}"
+ // implementation "io.agora.rtc:spatial-audio:${agora_sdk_version}"
+ // implementation "io.agora.rtc:audio-beauty:${agora_sdk_version}"
+ // implementation "io.agora.rtc:aiaec:${agora_sdk_version}"
+ // implementation "io.agora.rtc:drm-loader:${agora_sdk_version}"
+ // implementation "io.agora.rtc:drm:${agora_sdk_version}"
+ }
+
+ implementation 'androidx.appcompat:appcompat:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+
+ // Java language implementation
+ implementation "androidx.navigation:navigation-fragment:2.5.0"
+ implementation "androidx.navigation:navigation-ui:2.5.0"
+
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation 'io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:1.2.0'
+ implementation 'com.yanzhenjie:permission:2.0.3'
+ implementation 'de.javagl:obj:0.2.1'
+
+ implementation "com.squareup.okhttp3:okhttp:4.10.0"
+ implementation "com.squareup.okhttp3:logging-interceptor:4.10.0"
+}
diff --git a/Android/APIExample-Audio/app/proguard-rules.pro b/Android/APIExample-Audio/app/proguard-rules.pro
new file mode 100644
index 000000000..f7a3f52f1
--- /dev/null
+++ b/Android/APIExample-Audio/app/proguard-rules.pro
@@ -0,0 +1,25 @@
+# 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
+
+-keep class io.agora.**{*;}
+-dontwarn javax.**
+-dontwarn com.google.devtools.build.android.**
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/AndroidManifest.xml b/Android/APIExample-Audio/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..e663d98a8
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/assets/agora-logo.png b/Android/APIExample-Audio/app/src/main/assets/agora-logo.png
new file mode 100644
index 000000000..0b93e8b9f
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/assets/agora-logo.png differ
diff --git a/Android/APIExample-Audio/app/src/main/assets/effectA.wav b/Android/APIExample-Audio/app/src/main/assets/effectA.wav
new file mode 100644
index 000000000..dc31fdb68
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/assets/effectA.wav differ
diff --git a/Android/APIExample-Audio/app/src/main/assets/music_1.m4a b/Android/APIExample-Audio/app/src/main/assets/music_1.m4a
new file mode 100644
index 000000000..3fb0b5ba5
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/assets/music_1.m4a differ
diff --git a/Android/APIExample-Audio/app/src/main/assets/output.raw b/Android/APIExample-Audio/app/src/main/assets/output.raw
new file mode 100644
index 000000000..e252ed263
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/assets/output.raw differ
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainActivity.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainActivity.java
new file mode 100644
index 000000000..48889e115
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainActivity.java
@@ -0,0 +1,47 @@
+package io.agora.api.example;
+
+import android.os.Bundle;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.Constant;
+import io.agora.api.example.common.model.ExampleBean;
+
+/**
+ * @author cjw
+ */
+public class MainActivity extends AppCompatActivity implements MainFragment.OnListFragmentInteractionListener {
+ private AppBarConfiguration appBarConfiguration;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
+ appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
+ NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
+ return NavigationUI.navigateUp(navController, appBarConfiguration)
+ || super.onSupportNavigateUp();
+ }
+
+ @Override
+ public void onListFragmentInteraction(Example item) {
+ ExampleBean exampleBean = new ExampleBean(item.index(), item.group(), item.name(), item.actionId(), item.tipsId());
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(Constant.DATA, exampleBean);
+ Navigation.findNavController(this, R.id.nav_host_fragment)
+ .navigate(R.id.action_mainFragment_to_Ready, bundle);
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainApplication.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainApplication.java
new file mode 100644
index 000000000..0a057c895
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainApplication.java
@@ -0,0 +1,49 @@
+package io.agora.api.example;
+
+import android.app.Application;
+
+import java.lang.annotation.Annotation;
+import java.util.Set;
+
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.model.Examples;
+import io.agora.api.example.common.model.GlobalSettings;
+import io.agora.api.example.utils.ClassUtils;
+
+public class MainApplication extends Application {
+
+ private GlobalSettings globalSettings;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ initExamples();
+ }
+
+ private void initExamples() {
+ try {
+ Set packageName = ClassUtils.getFileNameByPackageName(this, "io.agora.api.example.examples");
+ for (String name : packageName) {
+ Class> aClass = Class.forName(name);
+ Annotation[] declaredAnnotations = aClass.getAnnotations();
+ for (Annotation annotation : declaredAnnotations) {
+ if (annotation instanceof Example) {
+ Example example = (Example) annotation;
+ Examples.addItem(example);
+ }
+ }
+ }
+ Examples.sortItem();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public GlobalSettings getGlobalSettings() {
+ if(globalSettings == null){
+ globalSettings = new GlobalSettings();
+ }
+ return globalSettings;
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainFragment.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainFragment.java
new file mode 100644
index 000000000..3b843ef71
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/MainFragment.java
@@ -0,0 +1,137 @@
+package io.agora.api.example;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+import static io.agora.api.example.common.model.Examples.BASIC;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.adapter.ExampleSection;
+import io.agora.api.example.common.model.Examples;
+import io.github.luizgrp.sectionedrecyclerviewadapter.SectionedRecyclerViewAdapter;
+
+/**
+ * A fragment representing a list of Items.
+ *
+ * Activities containing this fragment MUST implement the {@link OnListFragmentInteractionListener}
+ * interface.
+ */
+public class MainFragment extends Fragment {
+ // TODO: Customize parameter argument names
+ private static final String ARG_COLUMN_COUNT = "column-count";
+ // TODO: Customize parameters
+ private int mColumnCount = 1;
+ private OnListFragmentInteractionListener mListener;
+
+ /**
+ * Mandatory empty constructor for the fragment manager to instantiate the
+ * fragment (e.g. upon screen orientation changes).
+ */
+ public MainFragment() {
+ }
+
+ // TODO: Customize parameter initialization
+ @SuppressWarnings("unused")
+ public static MainFragment newInstance(int columnCount) {
+ MainFragment fragment = new MainFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_COLUMN_COUNT, columnCount);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getArguments() != null) {
+ mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT);
+ }
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_main, container, false);
+
+ // Set the adapter
+ if (view instanceof RecyclerView) {
+ Context context = view.getContext();
+ RecyclerView recyclerView = (RecyclerView) view;
+ if (mColumnCount <= 1) {
+ recyclerView.setLayoutManager(new LinearLayoutManager(context));
+ } else {
+ recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+ }
+ SectionedRecyclerViewAdapter sectionedAdapter = new SectionedRecyclerViewAdapter();
+ sectionedAdapter.addSection(new ExampleSection(BASIC, Examples.ITEM_MAP.get(BASIC), mListener));
+ sectionedAdapter.addSection(new ExampleSection(ADVANCED, Examples.ITEM_MAP.get(ADVANCED), mListener));
+ recyclerView.setAdapter(sectionedAdapter);
+ }
+ return view;
+ }
+
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof OnListFragmentInteractionListener) {
+ mListener = (OnListFragmentInteractionListener) context;
+ } else {
+ throw new RuntimeException(context.toString()
+ + " must implement OnListFragmentInteractionListener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mListener = null;
+ }
+
+ /**
+ * This interface must be implemented by activities that contain this
+ * fragment to allow an interaction in this fragment to be communicated
+ * to the activity and potentially other fragments contained in that
+ * activity.
+ *
+ * See the Android Training lesson Communicating with Other Fragments for more information.
+ */
+ public interface OnListFragmentInteractionListener {
+ // TODO: Update argument type and name
+ void onListFragmentInteraction(Example item);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.menu_main_activity, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.setting) {
+ startActivity(new Intent(getContext(), SettingActivity.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/ReadyFragment.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/ReadyFragment.java
new file mode 100644
index 000000000..f183c3616
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/ReadyFragment.java
@@ -0,0 +1,121 @@
+package io.agora.api.example;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
+import androidx.navigation.Navigation;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.Constant;
+import io.agora.api.example.common.model.ExampleBean;
+
+/**
+ * @author cjw
+ */
+public class ReadyFragment extends BaseFragment {
+ private static final String TAG = ReadyFragment.class.getSimpleName();
+
+ private AppCompatTextView tips;
+ private ExampleBean exampleBean;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ exampleBean = getArguments().getParcelable(Constant.DATA);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_ready_layout, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if(actionBar != null){
+ actionBar.setTitle(exampleBean.getName());
+ actionBar.setHomeButtonEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ tips = view.findViewById(R.id.tips);
+ tips.setText(getString(exampleBean.getTipsId()));
+ view.findViewById(R.id.next).setOnClickListener(v -> {
+ runOnPermissionGranted(new Runnable() {
+ @Override
+ public void run() {
+ NavController navController = Navigation.findNavController(requireView());
+ navController.navigate(exampleBean.getActionId());
+ navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
+ @Override
+ public void onDestinationChanged(@NonNull NavController controller,
+ @NonNull NavDestination destination,
+ @Nullable Bundle arguments) {
+ if (destination.getId() == R.id.Ready) {
+ controller.navigateUp();
+ controller.removeOnDestinationChangedListener(this);
+ }
+ }
+ });
+ }
+ });
+ });
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(@NonNull Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ menu.setGroupVisible(R.id.main_setting_group, false);
+ }
+
+ @SuppressLint("WrongConstant")
+ private void runOnPermissionGranted(@NonNull Runnable runnable) {
+ List permissionList = new ArrayList<>();
+ permissionList.add(Permission.READ_EXTERNAL_STORAGE);
+ permissionList.add(Permission.WRITE_EXTERNAL_STORAGE);
+ permissionList.add(Permission.RECORD_AUDIO);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ permissionList.add(Manifest.permission.READ_PHONE_STATE);
+ permissionList.add(Manifest.permission.BLUETOOTH_CONNECT);
+ }
+
+ String[] permissionArray = new String[permissionList.size()];
+ permissionList.toArray(permissionArray);
+
+ if (AndPermission.hasPermissions(this, permissionArray)) {
+ runnable.run();
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ permissionArray
+ ).onGranted(permissions ->
+ {
+ // Permissions Granted
+ runnable.run();
+ }).start();
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/SettingActivity.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/SettingActivity.java
new file mode 100644
index 000000000..42dd12ef4
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/SettingActivity.java
@@ -0,0 +1,84 @@
+package io.agora.api.example;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import io.agora.api.example.common.model.GlobalSettings;
+import io.agora.api.example.databinding.ActivitySettingLayoutBinding;
+import io.agora.rtc2.RtcEngine;
+
+/**
+ * @author cjw
+ */
+public class SettingActivity extends AppCompatActivity{
+ private static final String TAG = SettingActivity.class.getSimpleName();
+
+ private ActivitySettingLayoutBinding mBinding;
+ private MenuItem saveMenu;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mBinding = ActivitySettingLayoutBinding.inflate(LayoutInflater.from(this));
+ setContentView(mBinding.getRoot());
+ mBinding.sdkVersion.setText(String.format(getString(R.string.sdkversion1), RtcEngine.getSdkVersion()));
+ fetchGlobalSettings();
+ }
+
+ private void fetchGlobalSettings(){
+ GlobalSettings globalSettings = ((MainApplication)getApplication()).getGlobalSettings();
+
+ String[] mItems = getResources().getStringArray(R.array.areaCode);
+ String selectedItem = globalSettings.getAreaCodeStr();
+ int i = 0;
+ if(selectedItem!=null){
+ for(String item : mItems){
+ if(selectedItem.equals(item)){
+ break;
+ }
+ i++;
+ }
+ }
+ mBinding.areaSpinner.setSelection(i);
+
+
+ mBinding.privateCloudLayout.etIpAddress.setText(globalSettings.privateCloudIp);
+ mBinding.privateCloudLayout.swLogReport.setChecked(globalSettings.privateCloudLogReportEnable);
+ mBinding.privateCloudLayout.etLogServerDomain.setText(globalSettings.privateCloudLogServerDomain);
+ mBinding.privateCloudLayout.etLogServerPort.setText(globalSettings.privateCloudLogServerPort + "");
+ mBinding.privateCloudLayout.etLogServerPath.setText(globalSettings.privateCloudLogServerPath);
+ mBinding.privateCloudLayout.swUseHttps.setChecked(globalSettings.privateCloudUseHttps);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(@NonNull Menu menu) {
+ saveMenu = menu.add(R.string.save);
+ saveMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == saveMenu.getItemId()) {
+ GlobalSettings globalSettings = ((MainApplication)getApplication()).getGlobalSettings();
+ globalSettings.privateCloudIp = mBinding.privateCloudLayout.etIpAddress.getText().toString();
+ globalSettings.privateCloudLogReportEnable = mBinding.privateCloudLayout.swLogReport.isChecked();
+ globalSettings.privateCloudLogServerDomain = mBinding.privateCloudLayout.etLogServerDomain.getText().toString();
+ globalSettings.privateCloudLogServerPort = Integer.parseInt(mBinding.privateCloudLayout.etLogServerPort.getText().toString());
+ globalSettings.privateCloudLogServerPath = mBinding.privateCloudLayout.etLogServerPath.getText().toString();
+ globalSettings.privateCloudUseHttps = mBinding.privateCloudLayout.swUseHttps.isChecked();
+
+ globalSettings.setAreaCodeStr(getResources().getStringArray(R.array.areaCode)[mBinding.areaSpinner.getSelectedItemPosition()]);
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/annotation/Example.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/annotation/Example.java
new file mode 100644
index 000000000..1e790bb1d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/annotation/Example.java
@@ -0,0 +1,37 @@
+package io.agora.api.example.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Example {
+ /**
+ * @return example index
+ */
+ int index();
+ /**
+ * @return group name
+ */
+ String group() default "";
+
+ /**
+ * @return example name
+ */
+ int name();
+
+ /**
+ * @return action ID
+ */
+ int actionId();
+
+ /**
+ * @return tips ID
+ * */
+ int tipsId();
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java
new file mode 100644
index 000000000..3dde5fc35
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java
@@ -0,0 +1,174 @@
+package io.agora.api.example.common;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Toast;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.navigation.Navigation;
+
+/**
+ * The type Base fragment.
+ */
+public class BaseFragment extends Fragment {
+ /**
+ * The Handler.
+ */
+ protected Handler handler;
+ private AlertDialog mAlertDialog;
+ private String mAlertMessage;
+ private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
+ @Override
+ public void handleOnBackPressed() {
+ onBackPressed();
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler(Looper.getMainLooper());
+ requireActivity().getOnBackPressedDispatcher().addCallback(onBackPressedCallback);
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ onBackPressedCallback.setEnabled(true);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ onBackPressedCallback.setEnabled(false);
+ }
+
+ /**
+ * Show alert.
+ *
+ * @param message the message
+ */
+ protected void showAlert(String message) {
+ this.showAlert(message, true);
+ }
+
+ /**
+ * Show alert.
+ *
+ * @param message the message
+ * @param showRepeatMsg the show repeat msg
+ */
+ protected void showAlert(String message, boolean showRepeatMsg) {
+ runOnUIThread(() -> {
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ if (mAlertDialog == null) {
+ mAlertDialog = new AlertDialog.Builder(context).setTitle("Tips")
+ .setPositiveButton("OK", (dialog, which) -> dialog.dismiss())
+ .create();
+ }
+ if (!showRepeatMsg && !TextUtils.isEmpty(mAlertMessage) && mAlertMessage.equals(message)) {
+ return;
+ }
+ mAlertMessage = message;
+ mAlertDialog.setMessage(message);
+ mAlertDialog.show();
+ });
+ }
+
+ /**
+ * Reset alert.
+ */
+ protected void resetAlert() {
+ runOnUIThread(() -> mAlertMessage = "");
+ }
+
+ /**
+ * Show long toast.
+ *
+ * @param msg the msg
+ */
+ protected final void showLongToast(final String msg) {
+ runOnUIThread(() -> {
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
+ });
+ }
+
+ /**
+ * Show short toast.
+ *
+ * @param msg the msg
+ */
+ protected final void showShortToast(final String msg) {
+ runOnUIThread(() -> {
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ /**
+ * Run on ui thread.
+ *
+ * @param runnable the runnable
+ */
+ protected final void runOnUIThread(Runnable runnable) {
+ this.runOnUIThread(runnable, 0);
+ }
+
+ /**
+ * Run on ui thread.
+ *
+ * @param runnable the runnable
+ * @param delay the delay
+ */
+ protected final void runOnUIThread(Runnable runnable, long delay) {
+ if (handler != null && runnable != null && getContext() != null) {
+ if (delay <= 0 && handler.getLooper().getThread() == Thread.currentThread()) {
+ runnable.run();
+ } else {
+ handler.postDelayed(() -> {
+ if (getContext() != null) {
+ runnable.run();
+ }
+ }, delay);
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ handler.removeCallbacksAndMessages(null);
+ if (mAlertDialog != null) {
+ mAlertDialog.dismiss();
+ mAlertDialog = null;
+ }
+ }
+
+ /**
+ * On back pressed.
+ */
+ protected void onBackPressed() {
+ View view = getView();
+ if (view != null) {
+ Navigation.findNavController(view).navigateUp();
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/Constant.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/Constant.java
new file mode 100644
index 000000000..5fb4c2f4f
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/Constant.java
@@ -0,0 +1,30 @@
+package io.agora.api.example.common;
+
+import android.view.TextureView;
+
+import io.agora.rtc2.RtcEngine;
+
+public class Constant {
+ public static TextureView TEXTUREVIEW;
+
+ public static RtcEngine ENGINE;
+
+ public static String TIPS = "tips";
+
+ public static String DATA = "data";
+
+ public static final String MIX_FILE_PATH = "/assets/music_1.m4a";
+
+ public static final String EFFECT_FILE_PATH = "https://webdemo.agora.io/ding.mp3";
+
+ public static final String WATER_MARK_FILE_PATH = "/assets/agora-logo.png";
+
+ public static final String URL_PLAY_AUDIO_FILES_MULTI_TRACK = "https://webdemo.agora.io/mTrack.m4a";
+
+ public static final String URL_PLAY_AUDIO_FILES = "https://webdemo.agora.io/audiomixing.mp3";
+
+ public static final String URL_UPBEAT = "https://webdemo.agora.io/ding.mp3";
+
+ public static final String URL_DOWNBEAT = "https://webdemo.agora.io/dang.mp3";
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/adapter/ExampleSection.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/adapter/ExampleSection.java
new file mode 100644
index 000000000..31505545d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/adapter/ExampleSection.java
@@ -0,0 +1,79 @@
+package io.agora.api.example.common.adapter;
+
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.List;
+
+import io.agora.api.example.MainFragment;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.github.luizgrp.sectionedrecyclerviewadapter.Section;
+import io.github.luizgrp.sectionedrecyclerviewadapter.SectionParameters;
+
+public class ExampleSection extends Section {
+ private final String mTitle;
+ private final List mValues;
+ private final MainFragment.OnListFragmentInteractionListener mListener;
+
+ public ExampleSection(String title, List items, MainFragment.OnListFragmentInteractionListener listener) {
+ super(SectionParameters.builder().headerResourceId(R.layout.layout_main_list_section).itemResourceId(R.layout.layout_main_list_item).build());
+ mTitle = title;
+ mValues = items;
+ mListener = listener;
+ }
+
+ @Override
+ public int getContentItemsTotal() {
+ return mValues.size();
+ }
+
+ @Override
+ public RecyclerView.ViewHolder getItemViewHolder(View view) {
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindItemViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ if (viewHolder instanceof ViewHolder) {
+ ViewHolder holder = (ViewHolder) viewHolder;
+ holder.mItem = mValues.get(position);
+ holder.mNameView.setText(holder.mView.getContext().getString(holder.mItem.name()));
+
+ holder.mView.setOnClickListener(v -> {
+ if (null != mListener) {
+ // Notify the active callbacks interface (the activity, if the
+ // fragment is attached to one) that an item has been selected.
+ mListener.onListFragmentInteraction(holder.mItem);
+ }
+ });
+ }
+ }
+
+ @Override
+ public RecyclerView.ViewHolder getHeaderViewHolder(View view) {
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder) {
+ if (viewHolder instanceof ViewHolder) {
+ ViewHolder holder = (ViewHolder) viewHolder;
+ holder.mNameView.setText(mTitle);
+ }
+ }
+
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ final View mView;
+ final TextView mNameView;
+ Example mItem;
+
+ ViewHolder(View view) {
+ super(view);
+ mView = view;
+ mNameView = view.findViewById(R.id.item_name);
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/Drawable2dFull.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/Drawable2dFull.java
new file mode 100644
index 000000000..8db5cb290
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/Drawable2dFull.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles;
+
+
+import io.agora.api.example.common.gles.core.Drawable2d;
+
+/**
+ * Base class for stuff we like to draw.
+ */
+public class Drawable2dFull extends Drawable2d {
+
+ /**
+ * A "full" square, extending from -1 to +1 in both dimensions. When the model/view/projection
+ * matrix is identity, this will exactly cover the viewport.
+ *
+ * The texture coordinates are Y-inverted relative to RECTANGLE. (This seems to work out
+ * right with external textures from SurfaceTexture.)
+ */
+ private static final float FULL_RECTANGLE_COORDS[] = {
+ -1.0f, -1.0f, // 0 bottom left
+ 1.0f, -1.0f, // 1 bottom right
+ -1.0f, 1.0f, // 2 top left
+ 1.0f, 1.0f, // 3 top right
+ };
+ private static final float FULL_RECTANGLE_TEX_COORDS[] = {
+ 0.0f, 0.0f, // 0 bottom left
+ 1.0f, 0.0f, // 1 bottom right
+ 0.0f, 1.0f, // 2 top left
+ 1.0f, 1.0f // 3 top right
+ };
+
+ public Drawable2dFull() {
+ super(FULL_RECTANGLE_COORDS, FULL_RECTANGLE_TEX_COORDS);
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/Drawable2dLandmarks.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/Drawable2dLandmarks.java
new file mode 100644
index 000000000..29d798b12
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/Drawable2dLandmarks.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles;
+
+
+import io.agora.api.example.common.gles.core.Drawable2d;
+
+/**
+ * Base class for stuff we like to draw.
+ */
+public class Drawable2dLandmarks extends Drawable2d {
+
+
+ private float pointsCoords[] = new float[150];
+
+ public Drawable2dLandmarks() {
+ updateVertexArray(pointsCoords);
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramLandmarks.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramLandmarks.java
new file mode 100644
index 000000000..541d6e3c2
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramLandmarks.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agora.api.example.common.gles;
+
+import android.hardware.Camera;
+import android.opengl.GLES20;
+import android.opengl.Matrix;
+
+import java.util.Arrays;
+
+import io.agora.api.example.common.gles.core.Drawable2d;
+import io.agora.api.example.common.gles.core.GlUtil;
+import io.agora.api.example.common.gles.core.Program;
+
+
+public class ProgramLandmarks extends Program {
+
+ private static final String vertexShaderCode =
+ // This matrix member variable provides a hook to manipulate
+ // the coordinates of the objects that use this vertex shader
+ "uniform mat4 uMVPMatrix;" +
+ "attribute vec4 vPosition;" +
+ "uniform float uPointSize;" +
+ "void main() {" +
+ // the matrix must be included as a modifier of gl_Position
+ // Note that the uMVPMatrix factor *must be first* in order
+ // for the matrix multiplication product to be correct.
+ " gl_Position = uMVPMatrix * vPosition;" +
+ " gl_PointSize = uPointSize;" +
+ "}";
+
+ private static final String fragmentShaderCode =
+ "precision mediump float;" +
+ "uniform vec4 vColor;" +
+ "void main() {" +
+ " gl_FragColor = vColor;" +
+ "}";
+
+ private static final float color[] = {0.63671875f, 0.76953125f, 0.22265625f, 1.0f};
+
+ private int mPositionHandle;
+ private int mColorHandle;
+ private int mMVPMatrixHandle;
+ private int mPointSizeHandle;
+
+ private float mPointSize = 6.0f;
+
+ public ProgramLandmarks() {
+ super(vertexShaderCode, fragmentShaderCode);
+ }
+
+ @Override
+ protected Drawable2d getDrawable2d() {
+ return new Drawable2dLandmarks();
+ }
+
+ @Override
+ protected void getLocations() {
+ // get handle to vertex shader's vPosition member
+ mPositionHandle = GLES20.glGetAttribLocation(mProgramHandle, "vPosition");
+ GlUtil.checkGlError("vPosition");
+ // get handle to fragment shader's vColor member
+ mColorHandle = GLES20.glGetUniformLocation(mProgramHandle, "vColor");
+ GlUtil.checkGlError("vColor");
+ // get handle to shape's transformation matrix
+ mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix");
+ GlUtil.checkGlError("glGetUniformLocation");
+ mPointSizeHandle = GLES20.glGetUniformLocation(mProgramHandle, "uPointSize");
+ GlUtil.checkGlError("uPointSize");
+ }
+
+ @Override
+ public void drawFrame(int textureId, float[] texMatrix, float[] mvpMatrix) {
+ // Add program to OpenGL environment
+ GLES20.glUseProgram(mProgramHandle);
+
+ // Enable a handle to the triangle vertices
+ GLES20.glEnableVertexAttribArray(mPositionHandle);
+
+ // Prepare the triangle coordinate data
+ GLES20.glVertexAttribPointer(
+ mPositionHandle, Drawable2d.COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT, false,
+ Drawable2d.VERTEXTURE_STRIDE, mDrawable2d.vertexArray());
+
+ // Set color for drawing the triangle
+ GLES20.glUniform4fv(mColorHandle, 1, color, 0);
+
+ // Apply the projection and view transformation
+ GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMtx, 0);
+
+ GLES20.glUniform1f(mPointSizeHandle, mPointSize);
+
+ // Draw the triangle
+ GLES20.glDrawArrays(GLES20.GL_POINTS, 0, mDrawable2d.vertexCount());
+
+ // Disable vertex array
+ GLES20.glDisableVertexAttribArray(mPositionHandle);
+ }
+
+ public void drawFrame(int x, int y, int width, int height) {
+ drawFrame(0, null, null, x, y, width, height);
+ }
+
+ private final float[] mvpMtx = new float[16];
+ private int mCameraType;
+ private int mOrientation;
+ private int mWidth;
+ private int mHeight;
+
+ public void refresh(float[] landmarksData, int width, int height, int orientation, int cameraType) {
+ if (mWidth != width || mHeight != height || mOrientation != orientation || mCameraType != cameraType) {
+ float[] orthoMtx = new float[16];
+ float[] rotateMtx = new float[16];
+ Matrix.orthoM(orthoMtx, 0, 0, width, 0, height, -1, 1);
+ Matrix.setRotateM(rotateMtx, 0, 360 - orientation, 0.0f, 0.0f, 1.0f);
+ if (cameraType == Camera.CameraInfo.CAMERA_FACING_BACK) {
+ Matrix.rotateM(rotateMtx, 0, 180, 1.0f, 0.0f, 0.0f);
+ }
+ Matrix.multiplyMM(mvpMtx, 0, rotateMtx, 0, orthoMtx, 0);
+
+ mWidth = width;
+ mHeight = height;
+ mOrientation = orientation;
+ mCameraType = cameraType;
+ }
+
+ updateVertexArray(Arrays.copyOf(landmarksData, landmarksData.length));
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramTexture2d.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramTexture2d.java
new file mode 100644
index 000000000..73221726b
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramTexture2d.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles;
+
+import android.opengl.GLES20;
+
+import io.agora.api.example.common.gles.core.Drawable2d;
+import io.agora.api.example.common.gles.core.GlUtil;
+import io.agora.api.example.common.gles.core.Program;
+
+
+public class ProgramTexture2d extends Program {
+
+ // Simple vertex shader, used for all programs.
+ private static final String VERTEX_SHADER =
+ "uniform mat4 uMVPMatrix;\n" +
+ "uniform mat4 uTexMatrix;\n" +
+ "attribute vec4 aPosition;\n" +
+ "attribute vec4 aTextureCoord;\n" +
+ "varying vec2 vTextureCoord;\n" +
+ "void main() {\n" +
+ " gl_Position = uMVPMatrix * aPosition;\n" +
+ " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
+ "}\n";
+
+ // Simple fragment shader for use with "normal" 2D textures.
+ private static final String FRAGMENT_SHADER_2D =
+ "precision mediump float;\n" +
+ "varying vec2 vTextureCoord;\n" +
+ "uniform sampler2D sTexture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = vec4(texture2D(sTexture, vTextureCoord).rgb, 1.0);\n" +
+ "}\n";
+
+ private int muMVPMatrixLoc;
+ private int muTexMatrixLoc;
+ private int maPositionLoc;
+ private int maTextureCoordLoc;
+
+ public ProgramTexture2d() {
+ super(VERTEX_SHADER, FRAGMENT_SHADER_2D);
+ }
+
+ @Override
+ protected Drawable2d getDrawable2d() {
+ return new Drawable2dFull();
+ }
+
+ @Override
+ protected void getLocations() {
+ maPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition");
+ GlUtil.checkLocation(maPositionLoc, "aPosition");
+ maTextureCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "aTextureCoord");
+ GlUtil.checkLocation(maTextureCoordLoc, "aTextureCoord");
+ muMVPMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix");
+ GlUtil.checkLocation(muMVPMatrixLoc, "uMVPMatrix");
+ muTexMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexMatrix");
+ GlUtil.checkLocation(muTexMatrixLoc, "uTexMatrix");
+ }
+
+ @Override
+ public void drawFrame(int textureId, float[] texMatrix, float[] mvpMatrix) {
+ GlUtil.checkGlError("draw start");
+
+ // Select the program.
+ GLES20.glUseProgram(mProgramHandle);
+ GlUtil.checkGlError("glUseProgram");
+
+ // Set the texture.
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
+
+ // Copy the model / view / projection matrix over.
+ GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mvpMatrix, 0);
+ GlUtil.checkGlError("glUniformMatrix4fv");
+
+ // Copy the texture transformation matrix over.
+ GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, texMatrix, 0);
+ GlUtil.checkGlError("glUniformMatrix4fv");
+
+ // Enable the "aPosition" vertex attribute.
+ GLES20.glEnableVertexAttribArray(maPositionLoc);
+ GlUtil.checkGlError("glEnableVertexAttribArray");
+
+ // Connect vertexBuffer to "aPosition".
+ GLES20.glVertexAttribPointer(maPositionLoc, Drawable2d.COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT, false, Drawable2d.VERTEXTURE_STRIDE, mDrawable2d.vertexArray());
+ GlUtil.checkGlError("glVertexAttribPointer");
+
+ // Enable the "aTextureCoord" vertex attribute.
+ GLES20.glEnableVertexAttribArray(maTextureCoordLoc);
+ GlUtil.checkGlError("glEnableVertexAttribArray");
+
+ // Connect texBuffer to "aTextureCoord".
+ GLES20.glVertexAttribPointer(maTextureCoordLoc, 2,
+ GLES20.GL_FLOAT, false, Drawable2d.TEXTURE_COORD_STRIDE, mDrawable2d.texCoordArray());
+ GlUtil.checkGlError("glVertexAttribPointer");
+
+ // Draw the rect.
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mDrawable2d.vertexCount());
+ GlUtil.checkGlError("glDrawArrays");
+
+ // Done -- disable vertex array, texture, and program.
+ GLES20.glDisableVertexAttribArray(maPositionLoc);
+ GLES20.glDisableVertexAttribArray(maTextureCoordLoc);
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
+ GLES20.glUseProgram(0);
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramTextureOES.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramTextureOES.java
new file mode 100644
index 000000000..9eaff0787
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/ProgramTextureOES.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles;
+
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+
+import io.agora.api.example.common.gles.core.Drawable2d;
+import io.agora.api.example.common.gles.core.GlUtil;
+import io.agora.api.example.common.gles.core.Program;
+
+
+public class ProgramTextureOES extends Program {
+
+ // Simple vertex shader, used for all programs.
+ private static final String VERTEX_SHADER =
+ "uniform mat4 uMVPMatrix;\n" +
+ "uniform mat4 uTexMatrix;\n" +
+ "attribute vec4 aPosition;\n" +
+ "attribute vec4 aTextureCoord;\n" +
+ "varying vec2 vTextureCoord;\n" +
+ "void main() {\n" +
+ " gl_Position = uMVPMatrix * aPosition;\n" +
+ " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
+ "}\n";
+
+ // Simple fragment shader for use with external 2D textures (e.g. what we get from
+ // SurfaceTexture).
+ private static final String FRAGMENT_SHADER_EXT =
+ "#extension GL_OES_EGL_image_external : require\n" +
+ "precision mediump float;\n" +
+ "varying vec2 vTextureCoord;\n" +
+ "uniform samplerExternalOES sTexture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
+ "}\n";
+
+ private int muMVPMatrixLoc;
+ private int muTexMatrixLoc;
+ private int maPositionLoc;
+ private int maTextureCoordLoc;
+
+ /**
+ * Prepares the program in the current EGL context.
+ */
+ public ProgramTextureOES() {
+ super(VERTEX_SHADER, FRAGMENT_SHADER_EXT);
+ }
+
+ @Override
+ protected Drawable2d getDrawable2d() {
+ return new Drawable2dFull();
+ }
+
+ @Override
+ protected void getLocations() {
+ maPositionLoc = GLES20.glGetAttribLocation(mProgramHandle, "aPosition");
+ GlUtil.checkLocation(maPositionLoc, "aPosition");
+ maTextureCoordLoc = GLES20.glGetAttribLocation(mProgramHandle, "aTextureCoord");
+ GlUtil.checkLocation(maTextureCoordLoc, "aTextureCoord");
+ muMVPMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uMVPMatrix");
+ GlUtil.checkLocation(muMVPMatrixLoc, "uMVPMatrix");
+ muTexMatrixLoc = GLES20.glGetUniformLocation(mProgramHandle, "uTexMatrix");
+ GlUtil.checkLocation(muTexMatrixLoc, "uTexMatrix");
+ }
+
+ @Override
+ public void drawFrame(int textureId, float[] texMatrix, float[] mvpMatrix) {
+ GlUtil.checkGlError("draw start");
+
+ // Select the program.
+ GLES20.glUseProgram(mProgramHandle);
+ GlUtil.checkGlError("glUseProgram");
+
+ // Set the texture.
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
+
+ // Copy the model / view / projection matrix over.
+ GLES20.glUniformMatrix4fv(muMVPMatrixLoc, 1, false, mvpMatrix, 0);
+ GlUtil.checkGlError("glUniformMatrix4fv");
+
+ // Copy the texture transformation matrix over.
+ GLES20.glUniformMatrix4fv(muTexMatrixLoc, 1, false, texMatrix, 0);
+ GlUtil.checkGlError("glUniformMatrix4fv");
+
+ // Enable the "aPosition" vertex attribute.
+ GLES20.glEnableVertexAttribArray(maPositionLoc);
+ GlUtil.checkGlError("glEnableVertexAttribArray");
+
+ // Connect vertexBuffer to "aPosition".
+ GLES20.glVertexAttribPointer(maPositionLoc, Drawable2d.COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT, false, Drawable2d.VERTEXTURE_STRIDE, mDrawable2d.vertexArray());
+ GlUtil.checkGlError("glVertexAttribPointer");
+
+ // Enable the "aTextureCoord" vertex attribute.
+ GLES20.glEnableVertexAttribArray(maTextureCoordLoc);
+ GlUtil.checkGlError("glEnableVertexAttribArray");
+
+ // Connect texBuffer to "aTextureCoord".
+ GLES20.glVertexAttribPointer(maTextureCoordLoc, 2,
+ GLES20.GL_FLOAT, false, Drawable2d.TEXTURE_COORD_STRIDE, mDrawable2d.texCoordArray());
+ GlUtil.checkGlError("glVertexAttribPointer");
+
+ // Draw the rect.
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mDrawable2d.vertexCount());
+ GlUtil.checkGlError("glDrawArrays");
+
+ // Done -- disable vertex array, texture, and program.
+ GLES20.glDisableVertexAttribArray(maPositionLoc);
+ GLES20.glDisableVertexAttribArray(maTextureCoordLoc);
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
+ GLES20.glUseProgram(0);
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Drawable2d.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Drawable2d.java
new file mode 100644
index 000000000..c3d2abe68
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Drawable2d.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles.core;
+
+import java.nio.FloatBuffer;
+
+/**
+ * Base class for stuff we like to draw.
+ */
+public class Drawable2d {
+
+ public static final int SIZEOF_FLOAT = 4;
+ public static final int COORDS_PER_VERTEX = 2;
+ public static final int TEXTURE_COORD_STRIDE = 2 * SIZEOF_FLOAT;
+ public static final int VERTEXTURE_STRIDE = COORDS_PER_VERTEX * SIZEOF_FLOAT;
+
+ private FloatBuffer mTexCoordArray;
+ private FloatBuffer mVertexArray;
+ private int mVertexCount;
+
+ public Drawable2d() {
+ }
+
+ /**
+ * Prepares a drawable from a "pre-fabricated" shape definition.
+ *
+ * Does no EGL/GL operations, so this can be done at any time.
+ */
+ public Drawable2d(float[] FULL_RECTANGLE_COORDS, float[] FULL_RECTANGLE_TEX_COORDS) {
+ updateVertexArray(FULL_RECTANGLE_COORDS);
+ updateTexCoordArray(FULL_RECTANGLE_TEX_COORDS);
+ }
+
+ public void updateVertexArray(float[] FULL_RECTANGLE_COORDS) {
+ mVertexArray = GlUtil.createFloatBuffer(FULL_RECTANGLE_COORDS);
+ mVertexCount = FULL_RECTANGLE_COORDS.length / COORDS_PER_VERTEX;
+ }
+
+ public void updateTexCoordArray(float[] FULL_RECTANGLE_TEX_COORDS) {
+ mTexCoordArray = GlUtil.createFloatBuffer(FULL_RECTANGLE_TEX_COORDS);
+ }
+
+ /**
+ * Returns the array of vertices.
+ *
+ * To avoid allocations, this returns internal state. The caller must not modify it.
+ */
+ public FloatBuffer vertexArray() {
+ return mVertexArray;
+ }
+
+ /**
+ * Returns the array of texture coordinates.
+ *
+ * To avoid allocations, this returns internal state. The caller must not modify it.
+ */
+ public FloatBuffer texCoordArray() {
+ return mTexCoordArray;
+ }
+
+ /**
+ * Returns the number of vertices stored in the vertex array.
+ */
+ public int vertexCount() {
+ return mVertexCount;
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/EglCore.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/EglCore.java
new file mode 100644
index 000000000..59ce2f986
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/EglCore.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2013 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles.core;
+
+import android.graphics.SurfaceTexture;
+import android.opengl.EGL14;
+import android.opengl.EGLConfig;
+import android.opengl.EGLContext;
+import android.opengl.EGLDisplay;
+import android.opengl.EGLExt;
+import android.opengl.EGLSurface;
+import android.util.Log;
+import android.view.Surface;
+
+/**
+ * Core EGL state (display, context, config).
+ *
+ * The EGLContext must only be attached to one thread at a time. This class is not thread-safe.
+ */
+public final class EglCore {
+ private static final String TAG = GlUtil.TAG;
+
+ /**
+ * Constructor flag: surface must be recordable. This discourages EGL from using a
+ * pixel format that cannot be converted efficiently to something usable by the video
+ * encoder.
+ */
+ public static final int FLAG_RECORDABLE = 0x01;
+
+ /**
+ * Constructor flag: ask for GLES3, fall back to GLES2 if not available. Without this
+ * flag, GLES2 is used.
+ */
+ public static final int FLAG_TRY_GLES3 = 0x02;
+
+ // Android-specific extension.
+ private static final int EGL_RECORDABLE_ANDROID = 0x3142;
+
+ private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
+ private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
+ private EGLConfig mEGLConfig = null;
+ private int mGlVersion = -1;
+
+
+ /**
+ * Prepares EGL display and context.
+ *
+ * Equivalent to EglCore(null, 0).
+ */
+ public EglCore() {
+ this(null, 0);
+ }
+
+ /**
+ * Prepares EGL display and context.
+ *
+ *
+ * @param sharedContext The context to share, or null if sharing is not desired.
+ * @param flags Configuration bit flags, e.g. FLAG_RECORDABLE.
+ */
+ public EglCore(EGLContext sharedContext, int flags) {
+ if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+ throw new RuntimeException("EGL already set up");
+ }
+
+ if (sharedContext == null) {
+ sharedContext = EGL14.EGL_NO_CONTEXT;
+ }
+
+ mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
+ if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
+ throw new RuntimeException("unable to get EGL14 display");
+ }
+ int[] version = new int[2];
+ if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
+ mEGLDisplay = null;
+ throw new RuntimeException("unable to initialize EGL14");
+ }
+
+ // Try to get a GLES3 context, if requested.
+ if ((flags & FLAG_TRY_GLES3) != 0) {
+ //Log.d(TAG, "Trying GLES 3");
+ EGLConfig config = getConfig(flags, 3);
+ if (config != null) {
+ int[] attrib3_list = {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
+ EGL14.EGL_NONE
+ };
+ EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
+ attrib3_list, 0);
+
+ if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
+ //Log.d(TAG, "Got GLES 3 config");
+ mEGLConfig = config;
+ mEGLContext = context;
+ mGlVersion = 3;
+ }
+ }
+ }
+ if (mEGLContext == EGL14.EGL_NO_CONTEXT) { // GLES 2 only, or GLES 3 attempt failed
+ //Log.d(TAG, "Trying GLES 2");
+ EGLConfig config = getConfig(flags, 2);
+ if (config == null) {
+ throw new RuntimeException("Unable to find a suitable EGLConfig");
+ }
+ int[] attrib2_list = {
+ EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
+ EGL14.EGL_NONE
+ };
+ EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext,
+ attrib2_list, 0);
+ checkEglError("eglCreateContext");
+ mEGLConfig = config;
+ mEGLContext = context;
+ mGlVersion = 2;
+ }
+
+ // Confirm with query.
+ int[] values = new int[1];
+ EGL14.eglQueryContext(mEGLDisplay, mEGLContext, EGL14.EGL_CONTEXT_CLIENT_VERSION,
+ values, 0);
+ Log.d(TAG, "EGLContext created, client version " + values[0]);
+ }
+
+ /**
+ * Finds a suitable EGLConfig.
+ *
+ * @param flags Bit flags from constructor.
+ * @param version Must be 2 or 3.
+ */
+ private EGLConfig getConfig(int flags, int version) {
+ int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
+ if (version >= 3) {
+ renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
+ }
+
+ // The actual surface is generally RGBA or RGBX, so situationally omitting alpha
+ // doesn't really help. It can also lead to a huge performance hit on glReadPixels()
+ // when reading into a GL_RGBA buffer.
+ int[] attribList = {
+ EGL14.EGL_RED_SIZE, 8,
+ EGL14.EGL_GREEN_SIZE, 8,
+ EGL14.EGL_BLUE_SIZE, 8,
+ EGL14.EGL_ALPHA_SIZE, 8,
+ //EGL14.EGL_DEPTH_SIZE, 16,
+ //EGL14.EGL_STENCIL_SIZE, 8,
+ EGL14.EGL_RENDERABLE_TYPE, renderableType,
+ EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
+ EGL14.EGL_NONE
+ };
+ if ((flags & FLAG_RECORDABLE) != 0) {
+ attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
+ attribList[attribList.length - 2] = 1;
+ }
+ EGLConfig[] configs = new EGLConfig[1];
+ int[] numConfigs = new int[1];
+ if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
+ numConfigs, 0)) {
+ Log.w(TAG, "unable to find RGB8888 / " + version + " EGLConfig");
+ return null;
+ }
+ return configs[0];
+ }
+
+ /**
+ * Discards all resources held by this class, notably the EGL context. This must be
+ * called from the thread where the context was created.
+ *
+ * On completion, no context will be current.
+ */
+ public void release() {
+ if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+ // Android is unusual in that it uses a reference-counted EGLDisplay. So for
+ // every eglInitialize() we need an eglTerminate().
+ EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
+ EGL14.EGL_NO_CONTEXT);
+ EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
+ EGL14.eglReleaseThread();
+ EGL14.eglTerminate(mEGLDisplay);
+ }
+
+ mEGLDisplay = EGL14.EGL_NO_DISPLAY;
+ mEGLContext = EGL14.EGL_NO_CONTEXT;
+ mEGLConfig = null;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
+ // We're limited here -- finalizers don't run on the thread that holds
+ // the EGL state, so if a surface or context is still current on another
+ // thread we can't fully release it here. Exceptions thrown from here
+ // are quietly discarded. Complain in the log file.
+ Log.w(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked");
+ release();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+
+ public EGLContext getEGLContext() {
+ return mEGLContext;
+ }
+
+ /**
+ * Destroys the specified surface. Note the EGLSurface won't actually be destroyed if it's
+ * still current in a context.
+ */
+ public void releaseSurface(EGLSurface eglSurface) {
+ EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
+ }
+
+ /**
+ * Creates an EGL surface associated with a Surface.
+ *
+ * If this is destined for MediaCodec, the EGLConfig should have the "recordable" attribute.
+ */
+ public EGLSurface createWindowSurface(Object surface) {
+ if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
+ throw new RuntimeException("invalid surface: " + surface);
+ }
+
+ // Create a window surface, and attach it to the Surface we received.
+ int[] surfaceAttribs = {
+ EGL14.EGL_NONE
+ };
+ EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
+ surfaceAttribs, 0);
+ checkEglError("eglCreateWindowSurface");
+ if (eglSurface == null) {
+ throw new RuntimeException("surface was null");
+ }
+ return eglSurface;
+ }
+
+ /**
+ * Creates an EGL surface associated with an offscreen buffer.
+ */
+ public EGLSurface createOffscreenSurface(int width, int height) {
+ int[] surfaceAttribs = {
+ EGL14.EGL_WIDTH, width,
+ EGL14.EGL_HEIGHT, height,
+ EGL14.EGL_NONE
+ };
+ EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
+ surfaceAttribs, 0);
+ checkEglError("eglCreatePbufferSurface");
+ if (eglSurface == null) {
+ throw new RuntimeException("surface was null");
+ }
+ return eglSurface;
+ }
+
+ /**
+ * Makes our EGL context current, using the supplied surface for both "draw" and "read".
+ */
+ public void makeCurrent(EGLSurface eglSurface) {
+ if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
+ // called makeCurrent() before create?
+ Log.d(TAG, "NOTE: makeCurrent w/o display");
+ }
+ if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
+ throw new RuntimeException("eglMakeCurrent failed");
+ }
+ }
+
+ /**
+ * Makes our EGL context current, using the supplied "draw" and "read" surfaces.
+ */
+ public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
+ if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
+ // called makeCurrent() before create?
+ Log.d(TAG, "NOTE: makeCurrent w/o display");
+ }
+ if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
+ throw new RuntimeException("eglMakeCurrent(draw,read) failed");
+ }
+ }
+
+ /**
+ * Makes no context current.
+ */
+ public void makeNothingCurrent() {
+ if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
+ EGL14.EGL_NO_CONTEXT)) {
+ throw new RuntimeException("eglMakeCurrent failed");
+ }
+ }
+
+ /**
+ * Calls eglSwapBuffers. Use this to "publish" the current frame.
+ *
+ * @return false on failure
+ */
+ public boolean swapBuffers(EGLSurface eglSurface) {
+ return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
+ }
+
+ /**
+ * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds.
+ */
+ public void setPresentationTime(EGLSurface eglSurface, long nsecs) {
+ EGLExt.eglPresentationTimeANDROID(mEGLDisplay, eglSurface, nsecs);
+ }
+
+ /**
+ * Returns true if our context and the specified surface are current.
+ */
+ public boolean isCurrent(EGLSurface eglSurface) {
+ return mEGLContext.equals(EGL14.eglGetCurrentContext()) &&
+ eglSurface.equals(EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW));
+ }
+
+ public EGLSurface getCurrentDrawingSurface() {
+ EGLSurface surface = null;
+ if (mEGLContext.equals(EGL14.eglGetCurrentContext())) {
+ surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
+ }
+ return surface;
+ }
+
+ /**
+ * Performs a simple surface query.
+ */
+ public int querySurface(EGLSurface eglSurface, int what) {
+ int[] value = new int[1];
+ EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
+ return value[0];
+ }
+
+ /**
+ * Queries a string value.
+ */
+ public String queryString(int what) {
+ return EGL14.eglQueryString(mEGLDisplay, what);
+ }
+
+ /**
+ * Returns the GLES version this context is configured for (currently 2 or 3).
+ */
+ public int getGlVersion() {
+ return mGlVersion;
+ }
+
+ /**
+ * Writes the current display, context, and surface to the log.
+ */
+ public static void logCurrent(String msg) {
+ EGLDisplay display;
+ EGLContext context;
+ EGLSurface surface;
+
+ display = EGL14.eglGetCurrentDisplay();
+ context = EGL14.eglGetCurrentContext();
+ surface = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
+ Log.i(TAG, "Current EGL (" + msg + "): display=" + display + ", context=" + context +
+ ", surface=" + surface);
+ }
+
+ /**
+ * Checks for EGL errors. Throws an exception if an error has been raised.
+ */
+ private void checkEglError(String msg) {
+ int error;
+ if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
+ throw new RuntimeException(msg + ": EGL error: 0x" + Integer.toHexString(error));
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/EglSurfaceBase.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/EglSurfaceBase.java
new file mode 100644
index 000000000..98f776ecf
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/EglSurfaceBase.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2013 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles.core;
+
+import android.graphics.Bitmap;
+import android.opengl.EGL14;
+import android.opengl.EGLSurface;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Common base class for EGL surfaces.
+ *
+ * There can be multiple surfaces associated with a single context.
+ */
+public class EglSurfaceBase {
+ protected static final String TAG = GlUtil.TAG;
+
+ // EglCore object we're associated with. It may be associated with multiple surfaces.
+ protected EglCore mEglCore;
+
+ private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
+ private int mWidth = -1;
+ private int mHeight = -1;
+
+ protected EglSurfaceBase(EglCore eglCore) {
+ mEglCore = eglCore;
+ }
+
+ /**
+ * Creates a window surface.
+ *
+ *
+ * @param surface May be a Surface or SurfaceTexture.
+ */
+ public void createWindowSurface(Object surface) {
+ if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
+ throw new IllegalStateException("surface already created");
+ }
+ mEGLSurface = mEglCore.createWindowSurface(surface);
+
+ // Don't cache width/height here, because the size of the underlying surface can change
+ // out from under us (see e.g. HardwareScalerActivity).
+ //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
+ //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
+ }
+
+ /**
+ * Creates an off-screen surface.
+ */
+ public void createOffscreenSurface(int width, int height) {
+ if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
+ throw new IllegalStateException("surface already created");
+ }
+ mEGLSurface = mEglCore.createOffscreenSurface(width, height);
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Returns the surface's width, in pixels.
+ *
+ * If this is called on a window surface, and the underlying surface is in the process
+ * of changing size, we may not see the new size right away (e.g. in the "surfaceChanged"
+ * callback). The size should match after the next buffer swap.
+ */
+ public int getWidth() {
+ if (mWidth < 0) {
+ return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
+ } else {
+ return mWidth;
+ }
+ }
+
+ /**
+ * Returns the surface's height, in pixels.
+ */
+ public int getHeight() {
+ if (mHeight < 0) {
+ return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
+ } else {
+ return mHeight;
+ }
+ }
+
+ /**
+ * Release the EGL surface.
+ */
+ public void releaseEglSurface() {
+ mEglCore.releaseSurface(mEGLSurface);
+ mEGLSurface = EGL14.EGL_NO_SURFACE;
+ mWidth = mHeight = -1;
+ }
+
+ /**
+ * Makes our EGL context and surface current.
+ */
+ public void makeCurrent() {
+ mEglCore.makeCurrent(mEGLSurface);
+ }
+
+ /**
+ * Makes our EGL context and surface current for drawing, using the supplied surface
+ * for reading.
+ */
+ public void makeCurrentReadFrom(EglSurfaceBase readSurface) {
+ mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
+ }
+
+ /**
+ * Calls eglSwapBuffers. Use this to "publish" the current frame.
+ *
+ * @return false on failure
+ */
+ public boolean swapBuffers() {
+ boolean result = mEglCore.swapBuffers(mEGLSurface);
+ if (!result) {
+ Log.d(TAG, "WARNING: swapBuffers() failed");
+ }
+ return result;
+ }
+
+ /**
+ * Sends the presentation time stamp to EGL.
+ *
+ * @param nsecs Timestamp, in nanoseconds.
+ */
+ public void setPresentationTime(long nsecs) {
+ mEglCore.setPresentationTime(mEGLSurface, nsecs);
+ }
+
+ /**
+ * Saves the EGL surface to a file.
+ *
+ * Expects that this object's EGL surface is current.
+ */
+ public void saveFrame(File file) throws IOException {
+ if (!mEglCore.isCurrent(mEGLSurface)) {
+ throw new RuntimeException("Expected EGL context/surface is not current");
+ }
+
+ // glReadPixels fills in a "direct" ByteBuffer with what is essentially big-endian RGBA
+ // data (i.e. a byte of red, followed by a byte of green...). While the Bitmap
+ // constructor that takes an int[] wants little-endian ARGB (blue/red swapped), the
+ // Bitmap "copy pixels" method wants the same format GL provides.
+ //
+ // Ideally we'd have some way to re-use the ByteBuffer, especially if we're calling
+ // here often.
+ //
+ // Making this even more interesting is the upside-down nature of GL, which means
+ // our output will look upside down relative to what appears on screen if the
+ // typical GL conventions are used.
+
+ String filename = file.toString();
+
+ int width = getWidth();
+ int height = getHeight();
+ ByteBuffer buf = ByteBuffer.allocateDirect(width * height * 4);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ GLES20.glReadPixels(0, 0, width, height,
+ GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
+ GlUtil.checkGlError("glReadPixels");
+ buf.rewind();
+
+ BufferedOutputStream bos = null;
+ try {
+ bos = new BufferedOutputStream(new FileOutputStream(filename));
+ Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ bmp.copyPixelsFromBuffer(buf);
+ bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
+ bmp.recycle();
+ } finally {
+ if (bos != null) bos.close();
+ }
+ Log.d(TAG, "Saved " + width + "x" + height + " frame as '" + filename + "'");
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Extensions.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Extensions.java
new file mode 100644
index 000000000..06f33b1cd
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Extensions.java
@@ -0,0 +1,38 @@
+package io.agora.api.example.common.gles.core;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public abstract class Extensions {
+
+ public static byte[] getBytes(InputStream inputStream) {
+ try {
+ byte[] bytes = new byte[inputStream.available()];
+ inputStream.read(bytes);
+ inputStream.close();
+ return bytes;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return new byte[0];
+ }
+
+ public static byte[] getBytes(AssetManager assetManager, String fileName) {
+ try {
+ return getBytes(assetManager.open(fileName));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return new byte[0];
+ }
+
+ public static String readTextFileFromResource(Context context, int resourceId) {
+ return new String(Extensions.getBytes(context.getResources().openRawResource(resourceId)));
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/GlUtil.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/GlUtil.java
new file mode 100644
index 000000000..13cb63889
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/GlUtil.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles.core;
+
+import android.graphics.Bitmap;
+import android.opengl.GLES11Ext;
+import android.opengl.GLES20;
+import android.opengl.GLES30;
+import android.opengl.GLUtils;
+import android.opengl.Matrix;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+/**
+ * Some OpenGL utility functions.
+ */
+public abstract class GlUtil {
+ //public static final String TAG = "Grafika";
+ public static final String TAG = "mqi";
+ /**
+ * Identity matrix for general use. Don't modify or life will get weird.
+ */
+ public static final float[] IDENTITY_MATRIX;
+
+ static {
+ IDENTITY_MATRIX = new float[16];
+ Matrix.setIdentityM(IDENTITY_MATRIX, 0);
+ }
+
+ private static final int SIZEOF_FLOAT = 4;
+
+
+ private GlUtil() {
+ } // do not instantiate
+
+ /**
+ * Creates a new program from the supplied vertex and fragment shaders.
+ *
+ * @return A handle to the program, or 0 on failure.
+ */
+ public static int createProgram(String vertexSource, String fragmentSource) {
+ int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
+ if (vertexShader == 0) {
+ return 0;
+ }
+ int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
+ if (pixelShader == 0) {
+ return 0;
+ }
+
+ int program = GLES20.glCreateProgram();
+ checkGlError("glCreateProgram");
+ if (program == 0) {
+ Log.e(TAG, "Could not create program");
+ }
+ GLES20.glAttachShader(program, vertexShader);
+ checkGlError("glAttachShader");
+ GLES20.glAttachShader(program, pixelShader);
+ checkGlError("glAttachShader");
+ GLES20.glLinkProgram(program);
+ int[] linkStatus = new int[1];
+ GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] != GLES20.GL_TRUE) {
+ Log.e(TAG, "Could not link program: ");
+ Log.e(TAG, GLES20.glGetProgramInfoLog(program));
+ GLES20.glDeleteProgram(program);
+ program = 0;
+ }
+ return program;
+ }
+
+ /**
+ * Compiles the provided shader source.
+ *
+ * @return A handle to the shader, or 0 on failure.
+ */
+ public static int loadShader(int shaderType, String source) {
+ int shader = GLES20.glCreateShader(shaderType);
+ checkGlError("glCreateShader type=" + shaderType);
+ GLES20.glShaderSource(shader, source);
+ GLES20.glCompileShader(shader);
+ int[] compiled = new int[1];
+ GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
+ if (compiled[0] == 0) {
+ Log.e(TAG, "Could not compile shader " + shaderType + ":");
+ Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader));
+ GLES20.glDeleteShader(shader);
+ shader = 0;
+ }
+ return shader;
+ }
+
+ /**
+ * Checks to see if a GLES error has been raised.
+ */
+ public static void checkGlError(String op) {
+ int error = GLES20.glGetError();
+ if (error != GLES20.GL_NO_ERROR) {
+ String msg = op + ": glError 0x" + Integer.toHexString(error);
+ Log.e(TAG, msg);
+ }
+ }
+
+ /**
+ * Checks to see if the location we obtained is valid. GLES returns -1 if a label
+ * could not be found, but does not set the GL error.
+ *
+ * Throws a RuntimeException if the location is invalid.
+ */
+ public static void checkLocation(int location, String label) {
+ if (location < 0) {
+ Log.e(TAG, "Unable to locate '" + label + "' in program");
+ }
+ }
+
+ /**
+ * Creates a texture from raw data.
+ *
+ * @param data Image data, in a "direct" ByteBuffer.
+ * @param width Texture width, in pixels (not bytes).
+ * @param height Texture height, in pixels.
+ * @param format Image data format (use constant appropriate for glTexImage2D(), e.g. GL_RGBA).
+ * @return Handle to texture.
+ */
+ public static int createImageTexture(ByteBuffer data, int width, int height, int format) {
+ int[] textureHandles = new int[1];
+ int textureHandle;
+
+ GLES20.glGenTextures(1, textureHandles, 0);
+ textureHandle = textureHandles[0];
+ GlUtil.checkGlError("glGenTextures");
+
+ // Bind the texture handle to the 2D texture target.
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle);
+
+ // Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering
+ // is smaller or larger than the source image.
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+ GlUtil.checkGlError("loadImageTexture");
+
+ // Load the data from the buffer into the texture handle.
+ GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, /*level*/ 0, format,
+ width, height, /*border*/ 0, format, GLES20.GL_UNSIGNED_BYTE, data);
+ GlUtil.checkGlError("loadImageTexture");
+
+ return textureHandle;
+ }
+
+ /**
+ * Creates a texture from bitmap.
+ *
+ * @param bmp bitmap data
+ * @return Handle to texture.
+ */
+ public static int createImageTexture(Bitmap bmp) {
+ int[] textureHandles = new int[1];
+ int textureHandle;
+
+ GLES20.glGenTextures(1, textureHandles, 0);
+ textureHandle = textureHandles[0];
+ GlUtil.checkGlError("glGenTextures");
+
+ // Bind the texture handle to the 2D texture target.
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle);
+
+ // Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering
+ // is smaller or larger than the source image.
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+ GlUtil.checkGlError("loadImageTexture");
+
+ // Load the data from the buffer into the texture handle.
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /*level*/ 0, bmp, 0);
+ GlUtil.checkGlError("loadImageTexture");
+
+ return textureHandle;
+ }
+
+ /**
+ * Allocates a direct float buffer, and populates it with the float array data.
+ */
+ public static FloatBuffer createFloatBuffer(float[] coords) {
+ // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it.
+ ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT);
+ bb.order(ByteOrder.nativeOrder());
+ FloatBuffer fb = bb.asFloatBuffer();
+ fb.put(coords);
+ fb.position(0);
+ return fb;
+ }
+
+ /**
+ * Writes GL version info to the log.
+ */
+ public static void logVersionInfo() {
+ Log.i(TAG, "vendor : " + GLES20.glGetString(GLES20.GL_VENDOR));
+ Log.i(TAG, "renderer: " + GLES20.glGetString(GLES20.GL_RENDERER));
+ Log.i(TAG, "version : " + GLES20.glGetString(GLES20.GL_VERSION));
+
+ if (false) {
+ int[] values = new int[1];
+ GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, values, 0);
+ int majorVersion = values[0];
+ GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, values, 0);
+ int minorVersion = values[0];
+ if (GLES30.glGetError() == GLES30.GL_NO_ERROR) {
+ Log.i(TAG, "iversion: " + majorVersion + "." + minorVersion);
+ }
+ }
+ }
+
+
+ /**
+ * Creates a texture object suitable for use with this program.
+ *
+ * On exit, the texture will be bound.
+ */
+ public static int createTextureObject(int textureTarget) {
+ int[] textures = new int[1];
+ GLES20.glGenTextures(1, textures, 0);
+ GlUtil.checkGlError("glGenTextures");
+
+ int texId = textures[0];
+ GLES20.glBindTexture(textureTarget, texId);
+ GlUtil.checkGlError("glBindTexture " + texId);
+
+ GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_NEAREST);
+ GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR);
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
+ GLES20.GL_CLAMP_TO_EDGE);
+ GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
+ GLES20.GL_CLAMP_TO_EDGE);
+ GlUtil.checkGlError("glTexParameter");
+
+ return texId;
+ }
+
+ public static void deleteTextureObject(int textureId) {
+ int[] textures = new int[1];
+ textures[0] = textureId;
+ GLES20.glDeleteTextures(1, textures, 0);
+ GlUtil.checkGlError("glDeleteTextures");
+ }
+
+ public static float[] changeMVPMatrix(float[] mvpMatrix, float viewWidth, float viewHeight, float textureWidth, float textureHeight) {
+ float scale = viewWidth * textureHeight / viewHeight / textureWidth;
+ if (scale == 1) {
+ return mvpMatrix;
+ } else {
+ float[] mvp = new float[16];
+ float[] tmp = new float[16];
+ Matrix.setIdentityM(tmp, 0);
+ Matrix.scaleM(tmp, 0, scale > 1 ? 1F : (1F / scale), scale > 1 ? scale : 1F, 1F);
+ Matrix.multiplyMM(mvp, 0, tmp, 0, mvpMatrix, 0);
+ return mvp;
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/OffscreenSurface.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/OffscreenSurface.java
new file mode 100644
index 000000000..447a1416c
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/OffscreenSurface.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles.core;
+
+/**
+ * Off-screen EGL surface (pbuffer).
+ *
+ * It's good practice to explicitly release() the surface, preferably from a "finally" block.
+ */
+public class OffscreenSurface extends EglSurfaceBase {
+ /**
+ * Creates an off-screen surface with the specified width and height.
+ */
+ public OffscreenSurface(EglCore eglCore, int width, int height) {
+ super(eglCore);
+ createOffscreenSurface(width, height);
+ }
+
+ /**
+ * Releases any resources associated with the surface.
+ */
+ public void release() {
+ releaseEglSurface();
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Program.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Program.java
new file mode 100644
index 000000000..537e5d18d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/Program.java
@@ -0,0 +1,74 @@
+package io.agora.api.example.common.gles.core;
+
+import android.content.Context;
+import android.opengl.GLES20;
+
+/**
+ * Created by tujh on 2018/1/24.
+ */
+
+public abstract class Program {
+ private static final String TAG = GlUtil.TAG;
+
+ // Handles to the GL program and various components of it.
+ protected int mProgramHandle;
+
+
+ protected Drawable2d mDrawable2d;
+
+ /**
+ * Prepares the program in the current EGL context.
+ */
+ public Program(String VERTEX_SHADER, String FRAGMENT_SHADER_2D) {
+ mProgramHandle = GlUtil.createProgram(VERTEX_SHADER, FRAGMENT_SHADER_2D);
+ mDrawable2d = getDrawable2d();
+ getLocations();
+ }
+
+ public Program(Context context, int vertexShaderResourceId, int fragmentShaderResourceId) {
+ this(Extensions.readTextFileFromResource(context, vertexShaderResourceId), Extensions.readTextFileFromResource(context, fragmentShaderResourceId));
+ }
+
+ public void updateVertexArray(float[] FULL_RECTANGLE_COORDS) {
+ mDrawable2d.updateVertexArray(FULL_RECTANGLE_COORDS);
+ }
+
+ public void updateTexCoordArray(float[] FULL_RECTANGLE_TEX_COORDS) {
+ mDrawable2d.updateTexCoordArray(FULL_RECTANGLE_TEX_COORDS);
+ }
+
+ protected abstract Drawable2d getDrawable2d();
+
+ /**
+ * get locations of attributes and uniforms
+ */
+ protected abstract void getLocations();
+
+ /**
+ * Issues the draw call. Does the full setup on every call.
+ */
+ public abstract void drawFrame(int textureId, float[] texMatrix, float[] mvpMatrix);
+
+ public void drawFrame(int textureId, float[] texMatrix) {
+ drawFrame(textureId, texMatrix, GlUtil.IDENTITY_MATRIX);
+ }
+
+ public void drawFrame(int textureId, float[] texMatrix, float[] mvpMatrix, int x, int y, int width, int height) {
+ int[] originalViewport = new int[4];
+ GLES20.glGetIntegerv(GLES20.GL_VIEWPORT, originalViewport, 0);
+ GLES20.glViewport(x, y, width, height);
+ drawFrame(textureId, texMatrix, mvpMatrix);
+ GLES20.glViewport(originalViewport[0], originalViewport[1], originalViewport[2], originalViewport[3]);
+ }
+
+ /**
+ * Releases the program.
+ *
+ * The appropriate EGL context must be current (i.e. the one that was used to create
+ * the program).
+ */
+ public void release() {
+ GLES20.glDeleteProgram(mProgramHandle);
+ mProgramHandle = -1;
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/WindowSurface.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/WindowSurface.java
new file mode 100644
index 000000000..2c784f6ab
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/gles/core/WindowSurface.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agora.api.example.common.gles.core;
+
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+
+/**
+ * Recordable EGL window surface.
+ *
+ * It's good practice to explicitly release() the surface, preferably from a "finally" block.
+ */
+public class WindowSurface extends EglSurfaceBase {
+ private Surface mSurface;
+ private boolean mReleaseSurface;
+
+ /**
+ * Associates an EGL surface with the native window surface.
+ *
+ * Set releaseSurface to true if you want the Surface to be released when release() is
+ * called. This is convenient, but can interfere with framework classes that expect to
+ * manage the Surface themselves (e.g. if you release a SurfaceView's Surface, the
+ * surfaceDestroyed() callback won't fire).
+ */
+ public WindowSurface(EglCore eglCore, Surface surface, boolean releaseSurface) {
+ super(eglCore);
+ createWindowSurface(surface);
+ mSurface = surface;
+ mReleaseSurface = releaseSurface;
+ }
+
+ /**
+ * Associates an EGL surface with the SurfaceTexture.
+ */
+ public WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) {
+ super(eglCore);
+ createWindowSurface(surfaceTexture);
+ }
+
+ public WindowSurface(EglCore eglCore, int width, int height) {
+ super(eglCore);
+ createOffscreenSurface(width, height);
+ }
+
+ /**
+ * Releases any resources associated with the EGL surface (and, if configured to do so,
+ * with the Surface as well).
+ *
+ * Does not require that the surface's EGL context be current.
+ */
+ public void release() {
+ releaseEglSurface();
+ if (mSurface != null) {
+ if (mReleaseSurface) {
+ mSurface.release();
+ }
+ mSurface = null;
+ }
+ }
+
+ /**
+ * Recreate the EGLSurface, using the new EglBase. The caller should have already
+ * freed the old EGLSurface with releaseEglSurface().
+ *
+ * This is useful when we want to update the EGLSurface associated with a Surface.
+ * For example, if we want to share with a different EGLContext, which can only
+ * be done by tearing down and recreating the context. (That's handled by the caller;
+ * this just creates a new EGLSurface for the Surface we were handed earlier.)
+ *
+ * If the previous EGLSurface isn't fully destroyed, e.g. it's still current on a
+ * context somewhere, the create call will fail with complaints from the Surface
+ * about already being connected.
+ */
+ public void recreate(EglCore newEglCore) {
+ if (mSurface == null) {
+ throw new RuntimeException("not yet implemented for SurfaceTexture");
+ }
+ mEglCore = newEglCore; // switch to new context
+ createWindowSurface(mSurface); // create new surface
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/ExampleBean.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/ExampleBean.java
new file mode 100644
index 000000000..331ae96a7
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/ExampleBean.java
@@ -0,0 +1,99 @@
+package io.agora.api.example.common.model;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @author cjw
+ */
+public class ExampleBean implements Parcelable {
+ private int index;
+ private String group;
+ private int name;
+ private int actionId;
+ private int tipsId;
+
+ public ExampleBean(int index, String group, int name, int actionId, int tipsId) {
+ this.index = index;
+ this.group = group;
+ this.name = name;
+ this.actionId = actionId;
+ this.tipsId = tipsId;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ public String getGroup() {
+ return group;
+ }
+
+ public void setGroup(String group) {
+ this.group = group;
+ }
+
+ public int getName() {
+ return name;
+ }
+
+ public void setName(int name) {
+ this.name = name;
+ }
+
+ public int getActionId() {
+ return actionId;
+ }
+
+ public void setActionId(int actionId) {
+ this.actionId = actionId;
+ }
+
+ public int getTipsId() {
+ return tipsId;
+ }
+
+ public void setTipsId(int tipsId) {
+ this.tipsId = tipsId;
+ }
+
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(this.group);
+ dest.writeInt(this.name);
+ dest.writeInt(this.actionId);
+ dest.writeInt(this.tipsId);
+ }
+
+ public ExampleBean() {
+ }
+
+ protected ExampleBean(Parcel in) {
+ this.group = in.readString();
+ this.name = in.readInt();
+ this.actionId = in.readInt();
+ this.tipsId = in.readInt();
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public ExampleBean createFromParcel(Parcel source) {
+ return new ExampleBean(source);
+ }
+
+ @Override
+ public ExampleBean[] newArray(int size) {
+ return new ExampleBean[size];
+ }
+ };
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/Examples.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/Examples.java
new file mode 100644
index 000000000..80ec5092b
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/Examples.java
@@ -0,0 +1,41 @@
+package io.agora.api.example.common.model;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.agora.api.example.annotation.Example;
+
+public class Examples {
+ public static final String BASIC = "BASIC";
+ public static final String ADVANCED = "ADVANCED";
+
+ public static final Map> ITEM_MAP = new HashMap<>();
+
+ public static void addItem(@NonNull Example item) {
+ String group = item.group();
+ List list = ITEM_MAP.get(group);
+ if (list == null) {
+ list = new ArrayList<>();
+ ITEM_MAP.put(group, list);
+ }
+ list.add(item);
+ }
+
+ public static void sortItem() {
+ for (Map.Entry> entry : ITEM_MAP.entrySet()) {
+ List exampleList = ITEM_MAP.get(entry.getKey());
+ Collections.sort(exampleList, new Comparator() {
+ @Override
+ public int compare(Example o1, Example o2) {
+ return o1.index() - o2.index();
+ }
+ });
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/GlobalSettings.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/GlobalSettings.java
new file mode 100644
index 000000000..47733e200
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/GlobalSettings.java
@@ -0,0 +1,83 @@
+package io.agora.api.example.common.model;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+public class GlobalSettings {
+ private String areaCodeStr = "GLOBAL";
+
+ // private cloud config
+ public String privateCloudIp = "";
+ public boolean privateCloudLogReportEnable = false;
+ public String privateCloudLogServerDomain = "";
+ public int privateCloudLogServerPort = 80;
+ public String privateCloudLogServerPath = "";
+ public boolean privateCloudUseHttps = false;
+ // public String privateCloudIp = "10.62.0.85";
+ // public boolean privateCloudLogReportEnable = true;
+ // public String privateCloudLogServerDomain = "10.72.0.29";
+ // public int privateCloudLogServerPort = 442;
+ // public String privateCloudLogServerPath = "/kafka/log/upload/v1";
+ // public boolean privateCloudUseHttps = true;
+
+ public LocalAccessPointConfiguration getPrivateCloudConfig() {
+ LocalAccessPointConfiguration config = new LocalAccessPointConfiguration();
+ if(TextUtils.isEmpty(privateCloudIp)){
+ return null;
+ }
+ config.ipList = new ArrayList<>();
+ config.ipList.add(privateCloudIp);
+ config.domainList = new ArrayList<>();
+ config.mode = Constants.LOCAL_RPOXY_LOCAL_ONLY;
+ if (privateCloudLogReportEnable) {
+ LocalAccessPointConfiguration.AdvancedConfigInfo advancedConfig = new LocalAccessPointConfiguration.AdvancedConfigInfo();
+ LocalAccessPointConfiguration.LogUploadServerInfo logUploadServer = new LocalAccessPointConfiguration.LogUploadServerInfo();
+ logUploadServer.serverDomain = privateCloudLogServerDomain;
+ logUploadServer.serverPort = privateCloudLogServerPort;
+ logUploadServer.serverPath = privateCloudLogServerPath;
+ logUploadServer.serverHttps = privateCloudUseHttps;
+
+ advancedConfig.logUploadServer = logUploadServer;
+ config.advancedConfig = advancedConfig;
+ }
+ return config;
+ }
+
+
+ public String getAreaCodeStr() {
+ return areaCodeStr;
+ }
+
+ public void setAreaCodeStr(String areaCodeStr) {
+ this.areaCodeStr = areaCodeStr;
+ }
+
+ public int getAreaCode(){
+ if("CN".equals(areaCodeStr)){
+ return RtcEngineConfig.AreaCode.AREA_CODE_CN;
+ }
+ else if("NA".equals(areaCodeStr)){
+ return RtcEngineConfig.AreaCode.AREA_CODE_NA;
+ }
+ else if("EU".equals(areaCodeStr)){
+ return RtcEngineConfig.AreaCode.AREA_CODE_EU;
+ }
+ else if("AS".equals(areaCodeStr)){
+ return RtcEngineConfig.AreaCode.AREA_CODE_AS;
+ }
+ else if("JP".equals(areaCodeStr)){
+ return RtcEngineConfig.AreaCode.AREA_CODE_JP;
+ }
+ else if("IN".equals(areaCodeStr)){
+ return RtcEngineConfig.AreaCode.AREA_CODE_IN;
+ }
+ else{
+ return RtcEngineConfig.AreaCode.AREA_CODE_GLOB;
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/Peer.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/Peer.java
new file mode 100644
index 000000000..676f7c7ec
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/Peer.java
@@ -0,0 +1,16 @@
+package io.agora.api.example.common.model;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Created by wyylling@gmail.com on 03/01/2018.
+ */
+
+public class Peer {
+ public int uid;
+ public ByteBuffer data;
+ public int width;
+ public int height;
+ public int rotation;
+ public long ts;
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/StatisticsInfo.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/StatisticsInfo.java
new file mode 100644
index 000000000..0b41aa1eb
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/model/StatisticsInfo.java
@@ -0,0 +1,167 @@
+package io.agora.api.example.common.model;
+
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.IRtcEngineEventHandler.LocalVideoStats;
+import io.agora.rtc2.IRtcEngineEventHandler.RemoteAudioStats;
+import io.agora.rtc2.IRtcEngineEventHandler.RemoteVideoStats;
+
+public class StatisticsInfo {
+ private LocalVideoStats localVideoStats = new LocalVideoStats();
+ private IRtcEngineEventHandler.LocalAudioStats localAudioStats = new IRtcEngineEventHandler.LocalAudioStats();
+ private RemoteVideoStats remoteVideoStats = new RemoteVideoStats();
+ private RemoteAudioStats remoteAudioStats = new RemoteAudioStats();
+ private IRtcEngineEventHandler.RtcStats rtcStats = new IRtcEngineEventHandler.RtcStats();
+ private int quality;
+ private IRtcEngineEventHandler.LastmileProbeResult lastMileProbeResult;
+
+ public void setLocalVideoStats(LocalVideoStats localVideoStats) {
+ this.localVideoStats = localVideoStats;
+ }
+
+ public void setLocalAudioStats(IRtcEngineEventHandler.LocalAudioStats localAudioStats) {
+ this.localAudioStats = localAudioStats;
+ }
+
+ public void setRemoteVideoStats(RemoteVideoStats remoteVideoStats) {
+ this.remoteVideoStats = remoteVideoStats;
+ }
+
+ public void setRemoteAudioStats(RemoteAudioStats remoteAudioStats) {
+ this.remoteAudioStats = remoteAudioStats;
+ }
+
+ public void setRtcStats(IRtcEngineEventHandler.RtcStats rtcStats) {
+ this.rtcStats = rtcStats;
+ }
+
+ public String getLocalVideoStats() {
+ StringBuilder builder = new StringBuilder();
+ return builder
+ .append(""+localVideoStats.encodedFrameWidth)
+ .append("×")
+ .append(localVideoStats.encodedFrameHeight)
+ .append(",")
+ .append(localVideoStats.encoderOutputFrameRate)
+ .append("fps")
+ .append("\n")
+ .append("LM Delay: ")
+ .append(rtcStats.lastmileDelay)
+ .append("ms")
+ .append("\n")
+ .append("VSend: ")
+ .append(localVideoStats.sentBitrate)
+ .append("kbps")
+ .append("\n")
+ .append("ASend: ")
+ .append(localAudioStats.sentBitrate)
+ .append("kbps")
+ .append("\n")
+ .append("CPU: ")
+ .append(rtcStats.cpuAppUsage)
+ .append("%/")
+ .append(rtcStats.cpuTotalUsage)
+ .append("%/")
+ .append("\n")
+ .append("VSend Loss: ")
+ .append(rtcStats.txPacketLossRate)
+ .append("%")
+ .toString();
+ }
+
+ public String getRemoteVideoStats() {
+ StringBuilder builder = new StringBuilder();
+ return builder
+ .append(remoteVideoStats.width)
+ .append("×")
+ .append(remoteVideoStats.height)
+ .append(",")
+ .append(remoteVideoStats.rendererOutputFrameRate)
+ .append("fps")
+ .append("\n")
+ .append("VRecv: ")
+ .append(remoteVideoStats.receivedBitrate)
+ .append("kbps")
+ .append("\n")
+ .append("ARecv: ")
+ .append(remoteAudioStats.receivedBitrate)
+ .append("kbps")
+ .append("\n")
+ .append("VLoss: ")
+ .append(remoteVideoStats.packetLossRate)
+ .append("%")
+ .append("\n")
+ .append("ALoss: ")
+ .append(remoteAudioStats.audioLossRate)
+ .append("%")
+ .append("\n")
+ .append("AQuality: ")
+ .append(remoteAudioStats.quality)
+ .toString();
+ }
+
+ public void setLastMileQuality(int quality) {
+ this.quality = quality;
+ }
+
+ public String getLastMileQuality(){
+ switch (quality){
+ case 1:
+ return "EXCELLENT";
+ case 2:
+ return "GOOD";
+ case 3:
+ return "POOR";
+ case 4:
+ return "BAD";
+ case 5:
+ return "VERY BAD";
+ case 6:
+ return "DOWN";
+ case 7:
+ return "UNSUPPORTED";
+ case 8:
+ return "DETECTING";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ public String getLastMileResult() {
+ if(lastMileProbeResult == null)
+ return null;
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("Rtt: ")
+ .append(lastMileProbeResult.rtt)
+ .append("ms")
+ .append("\n")
+ .append("DownlinkAvailableBandwidth: ")
+ .append(lastMileProbeResult.downlinkReport.availableBandwidth)
+ .append("Kbps")
+ .append("\n")
+ .append("DownlinkJitter: ")
+ .append(lastMileProbeResult.downlinkReport.jitter)
+ .append("ms")
+ .append("\n")
+ .append("DownlinkLoss: ")
+ .append(lastMileProbeResult.downlinkReport.packetLossRate)
+ .append("%")
+ .append("\n")
+ .append("UplinkAvailableBandwidth: ")
+ .append(lastMileProbeResult.uplinkReport.availableBandwidth)
+ .append("Kbps")
+ .append("\n")
+ .append("UplinkJitter: ")
+ .append(lastMileProbeResult.uplinkReport.jitter)
+ .append("ms")
+ .append("\n")
+ .append("UplinkLoss: ")
+ .append(lastMileProbeResult.uplinkReport.packetLossRate)
+ .append("%");
+ return stringBuilder.toString();
+ }
+
+ public void setLastMileProbeResult(IRtcEngineEventHandler.LastmileProbeResult lastmileProbeResult) {
+ this.lastMileProbeResult = lastmileProbeResult;
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/AudioOnlyLayout.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/AudioOnlyLayout.java
new file mode 100644
index 000000000..30c60870c
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/AudioOnlyLayout.java
@@ -0,0 +1,71 @@
+package io.agora.api.example.common.widget;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Map;
+
+import io.agora.api.example.R;
+
+public class AudioOnlyLayout extends FrameLayout {
+
+ private TextView tvUserType, tvUserId;
+ private TableLayout tlState;
+
+ public AudioOnlyLayout(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public AudioOnlyLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AudioOnlyLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initView();
+ }
+
+
+ private void initView(){
+ View rootView = View.inflate(getContext(), R.layout.widget_audio_only_layout, this);
+ tvUserType = rootView.findViewById(R.id.tv_user_type);
+ tvUserId = rootView.findViewById(R.id.tv_user_id);
+ tlState = rootView.findViewById(R.id.table_layout_state);
+ }
+
+ public void updateUserInfo(String uid, boolean isLocal){
+ tvUserId.setText(uid + "");
+ tvUserType.setText(isLocal ? "Local": "Remote");
+ }
+
+ public void updateStats(Map states){
+ tlState.removeAllViews();
+ if(states == null || states.size() <= 0){
+ return;
+ }
+
+ for (Map.Entry entry : states.entrySet()) {
+ TableRow row = new TableRow(getContext());
+ TextView keyTv = new TextView(getContext());
+ keyTv.setTextSize(10);
+ keyTv.setTextColor(Color.BLACK);
+ keyTv.setText(entry.getKey());
+ row.addView(keyTv);
+ TextView valueTv = new TextView(getContext());
+ valueTv.setTextSize(10);
+ valueTv.setTextColor(Color.BLACK);
+ valueTv.setText(entry.getValue());
+ row.addView(valueTv);
+ tlState.addView(row);
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/AudioSeatManager.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/AudioSeatManager.java
new file mode 100644
index 000000000..0e1292787
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/AudioSeatManager.java
@@ -0,0 +1,131 @@
+package io.agora.api.example.common.widget;
+
+import android.view.View;
+
+import java.util.ArrayList;
+
+/**
+ * The type Audio seat manager.
+ */
+public class AudioSeatManager {
+
+ private final AudioOnlyLayout[] audioOnlyLayouts;
+
+ /**
+ * Instantiates a new Audio seat manager.
+ *
+ * @param seats the seats
+ */
+ public AudioSeatManager(AudioOnlyLayout... seats) {
+ audioOnlyLayouts = new AudioOnlyLayout[seats.length];
+ for (int i = 0; i < audioOnlyLayouts.length; i++) {
+ audioOnlyLayouts[i] = seats[i];
+ }
+ }
+
+ /**
+ * Up local seat.
+ *
+ * @param uid the uid
+ */
+ public void upLocalSeat(int uid) {
+ AudioOnlyLayout localSeat = audioOnlyLayouts[0];
+ localSeat.setTag(uid);
+ localSeat.setVisibility(View.VISIBLE);
+ localSeat.updateUserInfo(uid + "", true);
+ }
+
+ /**
+ * Up remote seat.
+ *
+ * @param uid the uid
+ */
+ public void upRemoteSeat(int uid) {
+ AudioOnlyLayout idleSeat = null;
+ for (AudioOnlyLayout audioOnlyLayout : audioOnlyLayouts) {
+ if (audioOnlyLayout.getTag() == null) {
+ idleSeat = audioOnlyLayout;
+ break;
+ }
+ }
+ if (idleSeat != null) {
+ idleSeat.setTag(uid);
+ idleSeat.setVisibility(View.VISIBLE);
+ idleSeat.updateUserInfo(uid + "", false);
+ }
+ }
+
+ /**
+ * Get seat remote uid list array list.
+ *
+ * @return the array list
+ */
+ public ArrayList getSeatRemoteUidList() {
+ ArrayList uidList = new ArrayList<>();
+ for (int i = 1; i < audioOnlyLayouts.length; i++) {
+ AudioOnlyLayout audioOnlyLayout = audioOnlyLayouts[i];
+ Object tag = audioOnlyLayout.getTag();
+ if (tag instanceof Integer) {
+ uidList.add((Integer) tag);
+ }
+ }
+ return uidList;
+ }
+
+ /**
+ * Down seat.
+ *
+ * @param uid the uid
+ */
+ public void downSeat(int uid) {
+ AudioOnlyLayout seat = null;
+ for (AudioOnlyLayout audioOnlyLayout : audioOnlyLayouts) {
+ Object tag = audioOnlyLayout.getTag();
+ if (tag instanceof Integer && (Integer) tag == uid) {
+ seat = audioOnlyLayout;
+ break;
+ }
+ }
+ if (seat != null) {
+ seat.setTag(null);
+ seat.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * Get local seat audio only layout.
+ *
+ * @return the audio only layout
+ */
+ public AudioOnlyLayout getLocalSeat() {
+ return audioOnlyLayouts[0];
+ }
+
+ /**
+ * Get remote seat audio only layout.
+ *
+ * @param uid the uid
+ * @return the audio only layout
+ */
+ public AudioOnlyLayout getRemoteSeat(int uid) {
+ AudioOnlyLayout seat = null;
+ for (AudioOnlyLayout audioOnlyLayout : audioOnlyLayouts) {
+ Object tag = audioOnlyLayout.getTag();
+ if (tag instanceof Integer && (Integer) tag == uid) {
+ seat = audioOnlyLayout;
+ break;
+ }
+ }
+ return seat;
+ }
+
+ /**
+ * Down all seats.
+ */
+ public void downAllSeats() {
+ for (AudioOnlyLayout audioOnlyLayout : audioOnlyLayouts) {
+ audioOnlyLayout.setTag(null);
+ audioOnlyLayout.setVisibility(View.INVISIBLE);
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/VideoReportLayout.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/VideoReportLayout.java
new file mode 100644
index 000000000..5210f4560
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/VideoReportLayout.java
@@ -0,0 +1,111 @@
+package io.agora.api.example.common.widget;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import io.agora.api.example.common.model.StatisticsInfo;
+import io.agora.rtc2.IRtcEngineEventHandler;
+
+public class VideoReportLayout extends FrameLayout {
+
+ private final StatisticsInfo statisticsInfo = new StatisticsInfo();
+ private TextView reportTextView;
+ private int reportUid = -1;
+
+ public VideoReportLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public VideoReportLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public VideoReportLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+
+ if (child instanceof SurfaceView || child instanceof TextureView) {
+ reportTextView = new TextView(getContext());
+ reportTextView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ reportTextView.removeOnAttachStateChangeListener(this);
+ reportTextView = null;
+ }
+ });
+ reportTextView.setTextColor(Color.parseColor("#eeeeee"));
+ LayoutParams reportParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ reportParams.topMargin = reportParams.leftMargin = 16;
+ addView(reportTextView, reportParams);
+ }
+ }
+
+ public void setReportUid(int uid) {
+ this.reportUid = uid;
+ }
+
+ public int getReportUid() {
+ return reportUid;
+ }
+
+ public void setLocalAudioStats(IRtcEngineEventHandler.LocalAudioStats stats){
+ statisticsInfo.setLocalAudioStats(stats);
+ setReportText(statisticsInfo.getLocalVideoStats());
+ }
+
+ public void setLocalVideoStats(IRtcEngineEventHandler.LocalVideoStats stats){
+ if (stats.uid != reportUid) {
+ return;
+ }
+ statisticsInfo.setLocalVideoStats(stats);
+ setReportText(statisticsInfo.getLocalVideoStats());
+ }
+
+ public void setRemoteAudioStats(IRtcEngineEventHandler.RemoteAudioStats stats){
+ if (stats.uid != reportUid) {
+ return;
+ }
+ statisticsInfo.setRemoteAudioStats(stats);
+ setReportText(statisticsInfo.getRemoteVideoStats());
+ }
+
+ public void setRemoteVideoStats(IRtcEngineEventHandler.RemoteVideoStats stats){
+ if (stats.uid != reportUid) {
+ return;
+ }
+ statisticsInfo.setRemoteVideoStats(stats);
+ setReportText(statisticsInfo.getRemoteVideoStats());
+ }
+
+
+ private void setReportText(String reportText) {
+ if(reportTextView != null){
+ reportTextView.post(new Runnable() {
+ @Override
+ public void run() {
+ reportTextView.setText(reportText);
+ }
+ });
+ }
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/WaveformView.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/WaveformView.java
new file mode 100644
index 000000000..839ebb022
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/widget/WaveformView.java
@@ -0,0 +1,209 @@
+package io.agora.api.example.common.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+
+import io.agora.api.example.R;
+
+public class WaveformView extends View {
+ private ArrayList datas = new ArrayList<>();
+ private short max = 100;
+ private float mWidth;
+ private float mHeight;
+ private float space =1f;
+ private Paint mWavePaint;
+ private Paint baseLinePaint;
+ private int mWaveColor = Color.WHITE;
+ private int mBaseLineColor = Color.WHITE;
+ private float waveStrokeWidth = 4f;
+ private int invalidateTime = 1000 / 100;
+ private long drawTime;
+ private boolean isMaxConstant = false;
+
+ public WaveformView(Context context) {
+ this(context, null);
+ }
+
+ public WaveformView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public WaveformView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs, defStyleAttr);
+ }
+
+ private void init(AttributeSet attrs, int defStyle) {
+ final TypedArray a = getContext().obtainStyledAttributes(
+ attrs, R.styleable.WaveView, defStyle, 0);
+ mWaveColor = a.getColor(
+ R.styleable.WaveView_waveColor,
+ mWaveColor);
+ mBaseLineColor = a.getColor(
+ R.styleable.WaveView_baselineColor,
+ mBaseLineColor);
+
+ waveStrokeWidth = a.getDimension(
+ R.styleable.WaveView_waveStokeWidth,
+ waveStrokeWidth);
+
+ max = (short) a.getInt(R.styleable.WaveView_maxValue, max);
+ invalidateTime = a.getInt(R.styleable.WaveView_invalidateTime, invalidateTime);
+
+ space = a.getDimension(R.styleable.WaveView_space, space);
+ a.recycle();
+ initPainters();
+
+ }
+
+ private void initPainters() {
+ mWavePaint = new Paint();
+ mWavePaint.setColor(mWaveColor);// 画笔为color
+ mWavePaint.setStrokeWidth(waveStrokeWidth);// 设置画笔粗细
+ mWavePaint.setAntiAlias(true);
+ mWavePaint.setFilterBitmap(true);
+ mWavePaint.setStrokeCap(Paint.Cap.ROUND);
+ mWavePaint.setStyle(Paint.Style.FILL);
+ Shader shader = new LinearGradient(0, 0, 1000, 0, 0xffffffff, 0xFFe850ee, Shader.TileMode.CLAMP);
+ mWavePaint.setShader(shader);
+ baseLinePaint = new Paint();
+ baseLinePaint.setColor(mBaseLineColor);// 画笔为color
+ baseLinePaint.setStrokeWidth(1f);// 设置画笔粗细
+ baseLinePaint.setAntiAlias(true);
+ baseLinePaint.setFilterBitmap(true);
+ baseLinePaint.setStyle(Paint.Style.FILL);
+ }
+
+ public short getMax() {
+ return max;
+ }
+
+ public void setMax(short max) {
+ this.max = max;
+ }
+
+ public float getSpace() {
+ return space;
+ }
+
+ public void setSpace(float space) {
+ this.space = space;
+ }
+
+ public int getmWaveColor() {
+ return mWaveColor;
+ }
+
+ public void setmWaveColor(int mWaveColor) {
+ this.mWaveColor = mWaveColor;
+ invalidateNow();
+ }
+
+ public int getmBaseLineColor() {
+ return mBaseLineColor;
+ }
+
+ public void setmBaseLineColor(int mBaseLineColor) {
+ this.mBaseLineColor = mBaseLineColor;
+ invalidateNow();
+ }
+
+ public float getWaveStrokeWidth() {
+ return waveStrokeWidth;
+ }
+
+ public void setWaveStrokeWidth(float waveStrokeWidth) {
+ this.waveStrokeWidth = waveStrokeWidth;
+ invalidateNow();
+ }
+
+ public int getInvalidateTime() {
+ return invalidateTime;
+ }
+
+ public void setInvalidateTime(int invalidateTime) {
+ this.invalidateTime = invalidateTime;
+ }
+
+ public boolean isMaxConstant() {
+ return isMaxConstant;
+ }
+
+ public void setMaxConstant(boolean maxConstant) {
+ isMaxConstant = maxConstant;
+ }
+
+ /**
+ * 如果改变相应配置 需要刷新相应的paint设置
+ */
+ public void invalidateNow() {
+ initPainters();
+ invalidate();
+ }
+
+ public void addData(short data) {
+
+ if (data < 0) {
+ data = (short) -data;
+ }
+ if (data > max && !isMaxConstant) {
+ max = data;
+ }
+ if (datas.size() > mWidth / space) {
+ synchronized (this) {
+ datas.remove(0);
+ datas.add(data);
+ }
+ } else {
+ datas.add(data);
+ }
+ if (System.currentTimeMillis() - drawTime > invalidateTime) {
+ invalidate();
+ drawTime = System.currentTimeMillis();
+ }
+
+ }
+
+ public void clear() {
+ datas.clear();
+ invalidateNow();
+ }
+
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.translate(0, mHeight / 2);
+ drawBaseLine(canvas);
+ drawWave(canvas);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ mWidth = w;
+ mHeight = h;
+ }
+
+ private void drawWave(Canvas mCanvas) {
+ for (int i = 0; i < datas.size(); i++) {
+ float x = (i) * space;
+ float y = (float) datas.get(i) / max * mHeight / 2;
+ mCanvas.drawLine(x, -y, x, y, mWavePaint);
+ }
+
+ }
+
+ private void drawBaseLine(Canvas mCanvas) {
+ mCanvas.drawLine(0, 0, mWidth, 0, baseLinePaint);
+ }
+}
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java
new file mode 100644
index 000000000..e41594819
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java
@@ -0,0 +1,591 @@
+package io.agora.api.example.examples.advanced;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.Constant;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IAudioEffectManager;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+@Example(
+ index = 15,
+ group = ADVANCED,
+ name = R.string.item_playaudiofiles,
+ actionId = R.id.action_mainFragment_to_PlayAudioFiles,
+ tipsId = R.string.playaudiofiles
+)
+public class PlayAudioFiles extends BaseFragment implements View.OnClickListener,
+ SeekBar.OnSeekBarChangeListener, AdapterView.OnItemSelectedListener {
+ private static final String TAG = PlayAudioFiles.class.getSimpleName();
+ private static final int EFFECT_SOUND_ID = 0;
+ private EditText et_channel;
+ private Button join;
+ private Spinner audioProfile, audioScenario;
+ private TextView mixingStart, mixingResume, mixingPause, mixingStop,
+ effectStart, effectResume, effectPause, effectStop;
+ private SeekBar mixingPublishVolBar, mixingPlayoutVolBar, mixingVolBar, effectVolBar;
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+ private IAudioEffectManager audioEffectManager;
+
+ private AudioSeatManager audioSeatManager;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.fragment_play_audio_files, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
+ {
+ super.onViewCreated(view, savedInstanceState);
+
+ join = view.findViewById(R.id.btn_join);
+ et_channel = view.findViewById(R.id.et_channel);
+ join.setOnClickListener(this);
+ audioProfile = view.findViewById(R.id.audio_profile_spinner);
+ audioScenario = view.findViewById(R.id.audio_scenario_spinner);
+ audioScenario.setOnItemSelectedListener(this);
+
+ // mixing
+ mixingStart = view.findViewById(R.id.mixing_start);
+ mixingResume = view.findViewById(R.id.mixing_resume);
+ mixingPause = view.findViewById(R.id.mixing_pause);
+ mixingStop = view.findViewById(R.id.mixing_stop);
+ mixingVolBar = view.findViewById(R.id.mixingVolBar);
+ mixingPlayoutVolBar = view.findViewById(R.id.mixingPlayoutVolBar);
+ mixingPublishVolBar = view.findViewById(R.id.mixingPublishVolBar);
+
+ mixingStart.setOnClickListener(this);
+ mixingResume.setOnClickListener(this);
+ mixingPause.setOnClickListener(this);
+ mixingStop.setOnClickListener(this);
+ mixingVolBar.setOnSeekBarChangeListener(this);
+ mixingPlayoutVolBar.setOnSeekBarChangeListener(this);
+ mixingPublishVolBar.setOnSeekBarChangeListener(this);
+
+ // effect
+ effectStart = view.findViewById(R.id.effect_start);
+ effectResume = view.findViewById(R.id.effect_resume);
+ effectPause = view.findViewById(R.id.effect_pause);
+ effectStop = view.findViewById(R.id.effect_stop);
+ effectVolBar = view.findViewById(R.id.effectVolBar);
+
+ effectStart.setOnClickListener(this);
+ effectResume.setOnClickListener(this);
+ effectPause.setOnClickListener(this);
+ effectStop.setOnClickListener(this);
+ effectVolBar.setOnSeekBarChangeListener(this);
+
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02)
+ );
+
+ resetLayoutByJoin();
+ }
+
+ private void resetLayoutByJoin(){
+ audioProfile.setEnabled(!joined);
+
+ mixingStart.setClickable(joined);
+ mixingResume.setClickable(joined);
+ mixingPause.setClickable(joined);
+ mixingStop.setClickable(joined);
+ mixingVolBar.setEnabled(joined);
+ mixingPlayoutVolBar.setEnabled(joined);
+ mixingPublishVolBar.setEnabled(joined);
+
+ effectStart.setClickable(joined);
+ effectResume.setClickable(joined);
+ effectPause.setClickable(joined);
+ effectStop.setClickable(joined);
+ effectVolBar.setEnabled(joined);
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState)
+ {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null)
+ {
+ return;
+ }
+ try
+ {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ preloadAudioEffect();
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ /**
+ * To ensure smooth communication, limit the size of the audio effect file.
+ * We recommend using this method to preload the audio effect before calling the joinChannel method.
+ */
+ private void preloadAudioEffect(){
+ // Gets the global audio effect manager.
+ audioEffectManager = engine.getAudioEffectManager();
+ // Preloads the audio effect (recommended). Note the file size, and preload the file before joining the channel.
+ // Only mp3, aac, m4a, 3gp, and wav files are supported.
+ // You may need to record the sound IDs and their file paths.
+ audioEffectManager.preloadEffect(EFFECT_SOUND_ID, Constant.EFFECT_FILE_PATH);
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ super.onDestroy();
+ /**leaveChannel and Destroy the RtcEngine instance*/
+ if(engine != null)
+ {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (parent == audioScenario) {
+ engine.setAudioScenario(Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())));
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+
+ @Override
+ public void onClick(View v)
+ {
+ if (v == join)
+ {
+ if (!joined)
+ {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA))
+ {
+ joinChannel(channelId);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ Permission.Group.STORAGE,
+ Permission.Group.MICROPHONE
+ ).onGranted(permissions ->
+ {
+ // Permissions Granted
+ joinChannel(channelId);
+ }).start();
+ }
+ else
+ {
+ joined = false;
+ /**After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ join.setText(getString(R.string.join));
+ resetLayoutByJoin();
+ audioSeatManager.downAllSeats();
+ }
+ }
+ else if (v == mixingStart)
+ {
+ int ret = engine.startAudioMixing(Constant.MIX_FILE_PATH, false, -1, 0);
+ Log.i(TAG, "startAudioMixing >> ret=" + ret);
+ }
+ else if (v == mixingResume)
+ {
+ int ret = engine.resumeAudioMixing();
+ Log.i(TAG, "resumeAudioMixing >> ret=" + ret);
+ }
+ else if (v == mixingPause)
+ {
+ int ret = engine.pauseAudioMixing();
+ Log.i(TAG, "pauseAudioMixing >> ret=" + ret);
+ }
+ else if (v == mixingStop)
+ {
+ int ret = engine.stopAudioMixing();
+ Log.i(TAG, "stopAudioMixing >> ret=" + ret);
+ }
+ else if (v == effectStart)
+ {
+ /** Plays an audio effect file.
+ * Returns
+ * 0: Success.
+ * < 0: Failure.
+ */
+ int playRet = audioEffectManager.playEffect(
+ EFFECT_SOUND_ID, // The sound ID of the audio effect file to be played.
+ Constant.EFFECT_FILE_PATH, // The file path of the audio effect file.
+ -1, // The number of playback loops. -1 means an infinite loop.
+ 1, // pitch The pitch of the audio effect. The value ranges between 0.5 and 2. The default value is 1 (no change to the pitch). The lower the value, the lower the pitch.
+ 0.0, // Sets the spatial position of the effect. 0 means the effect shows ahead.
+ 100, // Sets the volume. The value ranges between 0 and 100. 100 is the original volume.
+ true // Sets whether to publish the audio effect.
+ );
+ Log.i(TAG, "result playRet:"+ playRet);
+ }
+ else if(v == effectResume){
+ int ret = engine.resumeEffect(EFFECT_SOUND_ID);
+ Log.i(TAG, "resumeEffect >> ret=" + ret);
+ }
+ else if(v == effectPause){
+ int ret = engine.pauseEffect(EFFECT_SOUND_ID);
+ Log.i(TAG, "resumeEffect >> ret=" + ret);
+ }
+ else if(v == effectStop){
+ int ret = engine.stopEffect(EFFECT_SOUND_ID);
+ Log.i(TAG, "resumeEffect >> ret=" + ret);
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.*/
+ private void joinChannel(String channelId)
+ {
+ /**In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+ engine.setAudioProfile(
+ Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString())),
+ Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString()))
+ );
+
+ /**Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+ /** Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+ int res = engine.joinChannel(ret, channelId, 0, option);
+ if (res != 0)
+ {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+ }
+
+ /**IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.*/
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
+ {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err)
+ {
+ Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats)
+ {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed)
+ {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ handler.post(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ resetLayoutByJoin();
+ audioSeatManager.upLocalSeat(uid);
+ }
+ });
+ }
+
+ @Override
+ public void onLocalAudioStats(LocalAudioStats stats) {
+ super.onLocalAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("sentSampleRate", stats.sentSampleRate + "");
+ _stats.put("sentBitrate", stats.sentBitrate + " kbps");
+ _stats.put("internalCodec", stats.internalCodec + "");
+ _stats.put("audioDeviceDelay", stats.audioDeviceDelay + " ms");
+ audioSeatManager.getLocalSeat().updateStats(_stats);
+ });
+ }
+
+ @Override
+ public void onRemoteAudioStats(RemoteAudioStats stats) {
+ super.onRemoteAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("numChannels", stats.numChannels + "");
+ _stats.put("receivedBitrate", stats.receivedBitrate + " kbps");
+ _stats.put("audioLossRate", stats.audioLossRate + "");
+ _stats.put("jitterBufferDelay", stats.jitterBufferDelay + " ms");
+ audioSeatManager.getRemoteSeat(stats.uid).updateStats(_stats);
+ });
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed)
+ {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ runOnUIThread(() -> audioSeatManager.upRemoteSeat(uid));
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason)
+ {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ runOnUIThread(() -> audioSeatManager.downSeat(uid));
+ }
+
+ @Override
+ public void onAudioMixingStateChanged(int state, int errorCode) {
+ showLongToast(String.format("onAudioMixingStateChanged %d error code:%d", state, errorCode));
+ }
+
+ @Override
+ public void onAudioMixingFinished() {
+ super.onAudioMixingFinished();
+ }
+ };
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if(seekBar.getId() == R.id.mixingPublishVolBar){
+ /**
+ * Adjusts the volume of audio mixing for publishing (sending to other users).
+ * @param volume: Audio mixing volume for publishing. The value ranges between 0 and 100 (default).
+ */
+ engine.adjustAudioMixingPublishVolume(progress);
+ }
+ else if(seekBar.getId() == R.id.mixingPlayoutVolBar){
+ /**
+ * Adjusts the volume of audio mixing for local playback.
+ * @param volume: Audio mixing volume for local playback. The value ranges between 0 and 100 (default).
+ */
+ engine.adjustAudioMixingPlayoutVolume(progress);
+ }
+ else if(seekBar.getId() == R.id.mixingVolBar){
+ /**
+ * Adjusts the volume of audio mixing.
+ * Call this method when you are in a channel.
+ * @param volume: Audio mixing volume. The value ranges between 0 and 100 (default).
+ */
+ engine.adjustAudioMixingVolume(progress);
+ }
+ else if(seekBar.getId() == R.id.effectVolBar){
+ engine.setEffectsVolume(progress);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java
new file mode 100644
index 000000000..faee2efad
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java
@@ -0,0 +1,340 @@
+package io.agora.api.example.examples.advanced;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.model.StatisticsInfo;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.EchoTestConfiguration;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.internal.LastmileProbeConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+@Example(
+ index = 16,
+ group = ADVANCED,
+ name = R.string.item_precalltest,
+ actionId = R.id.action_mainFragment_to_PreCallTest,
+ tipsId = R.string.precalltest
+)
+public class PreCallTest extends BaseFragment implements View.OnClickListener {
+ private static final String TAG = PreCallTest.class.getSimpleName();
+
+ private RtcEngine engine;
+ private int myUid;
+ private Button btn_lastmile, btn_echo;
+ private StatisticsInfo statisticsInfo;
+ private TextView lastmileQuality, lastmileResult;
+ private static final Integer MAX_COUNT_DOWN = 8;
+ private int num;
+ private Timer echoTimer;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_precall_test, container, false);
+ return view;
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime evepnts.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ statisticsInfo = new StatisticsInfo();
+ btn_echo = view.findViewById(R.id.btn_echo);
+ btn_echo.setOnClickListener(this);
+ btn_lastmile = view.findViewById(R.id.btn_lastmile);
+ btn_lastmile.setOnClickListener(this);
+ lastmileQuality = view.findViewById(R.id.lastmile_quality);
+ lastmileResult = view.findViewById(R.id.lastmile_result);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ RtcEngine.destroy();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_lastmile)
+ {
+ // Configure a LastmileProbeConfig instance.
+ LastmileProbeConfig config = new LastmileProbeConfig(){};
+ // Probe the uplink network quality.
+ config.probeUplink = true;
+ // Probe the downlink network quality.
+ config.probeDownlink = true;
+ // The expected uplink bitrate (bps). The value range is [100000, 5000000].
+ config.expectedUplinkBitrate = 100000;
+ // The expected downlink bitrate (bps). The value range is [100000, 5000000].
+ config.expectedDownlinkBitrate = 100000;
+ // Start the last-mile network test before joining the channel.
+ engine.startLastmileProbeTest(config);
+ btn_lastmile.setEnabled(false);
+ btn_lastmile.setText("Testing ...");
+ }
+ else if (v.getId() == R.id.btn_echo){
+ num = 0;
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+ EchoTestConfiguration config = new EchoTestConfiguration();
+ config.enableVideo = false;
+ config.enableAudio = true;
+ config.intervalInSeconds = MAX_COUNT_DOWN;
+ config.channelId = (new Random().nextInt(10000) + 100000) + "";
+ engine.startEchoTest(config);
+ btn_echo.setEnabled(false);
+ btn_echo.setText("Recording on Microphone ...");
+ echoTimer = new Timer(true);
+ echoTimer.schedule(new TimerTask(){
+ public void run() {
+ num++;
+ if(num >= MAX_COUNT_DOWN * 2){
+ handler.post(() -> {
+ btn_echo.setEnabled(true);
+ btn_echo.setText(R.string.start);
+ });
+ engine.stopEchoTest();
+ echoTimer.cancel();
+ }
+ else if(num >= MAX_COUNT_DOWN) {
+ handler.post(() -> btn_echo.setText("PLaying with " + (MAX_COUNT_DOWN * 2 - num) + "Seconds"));
+ }
+ else{
+ handler.post(() -> btn_echo.setText("Recording with " + (MAX_COUNT_DOWN - num) + "Seconds"));
+ }
+ }
+ }, 1000, 1000);
+ }
+ }
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats) {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ }
+
+ /**
+ * Implemented in the global IRtcEngineEventHandler class.
+ * Triggered 2 seconds after starting the last-mile test.
+ * @param quality
+ */
+ @Override
+ public void onLastmileQuality(int quality){
+ statisticsInfo.setLastMileQuality(quality);
+ updateLastMileResult();
+ }
+
+ /**
+ * Implemented in the global IRtcEngineEventHandler class.
+ * Triggered 30 seconds after starting the last-mile test.
+ * @param lastmileProbeResult
+ */
+ @Override
+ public void onLastmileProbeResult(LastmileProbeResult lastmileProbeResult) {
+ // (1) Stop the test. Agora recommends not calling any other API method before the test ends.
+ engine.stopLastmileProbeTest();
+ statisticsInfo.setLastMileProbeResult(lastmileProbeResult);
+ updateLastMileResult();
+ handler.post(() -> {
+ btn_lastmile.setEnabled(true);
+ btn_lastmile.setText(R.string.start);
+ });
+ }
+
+ };
+
+ private void updateLastMileResult() {
+ handler.post(() -> {
+ if(statisticsInfo.getLastMileQuality() != null){
+ lastmileQuality.setText("Quality: " + statisticsInfo.getLastMileQuality());
+ }
+ if(statisticsInfo.getLastMileResult() != null){
+ lastmileResult.setText(statisticsInfo.getLastMileResult());
+ }
+ });
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java
new file mode 100644
index 000000000..99e7d1561
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java
@@ -0,0 +1,456 @@
+package io.agora.api.example.examples.advanced;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IAudioFrameObserver;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.audio.AudioParams;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**
+ * This demo demonstrates how to make a one-to-one voice call
+ *
+ * @author cjw
+ */
+@Example(
+ index = 9,
+ group = ADVANCED,
+ name = R.string.item_raw_audio,
+ actionId = R.id.action_mainFragment_raw_audio,
+ tipsId = R.string.rawaudio
+)
+public class ProcessAudioRawData extends BaseFragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
+ private static final String TAG = ProcessAudioRawData.class.getSimpleName();
+ private EditText et_channel;
+ private Button join;
+ private Switch writeBackAudio;
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+ private volatile boolean isWriteBackAudio = false;
+ private static final Integer SAMPLE_RATE = 44100;
+ private static final Integer SAMPLE_NUM_OF_CHANNEL = 1;
+ private static final Integer SAMPLES = 1024;
+ private static final String AUDIO_FILE = "output.raw";
+ private InputStream inputStream;
+
+ private AudioSeatManager audioSeatManager;
+
+ private void openAudioFile(){
+ try {
+ inputStream = this.getResources().getAssets().open(AUDIO_FILE);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void closeAudioFile(){
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private byte[] readBuffer(){
+ int byteSize = SAMPLES * SAMPLE_NUM_OF_CHANNEL * 2;
+ byte[] buffer = new byte[byteSize];
+ try {
+ if(inputStream.read(buffer) < 0){
+ inputStream.reset();
+ return readBuffer();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return buffer;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_raw_audio, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ join = view.findViewById(R.id.btn_join);
+ et_channel = view.findViewById(R.id.et_channel);
+ view.findViewById(R.id.btn_join).setOnClickListener(this);
+ writeBackAudio = view.findViewById(R.id.writebackAudio);
+ writeBackAudio.setOnCheckedChangeListener(this);
+
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02),
+ view.findViewById(R.id.audio_place_03),
+ view.findViewById(R.id.audio_place_04),
+ view.findViewById(R.id.audio_place_05),
+ view.findViewById(R.id.audio_place_06),
+ view.findViewById(R.id.audio_place_07),
+ view.findViewById(R.id.audio_place_08),
+ view.findViewById(R.id.audio_place_09)
+ );
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ engine.registerAudioFrameObserver(iAudioFrameObserver);
+ engine.setRecordingAudioFrameParameters(SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL, Constants.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, SAMPLES);
+ engine.setPlaybackAudioFrameParameters(SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL, Constants.RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, SAMPLES);
+ openAudioFile();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ private byte[] audioAggregate(byte[] origin, byte[] buffer) {
+ byte[] output = new byte[buffer.length];
+ for (int i = 0; i < origin.length; i++) {
+ output[i] = (byte) ((long) origin[i] / 2 + (long) buffer[i] / 2);
+ if(i == 2){
+ Log.i(TAG, "origin :" + (int) origin[i] + " audio: " + (int) buffer[i]);
+ }
+ }
+ return output;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ /**leaveChannel and Destroy the RtcEngine instance*/
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ closeAudioFile();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_join) {
+ if (!joined) {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE)) {
+ joinChannel(channelId);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ Permission.Group.STORAGE,
+ Permission.Group.MICROPHONE
+ ).onGranted(permissions ->
+ {
+ // Permissions Granted
+ joinChannel(channelId);
+ }).start();
+ } else {
+ joined = false;
+ /**After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ join.setText(getString(R.string.join));
+ audioSeatManager.downAllSeats();
+ }
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.
+ */
+ private void joinChannel(String channelId) {
+ /**In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+ engine.setDefaultAudioRoutetoSpeakerphone(true);
+
+ /**Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+ int res = engine.joinChannel(ret, channelId, 0, option);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+
+
+ }
+
+ private final IAudioFrameObserver iAudioFrameObserver = new IAudioFrameObserver() {
+
+ @Override
+ public boolean onRecordAudioFrame(String channel, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, ByteBuffer byteBuffer, long renderTimeMs, int bufferLength) {
+ if(isWriteBackAudio){
+ int length = byteBuffer.remaining();
+// byteBuffer.flip();
+ byte[] buffer = readBuffer();
+ byte[] origin = new byte[length];
+ byteBuffer.get(origin);
+ byteBuffer.flip();
+ byteBuffer.put(audioAggregate(origin, buffer), 0, byteBuffer.remaining());
+ byteBuffer.flip();
+ }
+ return true;
+ }
+
+
+ @Override
+ public boolean onPlaybackAudioFrame(String channel, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, ByteBuffer byteBuffer, long renderTimeMs, int bufferLength) {
+ return false;
+ }
+
+ @Override
+ public boolean onMixedAudioFrame(String channel, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, ByteBuffer byteBuffer, long renderTimeMs, int bufferLength) {
+ return false;
+ }
+
+ @Override
+ public boolean onEarMonitoringAudioFrame(int type, int samplesPerChannel, int bytesPerSample, int channels, int samplesPerSec, ByteBuffer buffer, long renderTimeMs, int avsync_type) {
+ return false;
+ }
+
+ @Override
+ public boolean onPlaybackAudioFrameBeforeMixing(String channel, int uid, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, ByteBuffer byteBuffer, long renderTimeMs, int bufferLength) {
+ return false;
+ }
+
+ @Override
+ public int getObservedAudioFramePosition() {
+ return 0;
+ }
+
+ @Override
+ public AudioParams getRecordAudioParams() {
+ return null;
+ }
+
+ @Override
+ public AudioParams getPlaybackAudioParams() {
+ return null;
+ }
+
+ @Override
+ public AudioParams getMixedAudioParams() {
+ return null;
+ }
+
+ @Override
+ public AudioParams getEarMonitoringAudioParams() {
+ return null;
+ }
+
+ };
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats) {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ writeBackAudio.setEnabled(true);
+ audioSeatManager.upLocalSeat(uid);
+ }
+ });
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ runOnUIThread(() -> audioSeatManager.upRemoteSeat(uid));
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ runOnUIThread(() -> audioSeatManager.downSeat(uid));
+ }
+
+ @Override
+ public void onActiveSpeaker(int uid) {
+ super.onActiveSpeaker(uid);
+ Log.i(TAG, String.format("onActiveSpeaker:%d", uid));
+ }
+ };
+
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ isWriteBackAudio = b;
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java
new file mode 100644
index 000000000..87309531b
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java
@@ -0,0 +1,434 @@
+package io.agora.api.example.examples.advanced;
+
+import static io.agora.api.example.common.Constant.URL_DOWNBEAT;
+import static io.agora.api.example.common.Constant.URL_UPBEAT;
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.audio.AgoraRhythmPlayerConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**
+ * This demo demonstrates how to make a VideoProcessExtension
+ */
+@Example(
+ index = 19,
+ group = ADVANCED,
+ name = R.string.item_rhythmplayer,
+ actionId = R.id.action_mainFragment_rhythm_player,
+ tipsId = R.string.rhythmplayer
+)
+public class RhythmPlayer extends BaseFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener {
+
+ private static final String TAG = RhythmPlayer.class.getSimpleName();
+ private EditText et_channel;
+ private Button join, play, stop;
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+ private boolean isPlaying = false;
+ private SeekBar beatPerMinute, beatPerMeasure;
+ private AgoraRhythmPlayerConfig agoraRhythmPlayerConfig = new AgoraRhythmPlayerConfig();
+ private ChannelMediaOptions mChannelMediaOptions;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.fragment_rhythm_player, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
+ {
+ super.onViewCreated(view, savedInstanceState);
+ join = view.findViewById(R.id.btn_join);
+ play = view.findViewById(R.id.play);
+ stop = view.findViewById(R.id.stop);
+ et_channel = view.findViewById(R.id.et_channel);
+ beatPerMeasure = view.findViewById(R.id.beatsPerMeasure);
+ beatPerMinute = view.findViewById(R.id.beatsPerMinute);
+ view.findViewById(R.id.btn_join).setOnClickListener(this);
+ play.setOnClickListener(this);
+ stop.setOnClickListener(this);
+ beatPerMinute.setOnSeekBarChangeListener(this);
+ beatPerMeasure.setOnSeekBarChangeListener(this);
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState)
+ {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null)
+ {
+ return;
+ }
+ try
+ {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ super.onDestroy();
+ /**leaveChannel and Destroy the RtcEngine instance*/
+ if(engine != null)
+ {
+ engine.stopRhythmPlayer();
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+
+ @Override
+ public void onClick(View v)
+ {
+ if (v.getId() == R.id.btn_join)
+ {
+ if (!joined)
+ {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA))
+ {
+ joinChannel(channelId);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ Permission.Group.STORAGE,
+ Permission.Group.MICROPHONE
+ ).onGranted(permissions ->
+ {
+ // Permissions Granted
+ joinChannel(channelId);
+ }).start();
+ }
+ else
+ {
+ joined = false;
+ /**After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ join.setText(getString(R.string.join));
+ }
+ }
+ else if(v.getId() == R.id.play){
+ if(!isPlaying){
+ int ret = engine.startRhythmPlayer(URL_DOWNBEAT, URL_UPBEAT, agoraRhythmPlayerConfig);
+ if(joined){
+ mChannelMediaOptions.publishRhythmPlayerTrack = true;
+ engine.updateChannelMediaOptions(mChannelMediaOptions);
+ }
+ Log.i(TAG, "startRhythmPlayer result:" + ret);
+ isPlaying = true;
+ beatPerMeasure.setEnabled(false);
+ beatPerMinute.setEnabled(false);
+ }
+ }
+ else if(v.getId() == R.id.stop){
+ engine.stopRhythmPlayer();
+ if(joined){
+ mChannelMediaOptions.publishRhythmPlayerTrack = false;
+ engine.updateChannelMediaOptions(mChannelMediaOptions);
+ }
+ isPlaying = false;
+ beatPerMeasure.setEnabled(true);
+ beatPerMinute.setEnabled(true);
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.*/
+ private void joinChannel(String channelId)
+ {
+ /**In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+ engine.enableAudioVolumeIndication(1000, 3, true);
+
+ /**Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> {
+ /** Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+
+ mChannelMediaOptions = new ChannelMediaOptions();
+ mChannelMediaOptions.autoSubscribeAudio = true;
+ mChannelMediaOptions.autoSubscribeVideo = true;
+ mChannelMediaOptions.publishMicrophoneTrack = true;
+ /**
+ * config this for whether need push rhythem player to remote
+ */
+ mChannelMediaOptions.publishRhythmPlayerTrack = isPlaying;
+ int res = engine.joinChannel(accessToken, channelId, 0, mChannelMediaOptions);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+ }
+
+ /**IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.*/
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
+ {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err)
+ {
+ Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats)
+ {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed)
+ {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ handler.post(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ play.setEnabled(true);
+ stop.setEnabled(true);
+ beatPerMeasure.setEnabled(true);
+ beatPerMinute.setEnabled(true);
+ }
+ });
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed)
+ {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason)
+ {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ }
+
+ @Override
+ public void onActiveSpeaker(int uid) {
+ super.onActiveSpeaker(uid);
+ Log.i(TAG, String.format("onActiveSpeaker:%d", uid));
+ }
+ };
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if(seekBar.getId() == R.id.beatsPerMeasure){
+ agoraRhythmPlayerConfig.beatsPerMeasure = seekBar.getProgress() < 1 ? 1 : seekBar.getProgress();
+ }
+ else if(seekBar.getId() == R.id.beatsPerMinute){
+ agoraRhythmPlayerConfig.beatsPerMinute = seekBar.getProgress() < 60 ? 60 : seekBar.getProgress();
+ }
+ Log.i(TAG, "agoraRhythmPlayerConfig beatsPerMeasure:"+ agoraRhythmPlayerConfig.beatsPerMeasure +", beatsPerMinute:" + agoraRhythmPlayerConfig.beatsPerMinute);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java
new file mode 100644
index 000000000..36e5fd7c3
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java
@@ -0,0 +1,696 @@
+package io.agora.api.example.examples.advanced;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.SeekBar;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.bottomsheet.BottomSheetDialog;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.Constant;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.mediaplayer.Constants;
+import io.agora.mediaplayer.IMediaPlayer;
+import io.agora.mediaplayer.IMediaPlayerObserver;
+import io.agora.mediaplayer.data.CacheStatistics;
+import io.agora.mediaplayer.data.PlayerPlaybackStats;
+import io.agora.mediaplayer.data.PlayerUpdatedInfo;
+import io.agora.mediaplayer.data.SrcInfo;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.RtcEngineEx;
+import io.agora.rtc2.SpatialAudioParams;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+import io.agora.spatialaudio.ILocalSpatialAudioEngine;
+import io.agora.spatialaudio.LocalSpatialAudioConfig;
+import io.agora.spatialaudio.RemoteVoicePositionInfo;
+import io.agora.spatialaudio.SpatialAudioZone;
+
+/**
+ * The type Spatial sound.
+ */
+@Example(
+ index = 22,
+ group = ADVANCED,
+ name = R.string.item_spatial_sound,
+ actionId = R.id.action_mainFragment_to_spatial_sound,
+ tipsId = R.string.spatial_sound
+)
+public class SpatialSound extends BaseFragment {
+ private static final String TAG = SpatialSound.class.getSimpleName();
+
+ private static final int AXIS_MAX_DISTANCE = 10;
+
+ private View rootView;
+ private ImageView localIv, mediaPlayerLeftIv, mediaPlayerRightIv;
+ private TextView tipTv, remoteLeftTv, remoteRightTv, zoneTv;
+ private Button joinBtn;
+ private EditText channelIdEt;
+ private Switch switchMic, switchZone;
+
+
+ private RtcEngineEx engine;
+ private IMediaPlayer mediaPlayerLeft, mediaPlayerRight;
+ private volatile boolean isJoined;
+ private ILocalSpatialAudioEngine localSpatial;
+ private final InnerRtcEngineEventHandler iRtcEngineEventHandler = new InnerRtcEngineEventHandler();
+ private final Map cacheDialogs = new HashMap<>();
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_spatial_sound, container, false);
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ /*Creates an RtcEngine instance.
+ * @param context The context of Android Activity
+ * @param appId The App ID issued to you by Agora. See
+ * How to get the App ID
+ * @param handler IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.*/
+ String appId = getString(R.string.agora_app_id);
+ RtcEngineConfig config = new RtcEngineConfig();
+ config.mContext = getContext().getApplicationContext();
+ config.mAppId = appId;
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = (RtcEngineEx) RtcEngine.create(config);
+ /*
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ engine.enableAudio();
+
+ localSpatial = ILocalSpatialAudioEngine.create();
+ LocalSpatialAudioConfig localSpatialAudioConfig = new LocalSpatialAudioConfig();
+ localSpatialAudioConfig.mRtcEngine = engine;
+ localSpatial.initialize(localSpatialAudioConfig);
+
+ localSpatial.setMaxAudioRecvCount(2);
+ localSpatial.setAudioRecvRange(AXIS_MAX_DISTANCE);
+ localSpatial.setDistanceUnit(1);
+
+ engine.setChannelProfile(io.agora.rtc2.Constants.CHANNEL_PROFILE_LIVE_BROADCASTING);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ rootView = view.findViewById(R.id.root_view);
+ channelIdEt = view.findViewById(R.id.et_channel);
+ joinBtn = view.findViewById(R.id.btn_join);
+ tipTv = view.findViewById(R.id.tv_tip);
+ mediaPlayerLeftIv = view.findViewById(R.id.iv_mediaplayer_left);
+ mediaPlayerRightIv = view.findViewById(R.id.iv_mediaplayer_right);
+ localIv = view.findViewById(R.id.iv_local);
+ remoteLeftTv = view.findViewById(R.id.iv_remote_left);
+ remoteRightTv = view.findViewById(R.id.iv_remote_right);
+ tipTv.setText(R.string.spatial_sound_tip);
+ switchMic = view.findViewById(R.id.switch_microphone);
+ switchZone = view.findViewById(R.id.switch_zone);
+ zoneTv = view.findViewById(R.id.tv_zone);
+ zoneTv.setVisibility(View.INVISIBLE);
+
+ joinBtn.setOnClickListener(v -> {
+ CommonUtil.hideInputBoard(requireActivity(), channelIdEt);
+ if (!isJoined) {
+ joinChannel();
+ } else {
+ leftChannel();
+ }
+ });
+ localIv.setOnTouchListener(new ListenerOnTouchListener() {
+ @Override
+ protected void onPositionChanged() {
+ float[] pos = getVoicePosition(localIv);
+ float[] forward = new float[]{1.0F, 0.0F, 0.0F};
+ float[] right = new float[]{0.0F, 1.0F, 0.0F};
+ float[] up = new float[]{0.0F, 0.0F, 1.0F};
+ Log.d(TAG, "updateSelfPosition >> pos=" + Arrays.toString(pos));
+ localSpatial.updateSelfPosition(pos, forward, right, up);
+ }
+ });
+ switchMic.setOnCheckedChangeListener((buttonView, isChecked) ->
+ localSpatial.muteLocalAudioStream(!isChecked));
+ switchZone.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (isChecked) {
+ zoneTv.setVisibility(View.VISIBLE);
+
+ // create room
+ SpatialAudioZone mediaPlayerLeftZone = new SpatialAudioZone();
+ mediaPlayerLeftZone.zoneSetId = 1;
+ mediaPlayerLeftZone.audioAttenuation = 1f;
+ float[] voicePosition = getVoicePosition(zoneTv);
+ float[] viewRelativeSizeInAxis = getViewRelativeSizeInAxis(zoneTv);
+ mediaPlayerLeftZone.position = new float[]{voicePosition[0], voicePosition[1], 0};
+ mediaPlayerLeftZone.forward = new float[]{1.f, 0, 0};
+ mediaPlayerLeftZone.right = new float[]{0, 1.f, 0};
+ mediaPlayerLeftZone.up = new float[]{0, 0, 1.f};
+ mediaPlayerLeftZone.forwardLength = viewRelativeSizeInAxis[1];
+ mediaPlayerLeftZone.rightLength = viewRelativeSizeInAxis[0];
+ mediaPlayerLeftZone.upLength = AXIS_MAX_DISTANCE;
+ localSpatial.setZones(new SpatialAudioZone[]{mediaPlayerLeftZone});
+ } else {
+ zoneTv.setVisibility(View.INVISIBLE);
+ localSpatial.setZones(null);
+ }
+ });
+ }
+
+ private void joinChannel() {
+ String channelId = channelIdEt.getText().toString();
+
+ engine.setDefaultAudioRoutetoSpeakerphone(true);
+
+ engine.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER);
+
+ /*Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.autoSubscribeAudio = true;
+
+ /* Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ int res = engine.joinChannel(ret, channelId, 0, option);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ joinBtn.setEnabled(false);
+ });
+
+ localSpatial.muteLocalAudioStream(!switchMic.isChecked());
+
+ float[] pos = getVoicePosition(localIv);
+ float[] forward = new float[]{1.0F, 0.0F, 0.0F};
+ float[] right = new float[]{0.0F, 1.0F, 0.0F};
+ float[] up = new float[]{0.0F, 0.0F, 1.0F};
+ localSpatial.updateSelfPosition(pos, forward, right, up);
+ }
+
+ private void leftChannel() {
+ isJoined = false;
+
+ engine.leaveChannel();
+ localSpatial.clearRemotePositions();
+
+ isJoined = false;
+ joinBtn.setText(R.string.join);
+
+ mediaPlayerLeftIv.setVisibility(View.GONE);
+ mediaPlayerRightIv.setVisibility(View.GONE);
+ localIv.setTranslationX(0);
+ localIv.setTranslationY(0);
+ localIv.setVisibility(View.GONE);
+ remoteLeftTv.setTag(null);
+ remoteLeftTv.setVisibility(View.GONE);
+ remoteRightTv.setTag(null);
+ remoteRightTv.setVisibility(View.GONE);
+ tipTv.setVisibility(View.GONE);
+ zoneTv.setVisibility(View.GONE);
+ switchZone.setVisibility(View.GONE);
+ switchZone.setChecked(false);
+
+ cacheDialogs.clear();
+
+ unInitMediaPlayers();
+ }
+
+
+ private void initMediaPlayers() {
+ mediaPlayerLeft = createLoopMediaPlayer();
+ mediaPlayerLeft.open(Constant.URL_PLAY_AUDIO_FILES, 0);
+ localSpatial.updatePlayerPositionInfo(mediaPlayerLeft.getMediaPlayerId(), getVoicePositionInfo(mediaPlayerLeftIv));
+
+ mediaPlayerRight = createLoopMediaPlayer();
+ mediaPlayerRight.open(Constant.URL_DOWNBEAT, 0);
+ localSpatial.updatePlayerPositionInfo(mediaPlayerRight.getMediaPlayerId(), getVoicePositionInfo(mediaPlayerRightIv));
+
+ mediaPlayerLeftIv.setOnClickListener(v -> showMediaPlayerSettingDialog(mediaPlayerLeft));
+ mediaPlayerRightIv.setOnClickListener(v -> showMediaPlayerSettingDialog(mediaPlayerRight));
+ }
+
+ private void showMediaPlayerSettingDialog(IMediaPlayer mediaPlayer) {
+ String key = "MediaPlayer_" + mediaPlayer.getMediaPlayerId();
+ BottomSheetDialog dialog = cacheDialogs.get(key);
+ if (dialog != null) {
+ dialog.show();
+ return;
+ }
+ boolean isPlaying = mediaPlayer.getState() == Constants.MediaPlayerState.PLAYER_STATE_PAUSED;
+ SpatialAudioParams spatialAudioParams = new SpatialAudioParams();
+ dialog = showCommonSettingDialog(
+ isPlaying,
+ spatialAudioParams,
+ (buttonView, isChecked) -> {
+ if (isChecked) {
+ mediaPlayer.pause();
+ } else {
+ mediaPlayer.resume();
+ }
+ },
+ (buttonView, isChecked) -> {
+ spatialAudioParams.enable_blur = isChecked;
+ mediaPlayer.setSpatialAudioParams(spatialAudioParams);
+ },
+ (buttonView, isChecked) -> {
+ spatialAudioParams.enable_air_absorb = isChecked;
+ mediaPlayer.setSpatialAudioParams(spatialAudioParams);
+ },
+ new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ localSpatial.setPlayerAttenuation(mediaPlayer.getMediaPlayerId(), (double) (progress * 1.0f / seekBar.getMax()), false);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ }
+ );
+ cacheDialogs.put(key, dialog);
+ }
+
+ private void showRemoteUserSettingDialog(int uid) {
+ String key = "RemoteUser_" + uid;
+ BottomSheetDialog dialog = cacheDialogs.get(key);
+ if (dialog != null) {
+ dialog.show();
+ return;
+ }
+ SpatialAudioParams spatialAudioParams = new SpatialAudioParams();
+ dialog = showCommonSettingDialog(
+ false,
+ spatialAudioParams,
+ (buttonView, isChecked) -> {
+ localSpatial.muteRemoteAudioStream(uid, isChecked);
+ },
+ (buttonView, isChecked) -> {
+ spatialAudioParams.enable_blur = isChecked;
+ engine.setRemoteUserSpatialAudioParams(uid, spatialAudioParams);
+ },
+ (buttonView, isChecked) -> {
+ spatialAudioParams.enable_air_absorb = isChecked;
+ engine.setRemoteUserSpatialAudioParams(uid, spatialAudioParams);
+ },
+ new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ localSpatial.setRemoteAudioAttenuation(uid, (double) (progress * 1.0f / seekBar.getMax()), false);
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ }
+ );
+ cacheDialogs.put(key, dialog);
+ }
+
+
+ private void unInitMediaPlayers() {
+ mediaPlayerLeft.destroy();
+ mediaPlayerLeft = null;
+
+ mediaPlayerRight.destroy();
+ mediaPlayerRight = null;
+ }
+
+ private IMediaPlayer createLoopMediaPlayer() {
+ IMediaPlayer mediaPlayer = engine.createMediaPlayer();
+ mediaPlayer.registerPlayerObserver(new IMediaPlayerObserver() {
+ @Override
+ public void onPlayerStateChanged(Constants.MediaPlayerState state, Constants.MediaPlayerReason reason) {
+ if (state.equals(PLAYER_STATE_OPEN_COMPLETED)) {
+ mediaPlayer.setLoopCount(-1);
+ mediaPlayer.play();
+ }
+ }
+
+ @Override
+ public void onPositionChanged(long positionMs, long timestampMs) {
+
+ }
+
+ @Override
+ public void onPlayerEvent(Constants.MediaPlayerEvent eventCode, long elapsedTime, String message) {
+
+ }
+
+ @Override
+ public void onMetaData(Constants.MediaPlayerMetadataType type, byte[] data) {
+
+ }
+
+ @Override
+ public void onPlayBufferUpdated(long playCachedBuffer) {
+
+ }
+
+ @Override
+ public void onPreloadEvent(String src, Constants.MediaPlayerPreloadEvent event) {
+
+ }
+
+ @Override
+ public void onAgoraCDNTokenWillExpire() {
+
+ }
+
+ @Override
+ public void onPlayerSrcInfoChanged(SrcInfo from, SrcInfo to) {
+
+ }
+
+ @Override
+ public void onPlayerInfoUpdated(PlayerUpdatedInfo info) {
+
+ }
+
+ @Override
+ public void onPlayerCacheStats(CacheStatistics stats) {
+
+ }
+
+ @Override
+ public void onPlayerPlaybackStats(PlayerPlaybackStats stats) {
+
+ }
+
+ @Override
+ public void onAudioVolumeIndication(int volume) {
+
+ }
+ });
+ return mediaPlayer;
+ }
+
+ private RemoteVoicePositionInfo getVoicePositionInfo(View view) {
+ RemoteVoicePositionInfo positionInfo = new RemoteVoicePositionInfo();
+ positionInfo.forward = new float[]{1.0F, 0.0F, 0.0F};
+ positionInfo.position = getVoicePosition(view);
+ return positionInfo;
+ }
+
+ private float[] getVoicePosition(View view) {
+ float transX = view.getTranslationX();
+ float transY = view.getTranslationY();
+ double posForward = -1 * AXIS_MAX_DISTANCE * transY / ((rootView.getHeight()) / 2.0f);
+ double posRight = AXIS_MAX_DISTANCE * transX / ((rootView.getWidth()) / 2.0f);
+ //Log.d(TAG, "VoicePosition posForward=" + posForward + ", posRight=" + posRight);
+ return new float[]{(float) posForward, (float) posRight, 0.0F};
+ }
+
+ private float[] getViewRelativeSizeInAxis(View view) {
+ return new float[]{
+ AXIS_MAX_DISTANCE * view.getWidth() * 1.0f / (rootView.getWidth() / 2.0f),
+ AXIS_MAX_DISTANCE * view.getHeight() * 1.0f / (rootView.getHeight() / 2.0f),
+ };
+ }
+
+ private BottomSheetDialog showCommonSettingDialog(boolean isMute, SpatialAudioParams params,
+ CompoundButton.OnCheckedChangeListener muteCheckListener,
+ CompoundButton.OnCheckedChangeListener blurCheckListener,
+ CompoundButton.OnCheckedChangeListener airborneCheckListener,
+ SeekBar.OnSeekBarChangeListener attenuationSeekChangeListener
+ ) {
+ BottomSheetDialog dialog = new BottomSheetDialog(requireContext());
+ View dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_spatial_sound, null);
+ Switch muteSwitch = dialogView.findViewById(R.id.switch_mute);
+ muteSwitch.setChecked(isMute);
+ muteSwitch.setOnCheckedChangeListener(muteCheckListener);
+ Switch blurSwitch = dialogView.findViewById(R.id.switch_blur);
+ blurSwitch.setChecked(params.enable_blur != null && params.enable_blur);
+ blurSwitch.setOnCheckedChangeListener(blurCheckListener);
+ Switch airborneSwitch = dialogView.findViewById(R.id.switch_airborne);
+ airborneSwitch.setChecked(params.enable_air_absorb != null && params.enable_air_absorb);
+ airborneSwitch.setOnCheckedChangeListener(airborneCheckListener);
+ TextView attenuationTv = dialogView.findViewById(R.id.tv_attenuation);
+ SeekBar attenuationSb = dialogView.findViewById(R.id.sb_attenuation);
+ attenuationTv.setText(String.valueOf(params.speaker_attenuation == null ? 0.5 : params.speaker_attenuation));
+ attenuationSb.setProgress((int) ((params.speaker_attenuation == null ? 0.5 : params.speaker_attenuation) * attenuationSb.getMax()));
+ attenuationSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ float value = progress * 1.0f / seekBar.getMax();
+ attenuationTv.setText(String.valueOf(value));
+ if (attenuationSeekChangeListener != null) {
+ attenuationSeekChangeListener.onProgressChanged(seekBar, progress, fromUser);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+
+ dialog.setContentView(dialogView);
+ dialog.setCanceledOnTouchOutside(true);
+ dialog.show();
+ return dialog;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ handler.removeCallbacksAndMessages(null);
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+ private abstract static class ListenerOnTouchListener implements View.OnTouchListener {
+ private float startX, startY, tranX, tranY, curX, curY, maxX, maxY, minX, minY;
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ startX = event.getRawX();
+ startY = event.getRawY();
+ tranX = v.getTranslationX();
+ tranY = v.getTranslationY();
+ if (v.getParent() instanceof ViewGroup) {
+ maxX = (((ViewGroup) v.getParent()).getWidth() - v.getWidth() + 1) / 2;
+ maxY = (((ViewGroup) v.getParent()).getHeight() - v.getHeight() + 1) / 2;
+ minX = -maxX;
+ minY = -maxY;
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ curX = event.getRawX();
+ curY = event.getRawY();
+ float newTranX = tranX + curX - startX;
+ if (minX != 0 && newTranX < minX) {
+ newTranX = minX;
+ }
+ if (maxX != 0 && newTranX > maxX) {
+ newTranX = maxX;
+ }
+ v.setTranslationX(newTranX);
+ float newTranY = tranY + curY - startY;
+ if (minY != 0 && newTranY < minY) {
+ newTranY = minY;
+ }
+ if (maxY != 0 && newTranY > maxY) {
+ newTranY = maxY;
+ }
+ v.setTranslationY(newTranY);
+ onPositionChanged();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * On position changed.
+ */
+ protected abstract void onPositionChanged();
+ }
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final class InnerRtcEngineEventHandler extends IRtcEngineEventHandler {
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ super.onJoinChannelSuccess(channel, uid, elapsed);
+
+ isJoined = true;
+
+ runOnUIThread(() -> {
+ joinBtn.setEnabled(true);
+ joinBtn.setText(R.string.leave);
+
+ mediaPlayerLeftIv.setVisibility(View.VISIBLE);
+ mediaPlayerRightIv.setVisibility(View.VISIBLE);
+ localIv.setVisibility(View.VISIBLE);
+ tipTv.setVisibility(View.VISIBLE);
+ switchZone.setVisibility(View.VISIBLE);
+
+ initMediaPlayers();
+ });
+ }
+
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**
+ * Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ *
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.
+ */
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ runOnUIThread(() -> {
+ if (remoteLeftTv.getTag() == null) {
+ remoteLeftTv.setTag(uid);
+ remoteLeftTv.setVisibility(View.VISIBLE);
+ remoteLeftTv.setText(uid + "");
+ RemoteVoicePositionInfo info = getVoicePositionInfo(remoteLeftTv);
+ Log.d(TAG, "left remote user >> pos=" + Arrays.toString(info.position));
+ localSpatial.updateRemotePosition(uid, info);
+
+ remoteLeftTv.setOnClickListener(v -> showRemoteUserSettingDialog(uid));
+ } else if (remoteRightTv.getTag() == null) {
+ remoteRightTv.setTag(uid);
+ remoteRightTv.setVisibility(View.VISIBLE);
+ remoteRightTv.setText(uid + "");
+ localSpatial.updateRemotePosition(uid, getVoicePositionInfo(remoteRightTv));
+
+ remoteRightTv.setOnClickListener(v -> showRemoteUserSettingDialog(uid));
+ }
+ });
+ }
+
+ /**
+ * Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ *
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.
+ */
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ runOnUIThread(() -> {
+ if (remoteLeftTv.getTag() instanceof Integer && (int) remoteLeftTv.getTag() == uid) {
+ remoteLeftTv.setTag(null);
+ remoteLeftTv.setVisibility(View.GONE);
+ localSpatial.removeRemotePosition(uid);
+ } else if (remoteRightTv.getTag() instanceof Integer && (int) remoteRightTv.getTag() == uid) {
+ remoteRightTv.setTag(null);
+ remoteRightTv.setVisibility(View.GONE);
+ localSpatial.removeRemotePosition(uid);
+ }
+ });
+ }
+
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java
new file mode 100644
index 000000000..ef1abdbf1
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java
@@ -0,0 +1,854 @@
+package io.agora.api.example.examples.advanced;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+import static io.agora.rtc2.Constants.AUDIO_EFFECT_OFF;
+import static io.agora.rtc2.Constants.CHAT_BEAUTIFIER_FRESH;
+import static io.agora.rtc2.Constants.CHAT_BEAUTIFIER_MAGNETIC;
+import static io.agora.rtc2.Constants.CHAT_BEAUTIFIER_VITALITY;
+import static io.agora.rtc2.Constants.PITCH_CORRECTION;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_3D_VOICE;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_ETHEREAL;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_KTV;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_PHONOGRAPH;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_SPACIAL;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_STUDIO;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_VIRTUAL_STEREO;
+import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_VOCAL_CONCERT;
+import static io.agora.rtc2.Constants.STYLE_TRANSFORMATION_POPULAR;
+import static io.agora.rtc2.Constants.STYLE_TRANSFORMATION_RNB;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_CLEAR;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_DEEP;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_FALSETTO;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_FULL;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_MELLOW;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_RESOUNDING;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_RINGING;
+import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_VIGOROUS;
+import static io.agora.rtc2.Constants.ULTRA_HIGH_QUALITY_VOICE;
+import static io.agora.rtc2.Constants.VOICE_BEAUTIFIER_OFF;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_BASS;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_CARTOON;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_CHILDLIKE;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_CHIPMUNK;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_DARTH_VADER;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_BOY;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_GIRL;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_HULK;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_OLDMAN;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_PIGKING;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_SISTER;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_UNCLE;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_GIRLISH_MAN;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_GROOT;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_IRON_LADY;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_MONSTER;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_NEUTRAL;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_PHONE_OPERATOR;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_SHIN_CHAN;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_SOLID;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_SWEET;
+import static io.agora.rtc2.Constants.VOICE_CHANGER_TRANSFORMERS;
+import static io.agora.rtc2.Constants.VOICE_CONVERSION_OFF;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+@Example(
+ index = 4,
+ group = ADVANCED,
+ name = R.string.item_voiceeffects,
+ actionId = R.id.action_mainFragment_to_VoiceEffects,
+ tipsId = R.string.voiceeffects
+)
+public class VoiceEffects extends BaseFragment implements View.OnClickListener, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener, SeekBar.OnSeekBarChangeListener {
+ private static final String TAG = VoiceEffects.class.getSimpleName();
+
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+
+ private EditText et_channel;
+ private Button join;
+ private Spinner audioProfile, audioScenario,
+ chatBeautifier, timbreTransformation, voiceChanger, styleTransformation, roomAcoustics, pitchCorrection, _pitchModeOption, _pitchValueOption, voiceConversion, ainsMode,
+ customBandFreq, customReverbKey;
+ private ViewGroup _voice3DLayout, _pitchModeLayout, _pitchValueLayout;
+ private SeekBar _voice3DCircle, customPitch, customBandGain, customReverbValue, customVoiceFormant;
+
+ private AudioSeatManager audioSeatManager;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_voice_effects, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // Join layout
+ join = view.findViewById(R.id.btn_join);
+ audioProfile = view.findViewById(R.id.audio_profile_spinner);
+ audioScenario = view.findViewById(R.id.audio_scenario_spinner);
+ et_channel = view.findViewById(R.id.et_channel);
+
+ audioScenario.setOnItemSelectedListener(this);
+ join.setOnClickListener(this);
+
+ // Voice Beautifier / Effects Preset layout
+ chatBeautifier = view.findViewById(R.id.audio_chat_beautifier);
+ timbreTransformation = view.findViewById(R.id.audio_timbre_transformation);
+ voiceChanger = view.findViewById(R.id.audio_voice_changer);
+ styleTransformation = view.findViewById(R.id.audio_style_transformation);
+ roomAcoustics = view.findViewById(R.id.audio_room_acoustics);
+ _voice3DLayout = view.findViewById(R.id.audio_3d_voice_layout);
+ _voice3DCircle = view.findViewById(R.id.audio_3d_voice_circle);
+ pitchCorrection = view.findViewById(R.id.audio_pitch_correction);
+ _pitchModeLayout = view.findViewById(R.id.audio_pitch_mode_layout);
+ _pitchModeOption = view.findViewById(R.id.audio_pitch_mode_option);
+ _pitchValueLayout = view.findViewById(R.id.audio_pitch_value_layout);
+ _pitchValueOption = view.findViewById(R.id.audio_pitch_value_option);
+ voiceConversion = view.findViewById(R.id.audio_voice_conversion);
+ ainsMode = view.findViewById(R.id.audio_ains_mode);
+
+ chatBeautifier.setOnItemSelectedListener(this);
+ timbreTransformation.setOnItemSelectedListener(this);
+ voiceChanger.setOnItemSelectedListener(this);
+ styleTransformation.setOnItemSelectedListener(this);
+ roomAcoustics.setOnItemSelectedListener(this);
+ pitchCorrection.setOnItemSelectedListener(this);
+ voiceConversion.setOnItemSelectedListener(this);
+ _voice3DCircle.setOnSeekBarChangeListener(this);
+ _pitchModeOption.setOnItemSelectedListener(this);
+ _pitchValueOption.setOnItemSelectedListener(this);
+ ainsMode.setOnItemSelectedListener(this);
+
+ // Customize Voice Effects Layout
+ customPitch = view.findViewById(R.id.audio_custom_pitch); // engine.setLocalVoicePitch()
+ customBandFreq = view.findViewById(R.id.audio_custom_band_freq); // engine.setLocalVoiceEqualization()
+ customBandGain = view.findViewById(R.id.audio_custom_band_gain); // engine.setLocalVoiceEqualization()
+ customReverbKey = view.findViewById(R.id.audio_custom_reverb_key);
+ customReverbValue = view.findViewById(R.id.audio_custom_reverb_value); //engine.setLocalVoiceReverb()
+ customVoiceFormant = view.findViewById(R.id.audio_voice_formant_value); //engine.setLocalVoiceFormant()
+
+ customPitch.setOnSeekBarChangeListener(this);
+ customBandGain.setOnSeekBarChangeListener(this);
+ customReverbValue.setOnSeekBarChangeListener(this);
+ customVoiceFormant.setOnSeekBarChangeListener(this);
+ customBandFreq.setOnItemSelectedListener(this);
+ customReverbKey.setOnItemSelectedListener(this);
+
+
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02)
+ );
+
+ resetControlLayoutByJoined();
+ }
+
+ private void resetControlLayoutByJoined() {
+ audioProfile.setEnabled(!joined);
+
+ chatBeautifier.setEnabled(joined);
+ timbreTransformation.setEnabled(joined);
+ voiceChanger.setEnabled(joined);
+ styleTransformation.setEnabled(joined);
+ roomAcoustics.setEnabled(joined);
+ _voice3DLayout.setVisibility(View.GONE);
+ pitchCorrection.setEnabled(joined);
+ _pitchModeLayout.setVisibility(View.GONE);
+ _pitchValueLayout.setVisibility(View.GONE);
+ voiceConversion.setEnabled(joined);
+ ainsMode.setEnabled(joined);
+
+ customPitch.setEnabled(joined);
+ customBandFreq.setEnabled(joined);
+ customBandGain.setEnabled(joined);
+ customReverbKey.setEnabled(joined);
+ customReverbValue.setEnabled(joined);
+ customVoiceFormant.setEnabled(joined);
+
+
+ chatBeautifier.setSelection(0);
+ voiceChanger.setSelection(0);
+ timbreTransformation.setSelection(0);
+ roomAcoustics.setSelection(0);
+ pitchCorrection.setSelection(0);
+ voiceConversion.setSelection(0);
+ ainsMode.setSelection(0);
+
+ customPitch.setProgress(0);
+ customBandGain.setProgress(0);
+ customReverbValue.setProgress(0);
+ customVoiceFormant.setProgress(50);
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ /**leaveChannel and Destroy the RtcEngine instance*/
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_join) {
+ if (!joined) {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) {
+ joinChannel(channelId);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ Permission.Group.STORAGE,
+ Permission.Group.MICROPHONE
+ ).onGranted(permissions ->
+ {
+ // Permissions Granted
+ joinChannel(channelId);
+ }).start();
+ } else {
+ joined = false;
+ resetControlLayoutByJoined();
+ /**After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ join.setText(getString(R.string.join));
+ audioSeatManager.downAllSeats();
+ }
+ }
+ }
+
+ private int getPitch1Value(String str) {
+ switch (str) {
+ case "Natural Minor":
+ return 2;
+ case "Breeze Minor":
+ return 3;
+ default:
+ return 1;
+ }
+ }
+
+ private int getPitch2Value(String str) {
+ switch (str) {
+ case "A Pitch":
+ return 1;
+ case "A# Pitch":
+ return 2;
+ case "B Pitch":
+ return 3;
+ case "C# Pitch":
+ return 5;
+ case "D Pitch":
+ return 6;
+ case "D# Pitch":
+ return 7;
+ case "E Pitch":
+ return 8;
+ case "F Pitch":
+ return 9;
+ case "F# Pitch":
+ return 10;
+ case "G Pitch":
+ return 11;
+ case "G# Pitch":
+ return 12;
+ default:
+ return 4;
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.
+ */
+ private void joinChannel(String channelId) {
+ /**In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+ // audio config
+ engine.setAudioProfile(
+ Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString())),
+ Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString()))
+ );
+
+ /**Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> {
+ /** Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+ int res = engine.joinChannel(accessToken, channelId, 0, option);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+ }
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats) {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ resetControlLayoutByJoined();
+ audioSeatManager.upLocalSeat(uid);
+ }
+ });
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ runOnUIThread(() -> audioSeatManager.upRemoteSeat(uid));
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ runOnUIThread(() -> audioSeatManager.downSeat(uid));
+ }
+
+ @Override
+ public void onLocalAudioStats(LocalAudioStats stats) {
+ super.onLocalAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("sentSampleRate", stats.sentSampleRate + "");
+ _stats.put("sentBitrate", stats.sentBitrate + " kbps");
+ _stats.put("internalCodec", stats.internalCodec + "");
+ _stats.put("audioDeviceDelay", stats.audioDeviceDelay + " ms");
+ audioSeatManager.getLocalSeat().updateStats(_stats);
+ });
+ }
+
+ @Override
+ public void onRemoteAudioStats(RemoteAudioStats stats) {
+ super.onRemoteAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("numChannels", stats.numChannels + "");
+ _stats.put("receivedBitrate", stats.receivedBitrate + " kbps");
+ _stats.put("audioLossRate", stats.audioLossRate + "");
+ _stats.put("jitterBufferDelay", stats.jitterBufferDelay + " ms");
+ audioSeatManager.getRemoteSeat(stats.uid).updateStats(_stats);
+ });
+ }
+ };
+
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (parent.getTag() != null) {
+ parent.setTag(null);
+ return;
+ }
+
+ if (parent == audioScenario) {
+ engine.setAudioScenario(Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())));
+ return;
+ }
+
+ // Voice Beautifier / Effects Preset layout
+ List voiceBeautifierSpinner = Arrays.asList(chatBeautifier, timbreTransformation);
+ if (voiceBeautifierSpinner.contains(parent)) {
+ String item = parent.getSelectedItem().toString();
+ int voiceBeautifierValue = getVoiceBeautifierValue(item);
+ engine.setVoiceBeautifierPreset(voiceBeautifierValue);
+
+ for (Spinner spinner : voiceBeautifierSpinner) {
+ if (spinner != parent) {
+ if(spinner.getSelectedItemPosition() != 0){
+ spinner.setTag("reset");
+ spinner.setSelection(0);
+ }
+ }
+ }
+ return;
+ }
+
+ List audioEffectSpinner = Arrays.asList(voiceChanger, styleTransformation, roomAcoustics, pitchCorrection);
+ if (audioEffectSpinner.contains(parent)) {
+ String item = parent.getSelectedItem().toString();
+ int audioEffectPreset = getAudioEffectPreset(item);
+ engine.setAudioEffectPreset(audioEffectPreset);
+
+ for (Spinner spinner : audioEffectSpinner) {
+ if (spinner != parent) {
+ if(spinner.getSelectedItemPosition() != 0){
+ spinner.setTag("reset");
+ spinner.setSelection(0);
+ }
+ }
+ }
+
+ _voice3DLayout.setVisibility(audioEffectPreset == ROOM_ACOUSTICS_3D_VOICE ? View.VISIBLE: View.GONE);
+ _pitchModeLayout.setVisibility(audioEffectPreset == PITCH_CORRECTION ? View.VISIBLE : View.GONE);
+ _pitchValueLayout.setVisibility(audioEffectPreset == PITCH_CORRECTION ? View.VISIBLE : View.GONE);
+ return;
+ }
+
+ if(parent == voiceConversion){
+ String item = parent.getSelectedItem().toString();
+ engine.setVoiceConversionPreset(getVoiceConversionValue(item));
+ return;
+ }
+
+
+ if(parent == _pitchModeOption || parent == _pitchValueOption){
+ int effectOption1 = getPitch1Value(_pitchModeOption.getSelectedItem().toString());
+ int effectOption2 = getPitch2Value(_pitchValueOption.getSelectedItem().toString());
+ engine.setAudioEffectParameters(PITCH_CORRECTION, effectOption1, effectOption2);
+ }
+
+ if(parent == ainsMode){
+ boolean enable = position > 0;
+ /*
+ The AI noise suppression modes:
+ 0: (Default) Balance mode. This mode allows for a balanced performance on noice suppression and time delay.
+ 1: Aggressive mode. In scenarios where high performance on noise suppression is required, such as live streaming
+ outdoor events, this mode reduces nosies more dramatically, but sometimes may affect the original character of the audio.
+ 2: Aggressive mode with low latency. The noise suppression delay of this mode is about only half of that of the balance
+ and aggressive modes. It is suitable for scenarios that have high requirements on noise suppression with low latency,
+ such as sing together online in real time.
+ */
+ engine.setAINSMode(enable, position - 1);
+ }
+ }
+
+ private int getVoiceConversionValue(String label) {
+ switch (label) {
+ case "VOICE_CHANGER_NEUTRAL":
+ return VOICE_CHANGER_NEUTRAL;
+ case "VOICE_CHANGER_SWEET":
+ return VOICE_CHANGER_SWEET;
+ case "VOICE_CHANGER_SOLID":
+ return VOICE_CHANGER_SOLID;
+ case "VOICE_CHANGER_BASS":
+ return VOICE_CHANGER_BASS;
+ case "VOICE_CHANGER_CARTOON":
+ return VOICE_CHANGER_CARTOON;
+ case "VOICE_CHANGER_CHILDLIKE":
+ return VOICE_CHANGER_CHILDLIKE;
+ case "VOICE_CHANGER_PHONE_OPERATOR":
+ return VOICE_CHANGER_PHONE_OPERATOR;
+ case "VOICE_CHANGER_MONSTER":
+ return VOICE_CHANGER_MONSTER;
+ case "VOICE_CHANGER_TRANSFORMERS":
+ return VOICE_CHANGER_TRANSFORMERS;
+ case "VOICE_CHANGER_GROOT":
+ return VOICE_CHANGER_GROOT;
+ case "VOICE_CHANGER_DARTH_VADER":
+ return VOICE_CHANGER_DARTH_VADER;
+ case "VOICE_CHANGER_IRON_LADY":
+ return VOICE_CHANGER_IRON_LADY;
+ case "VOICE_CHANGER_SHIN_CHAN":
+ return VOICE_CHANGER_SHIN_CHAN;
+ case "VOICE_CHANGER_GIRLISH_MAN":
+ return VOICE_CHANGER_GIRLISH_MAN;
+ case "VOICE_CHANGER_CHIPMUNK":
+ return VOICE_CHANGER_CHIPMUNK;
+ case "VOICE_CONVERSION_OFF":
+ default:
+ return VOICE_CONVERSION_OFF;
+ }
+ }
+
+ private int getVoiceBeautifierValue(String label) {
+ int value;
+ switch (label) {
+ case "CHAT_BEAUTIFIER_MAGNETIC":
+ value = CHAT_BEAUTIFIER_MAGNETIC;
+ break;
+ case "CHAT_BEAUTIFIER_FRESH":
+ value = CHAT_BEAUTIFIER_FRESH;
+ break;
+ case "CHAT_BEAUTIFIER_VITALITY":
+ value = CHAT_BEAUTIFIER_VITALITY;
+ break;
+ case "TIMBRE_TRANSFORMATION_VIGOROUS":
+ value = TIMBRE_TRANSFORMATION_VIGOROUS;
+ break;
+ case "TIMBRE_TRANSFORMATION_DEEP":
+ value = TIMBRE_TRANSFORMATION_DEEP;
+ break;
+ case "TIMBRE_TRANSFORMATION_MELLOW":
+ value = TIMBRE_TRANSFORMATION_MELLOW;
+ break;
+ case "TIMBRE_TRANSFORMATION_FALSETTO":
+ value = TIMBRE_TRANSFORMATION_FALSETTO;
+ break;
+ case "TIMBRE_TRANSFORMATION_FULL":
+ value = TIMBRE_TRANSFORMATION_FULL;
+ break;
+ case "TIMBRE_TRANSFORMATION_CLEAR":
+ value = TIMBRE_TRANSFORMATION_CLEAR;
+ break;
+ case "TIMBRE_TRANSFORMATION_RESOUNDING":
+ value = TIMBRE_TRANSFORMATION_RESOUNDING;
+ break;
+ case "TIMBRE_TRANSFORMATION_RINGING":
+ value = TIMBRE_TRANSFORMATION_RINGING;
+ break;
+ case "ULTRA_HIGH_QUALITY_VOICE":
+ value = ULTRA_HIGH_QUALITY_VOICE;
+ break;
+ default:
+ value = VOICE_BEAUTIFIER_OFF;
+ }
+ return value;
+ }
+
+ private int getAudioEffectPreset(String label) {
+ int value;
+ switch (label) {
+ case "ROOM_ACOUSTICS_KTV":
+ value = ROOM_ACOUSTICS_KTV;
+ break;
+ case "ROOM_ACOUSTICS_VOCAL_CONCERT":
+ value = ROOM_ACOUSTICS_VOCAL_CONCERT;
+ break;
+ case "ROOM_ACOUSTICS_STUDIO":
+ value = ROOM_ACOUSTICS_STUDIO;
+ break;
+ case "ROOM_ACOUSTICS_PHONOGRAPH":
+ value = ROOM_ACOUSTICS_PHONOGRAPH;
+ break;
+ case "ROOM_ACOUSTICS_VIRTUAL_STEREO":
+ value = ROOM_ACOUSTICS_VIRTUAL_STEREO;
+ break;
+ case "ROOM_ACOUSTICS_SPACIAL":
+ value = ROOM_ACOUSTICS_SPACIAL;
+ break;
+ case "ROOM_ACOUSTICS_ETHEREAL":
+ value = ROOM_ACOUSTICS_ETHEREAL;
+ break;
+ case "ROOM_ACOUSTICS_3D_VOICE":
+ value = ROOM_ACOUSTICS_3D_VOICE;
+ break;
+ case "VOICE_CHANGER_EFFECT_UNCLE":
+ value = VOICE_CHANGER_EFFECT_UNCLE;
+ break;
+ case "VOICE_CHANGER_EFFECT_OLDMAN":
+ value = VOICE_CHANGER_EFFECT_OLDMAN;
+ break;
+ case "VOICE_CHANGER_EFFECT_BOY":
+ value = VOICE_CHANGER_EFFECT_BOY;
+ break;
+ case "VOICE_CHANGER_EFFECT_SISTER":
+ value = VOICE_CHANGER_EFFECT_SISTER;
+ break;
+ case "VOICE_CHANGER_EFFECT_GIRL":
+ value = VOICE_CHANGER_EFFECT_GIRL;
+ break;
+ case "VOICE_CHANGER_EFFECT_PIGKING":
+ value = VOICE_CHANGER_EFFECT_PIGKING;
+ break;
+ case "VOICE_CHANGER_EFFECT_HULK":
+ value = VOICE_CHANGER_EFFECT_HULK;
+ break;
+ case "STYLE_TRANSFORMATION_RNB":
+ value = STYLE_TRANSFORMATION_RNB;
+ break;
+ case "STYLE_TRANSFORMATION_POPULAR":
+ value = STYLE_TRANSFORMATION_POPULAR;
+ break;
+ case "PITCH_CORRECTION":
+ value = PITCH_CORRECTION;
+ break;
+ default:
+ value = AUDIO_EFFECT_OFF;
+ }
+ return value;
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (!fromUser) {
+ return;
+ }
+ if(seekBar == _voice3DCircle){
+ int cicle = (int) (1 + 59 * progress * 1.0f / seekBar.getMax());
+ // [1,60], 10 default
+ engine.setAudioEffectParameters(ROOM_ACOUSTICS_3D_VOICE, cicle, 0);
+ }else if(seekBar == customPitch){
+ double pitch = 0.5 + 1.5 * progress * 1.0f / seekBar.getMax();
+ // pitch: [0.5,2.0], 1.0 default
+ engine.setLocalVoicePitch(pitch);
+ } else if (seekBar == customBandGain) {
+ int value = (int) (-15 + 30 * progress * 1.0f / seekBar.getMax());
+ // [-15,15], 0 default
+ engine.setLocalVoiceEqualization(Constants.AUDIO_EQUALIZATION_BAND_FREQUENCY.valueOf(customBandFreq.getSelectedItem().toString()), value);
+ } else if (seekBar == customReverbValue) {
+ Constants.AUDIO_REVERB_TYPE reverbKey = Constants.AUDIO_REVERB_TYPE.valueOf(customReverbKey.getSelectedItem().toString());
+ int value;
+ // AUDIO_REVERB_DRY_LEVEL(0):dry signal, [-20, 10] dB
+ // AUDIO_REVERB_WET_LEVEL(1):wet signal, [-20, 10] dB
+ // AUDIO_REVERB_ROOM_SIZE(2):[0, 100] dB
+ // AUDIO_REVERB_WET_DELAY(3):Wet signal, [0, 200] ms
+ // AUDIO_REVERB_STRENGTH(4): [0, 100]
+ if(reverbKey == Constants.AUDIO_REVERB_TYPE.AUDIO_REVERB_DRY_LEVEL || reverbKey == Constants.AUDIO_REVERB_TYPE.AUDIO_REVERB_WET_LEVEL){
+ value = (int) (-20 + 30 * progress * 1.0f / seekBar.getMax());
+ }else if(reverbKey == Constants.AUDIO_REVERB_TYPE.AUDIO_REVERB_WET_DELAY){
+ value = (int) (200 * progress * 1.0f / seekBar.getMax());
+ }else {
+ value = (int) (100 * progress * 1.0f / seekBar.getMax());
+ }
+ engine.setLocalVoiceReverb(reverbKey, value);
+ } else if (seekBar == customVoiceFormant) {
+ // [-1, 1]
+ double value = (progress - 50) * 1.0f / 100;
+ engine.setLocalVoiceFormant(value);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioPlayer.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioPlayer.java
new file mode 100644
index 000000000..84c814cef
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioPlayer.java
@@ -0,0 +1,73 @@
+package io.agora.api.example.examples.advanced.customaudio;
+
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.util.Log;
+
+public class AudioPlayer {
+
+ private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM;
+ private static final String TAG = "AudioPlayer";
+
+ private AudioTrack mAudioTrack;
+ private AudioStatus mAudioStatus = AudioStatus.STOPPED ;
+
+ public AudioPlayer(int streamType, int sampleRateInHz, int channelConfig, int audioFormat){
+ if(mAudioStatus == AudioStatus.STOPPED) {
+ int Val = 0;
+ if(1 == channelConfig)
+ Val = AudioFormat.CHANNEL_OUT_MONO;
+ else if(2 == channelConfig)
+ Val = AudioFormat.CHANNEL_OUT_STEREO;
+ else
+ Log.e(TAG, "channelConfig is wrong !");
+
+ int mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, Val, audioFormat);
+ Log.e(TAG, " sampleRateInHz :" + sampleRateInHz + " channelConfig :" + channelConfig + " audioFormat: " + audioFormat + " mMinBufferSize: " + mMinBufferSize);
+ if (mMinBufferSize == AudioTrack.ERROR_BAD_VALUE) {
+ Log.e(TAG,"AudioTrack.ERROR_BAD_VALUE : " + AudioTrack.ERROR_BAD_VALUE) ;
+ }
+
+ mAudioTrack = new AudioTrack(streamType, sampleRateInHz, Val, audioFormat, mMinBufferSize, DEFAULT_PLAY_MODE);
+ if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) {
+ throw new RuntimeException("Error on AudioTrack created");
+ }
+ mAudioStatus = AudioStatus.INITIALISING;
+ }
+ Log.e(TAG, "mAudioStatus: " + mAudioStatus);
+ }
+
+ public boolean startPlayer() {
+ if(mAudioStatus == AudioStatus.INITIALISING) {
+ mAudioTrack.play();
+ mAudioStatus = AudioStatus.RUNNING;
+ }
+ Log.e("AudioPlayer", "mAudioStatus: " + mAudioStatus);
+ return true;
+ }
+
+ public void stopPlayer() {
+ if(null != mAudioTrack){
+ mAudioStatus = AudioStatus.STOPPED;
+ mAudioTrack.stop();
+ mAudioTrack.release();
+ mAudioTrack = null;
+ }
+ Log.e(TAG, "mAudioStatus: " + mAudioStatus);
+ }
+
+ public boolean play(byte[] audioData, int offsetInBytes, int sizeInBytes) {
+ if(mAudioStatus == AudioStatus.RUNNING) {
+ mAudioTrack.write(audioData, offsetInBytes, sizeInBytes);
+ }else{
+ Log.e(TAG, "=== No data to AudioTrack !! mAudioStatus: " + mAudioStatus);
+ }
+ return true;
+ }
+
+ public enum AudioStatus {
+ INITIALISING,
+ RUNNING,
+ STOPPED
+ }
+}
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java
new file mode 100755
index 000000000..479854f9a
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java
@@ -0,0 +1,353 @@
+package io.agora.api.example.examples.advanced.customaudio;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+
+import android.content.Context;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Process;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import java.nio.ByteBuffer;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.RtcEngineEx;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**
+ * This demo demonstrates how to make a one-to-one voice call
+ */
+@Example(index = 6, group = ADVANCED, name = R.string.item_customaudiorender, actionId = R.id.action_mainFragment_to_CustomAudioRender, tipsId = R.string.customaudiorender)
+public class CustomAudioRender extends BaseFragment implements View.OnClickListener {
+ private static final String TAG = CustomAudioRender.class.getSimpleName();
+ private EditText et_channel;
+ private Button join;
+ private boolean joined = false;
+ /**
+ * The constant engine.
+ */
+ public static RtcEngineEx engine;
+
+ private static final Integer SAMPLE_RATE = 44100;
+ private static final Integer SAMPLE_NUM_OF_CHANNEL = 2;
+ private static final Integer BITS_PER_SAMPLE = 16;
+ private static final Integer SAMPLES = 441;
+ private static final Integer BUFFER_SIZE = SAMPLES * BITS_PER_SAMPLE / 8 * SAMPLE_NUM_OF_CHANNEL;
+ private static final Integer PULL_INTERVAL = SAMPLES * 1000 / SAMPLE_RATE;
+
+ private Thread pullingTask;
+ private volatile boolean pulling = false;
+ private AudioPlayer audioPlayer;
+
+ private AudioSeatManager audioSeatManager;
+
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_custom_audio_render, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ join = view.findViewById(R.id.btn_join);
+ et_channel = view.findViewById(R.id.et_channel);
+ view.findViewById(R.id.btn_join).setOnClickListener(this);
+
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02),
+ view.findViewById(R.id.audio_place_03),
+ view.findViewById(R.id.audio_place_04),
+ view.findViewById(R.id.audio_place_05),
+ view.findViewById(R.id.audio_place_06),
+ view.findViewById(R.id.audio_place_07),
+ view.findViewById(R.id.audio_place_08),
+ view.findViewById(R.id.audio_place_09));
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /*
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /*
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /* Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /*
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
+ config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = (RtcEngineEx) RtcEngine.create(config);
+ /*
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+
+ audioPlayer = new AudioPlayer(AudioManager.STREAM_MUSIC,
+ SAMPLE_RATE,
+ SAMPLE_NUM_OF_CHANNEL,
+ AudioFormat.ENCODING_PCM_16BIT);
+ } catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ pulling = false;
+ if (pullingTask != null) {
+ try {
+ pullingTask.join();
+ pullingTask = null;
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ audioPlayer.stopPlayer();
+ /*leaveChannel and Destroy the RtcEngine instance*/
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_join) {
+ if (!joined) {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) {
+ joinChannel(channelId);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(Permission.Group.STORAGE, Permission.Group.MICROPHONE).onGranted(permissions -> {
+ // Permissions Granted
+ joinChannel(channelId);
+ }).start();
+ } else {
+ joined = false;
+ /*After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ pulling = false;
+ join.setText(getString(R.string.join));
+ audioSeatManager.downAllSeats();
+ if (pullingTask != null) {
+ try {
+ pullingTask.join();
+ pullingTask = null;
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.
+ */
+ private void joinChannel(String channelId) {
+
+ engine.setExternalAudioSink(true, SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL);
+
+ /*Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
+
+
+ /* Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ int res = engine.joinChannel(ret, channelId, 0, option);
+
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+
+ }
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+
+ /**
+ * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ joined = true;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ audioSeatManager.upLocalSeat(uid);
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ pulling = true;
+ audioPlayer.startPlayer();
+ if (pullingTask == null) {
+ pullingTask = new Thread(new PullingTask());
+ pullingTask.start();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ runOnUIThread(() -> audioSeatManager.upRemoteSeat(uid));
+ }
+
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ super.onUserOffline(uid, reason);
+ runOnUIThread(() -> audioSeatManager.downSeat(uid));
+ }
+ };
+
+ /**
+ * The type Pulling task.
+ */
+ class PullingTask implements Runnable {
+ /**
+ * The Number.
+ */
+ long number = 0;
+
+ @Override
+ public void run() {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
+ while (pulling) {
+ Log.i(TAG, "pushExternalAudioFrame times:" + number++);
+
+ ByteBuffer frame = ByteBuffer.allocateDirect(BUFFER_SIZE);
+ engine.pullPlaybackAudioFrame(frame, BUFFER_SIZE);
+ byte[] data = new byte[frame.remaining()];
+ frame.get(data, 0, data.length);
+
+ // simple audio filter
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (data[i] + 5);
+ }
+
+ audioPlayer.play(data, 0, BUFFER_SIZE);
+ }
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java
new file mode 100755
index 000000000..bddfecc96
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java
@@ -0,0 +1,372 @@
+package io.agora.api.example.examples.advanced.customaudio;
+
+import static io.agora.api.example.common.model.Examples.ADVANCED;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.AudioFileReader;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.RtcEngineEx;
+import io.agora.rtc2.audio.AudioTrackConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**
+ * This demo demonstrates how to make a one-to-one voice call
+ */
+@Example(
+ index = 5,
+ group = ADVANCED,
+ name = R.string.item_customaudiosource,
+ actionId = R.id.action_mainFragment_to_CustomAudioSource,
+ tipsId = R.string.customaudio
+)
+public class CustomAudioSource extends BaseFragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
+ private static final String TAG = CustomAudioSource.class.getSimpleName();
+ private EditText et_channel;
+ private Button join;
+ private int myUid;
+ private boolean joined = false;
+ public static RtcEngineEx engine;
+ private Switch mic, pcm;
+ private ChannelMediaOptions option = new ChannelMediaOptions();
+ private int pushTimes = 0;
+
+ private AudioSeatManager audioSeatManager;
+ private AudioFileReader audioPushingHelper;
+ private int customAudioTrack = -1;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ initMediaOption();
+ }
+
+ private void initMediaOption() {
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+ option.publishMicrophoneTrack = true;
+ option.publishCustomAudioTrack = false;
+ option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
+ option.enableAudioRecordingOrPlayout = true;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_custom_audio_source, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ join = view.findViewById(R.id.btn_join);
+ et_channel = view.findViewById(R.id.et_channel);
+ view.findViewById(R.id.btn_join).setOnClickListener(this);
+ mic = view.findViewById(R.id.microphone);
+ pcm = view.findViewById(R.id.localAudio);
+ mic.setOnCheckedChangeListener(this);
+ pcm.setOnCheckedChangeListener(this);
+
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02),
+ view.findViewById(R.id.audio_place_03),
+ view.findViewById(R.id.audio_place_04),
+ view.findViewById(R.id.audio_place_05),
+ view.findViewById(R.id.audio_place_06),
+ view.findViewById(R.id.audio_place_07),
+ view.findViewById(R.id.audio_place_08),
+ view.findViewById(R.id.audio_place_09)
+ );
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = (RtcEngineEx) RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+
+ audioPushingHelper = new AudioFileReader(requireContext(), (buffer, timestamp) -> {
+ if(joined && engine != null && customAudioTrack != -1){
+ int ret = engine.pushExternalAudioFrame(buffer, timestamp, AudioFileReader.SAMPLE_RATE, AudioFileReader.SAMPLE_NUM_OF_CHANNEL, Constants.BytesPerSample.TWO_BYTES_PER_SAMPLE, customAudioTrack);
+ Log.i(TAG, "pushExternalAudioFrame times:" + (++pushTimes) + ", ret=" + ret);
+ }
+ });
+ } catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if(customAudioTrack != -1){
+ engine.destroyCustomAudioTrack(customAudioTrack);
+ customAudioTrack = -1;
+ }
+ if(audioPushingHelper != null){
+ audioPushingHelper.stop();
+ }
+ /**leaveChannel and Destroy the RtcEngine instance*/
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
+ if (compoundButton.getId() == R.id.microphone) {
+ option.publishMicrophoneTrack = checked;
+ engine.updateChannelMediaOptions(option);
+ } else if (compoundButton.getId() == R.id.localAudio) {
+ option.publishCustomAudioTrackId = customAudioTrack;
+ option.publishCustomAudioTrack = checked;
+ engine.updateChannelMediaOptions(option);
+ engine.enableCustomAudioLocalPlayback(customAudioTrack, checked);
+ }
+ }
+
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_join) {
+ if (!joined) {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) {
+ joinChannel(channelId);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ Permission.Group.STORAGE,
+ Permission.Group.MICROPHONE
+ ).onGranted(permissions ->
+ {
+ // Permissions Granted
+ joinChannel(channelId);
+ }).start();
+ } else {
+ joined = false;
+ /**After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ join.setText(getString(R.string.join));
+ mic.setEnabled(false);
+ pcm.setEnabled(false);
+ pcm.setChecked(false);
+ mic.setChecked(true);
+ if(audioPushingHelper != null){
+ audioPushingHelper.stop();
+ }
+ audioSeatManager.downAllSeats();
+ }
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.
+ */
+ private void joinChannel(String channelId) {
+ /**In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+ /**Sets the external audio source.
+ * @param enabled Sets whether to enable/disable the external audio source:
+ * true: Enable the external audio source.
+ * false: (Default) Disable the external audio source.
+ * @param sampleRate Sets the sample rate (Hz) of the external audio source, which can be
+ * set as 8000, 16000, 32000, 44100, or 48000 Hz.
+ * @param channels Sets the number of channels of the external audio source:
+ * 1: Mono.
+ * 2: Stereo.
+ * @return
+ * 0: Success.
+ * < 0: Failure.
+ * PS: Ensure that you call this method before the joinChannel method.*/
+ AudioTrackConfig config = new AudioTrackConfig();
+ config.enableLocalPlayback = false;
+ customAudioTrack = engine.createCustomAudioTrack(Constants.AudioTrackType.AUDIO_TRACK_MIXABLE, config);
+
+ /**Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+
+ /** Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ int res = engine.joinChannel(ret, channelId, 0, option);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+ }
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ mic.setEnabled(true);
+ pcm.setEnabled(true);
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ if(audioPushingHelper != null){
+ pushTimes = 0;
+ audioPushingHelper.start();
+ }
+ audioSeatManager.upLocalSeat(uid);
+ if (pcm.isChecked()) {
+ engine.enableCustomAudioLocalPlayback(0, true);
+ }
+ }
+ });
+ }
+
+
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ runOnUIThread(() -> audioSeatManager.upRemoteSeat(uid));
+ }
+
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ super.onUserOffline(uid, reason);
+ runOnUIThread(() -> audioSeatManager.downSeat(uid));
+ }
+ };
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java
new file mode 100644
index 000000000..b2c134dc6
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java
@@ -0,0 +1,319 @@
+package io.agora.api.example.examples.audio;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Locale;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.model.Examples;
+import io.agora.api.example.databinding.FragmentAudioWaveformBinding;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+@Example(
+ index = 7,
+ group = Examples.ADVANCED,
+ name = R.string.item_audiowaveform,
+ actionId = R.id.action_mainFragment_to_AudioWaveform,
+ tipsId = R.string.audiorouter_palyer
+)
+public class AudioWaveform extends BaseFragment {
+ private static final String TAG = "AudioWaveform";
+ private FragmentAudioWaveformBinding mBinding;
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /*
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /*
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /* Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /*
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAreaCode = ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /*
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ requireActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ /*leaveChannel and Destroy the RtcEngine instance*/
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ mBinding = FragmentAudioWaveformBinding.inflate(inflater);
+ return mBinding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mBinding.btnJoin.setOnClickListener(v -> {
+ if (!joined) {
+ CommonUtil.hideInputBoard(requireActivity(), mBinding.etChannel);
+ joinChannel(mBinding.etChannel.getText().toString());
+ joined = true;
+ mBinding.btnJoin.setText(R.string.leave);
+ mBinding.waveformView.clear();
+ } else {
+ engine.leaveChannel();
+ joined = false;
+ mBinding.btnJoin.setText(R.string.join);
+ }
+ });
+ }
+
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.
+ */
+ private void joinChannel(String channelId) {
+
+ engine.enableAudio();
+ engine.setDefaultAudioRoutetoSpeakerphone(true);
+
+ /*
+ * Enables the reporting of users' volume indication.
+ *
+ * @param interval Sets the time interval between two consecutive volume indications
+ * ≤ 0: Disables the volume indication.
+ * > 0: Time interval (ms) between two consecutive volume indications. The lowest value is 50.
+ * @param smooth The smoothing factor that sets the sensitivity of the audio volume indicator. The value ranges between 0 and 10.
+ * The recommended value is 3. The greater the value, the more sensitive the indicator.
+ * @param reportVad true: Enables the voice activity detection of the local user. Once it is enabled,
+ * the vad parameter of the onAudioVolumeIndication callback reports the voice activity status of the local user.
+ * false: (Default) Disables the voice activity detection of the local user. Once it is disabled,
+ * the vad parameter of the onAudioVolumeIndication callback does not report the voice activity status of the local user,
+ * except for the scenario where the engine automatically detects the voice activity of the local user.
+ */
+ engine.enableAudioVolumeIndication(1000, 3, true);
+
+ /*In the demo, the default is to enter as the anchor.*/
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ option.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER;
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+ option.publishMicrophoneTrack = true;
+
+ /*Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+
+ /* Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ int res = engine.joinChannel(ret, channelId, 0, option);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ mBinding.btnJoin.setEnabled(false);
+ });
+
+ }
+
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+ /**
+ * Error code description can be found at:
+ * en: ...
+ * cn: ...
+ */
+ @Override
+ public void onError(int error) {
+ Log.w(TAG, String.format("onError code %d message %s", error, RtcEngine.getErrorDescription(error)));
+ runOnUIThread(() -> mBinding.btnJoin.setEnabled(true));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats) {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format(Locale.US, "local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format(Locale.US, "onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ runOnUIThread(() -> mBinding.btnJoin.setEnabled(true));
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format(Locale.US, "user %d joined!", uid));
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format(Locale.US, "user %d offline! reason:%d", uid, reason));
+ }
+
+ /**
+ * Reports the volume information of users.
+ *
+ * @param speakers The volume information of the users. See AudioVolumeInfo.
+ * An empty speakers array in the callback indicates that no remote user is in the channel or is sending a stream.
+ * @param totalVolume The volume of the speaker. The value range is [0,255].
+ * In the callback for the local user, totalVolume is the volume of the local user who sends a stream.
+ * In the callback for remote users, totalVolume is the sum of the volume of all remote users (up to three)
+ * whose instantaneous volume is the highest. If the user calls startAudioMixing [2/2], then totalVolume is the volume after audio mixing.
+ */
+ @Override
+ public void onAudioVolumeIndication(AudioVolumeInfo[] speakers, int totalVolume) {
+ super.onAudioVolumeIndication(speakers, totalVolume);
+ runOnUIThread(() -> mBinding.waveformView.addData((short) totalVolume));
+ }
+ };
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java
new file mode 100755
index 000000000..f42034c23
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java
@@ -0,0 +1,748 @@
+package io.agora.api.example.examples.basic;
+
+import static io.agora.api.example.common.model.Examples.BASIC;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ServiceInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.app.NotificationManagerCompat;
+
+import com.yanzhenjie.permission.AndPermission;
+import com.yanzhenjie.permission.runtime.Permission;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import io.agora.api.example.MainActivity;
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.api.example.utils.TokenUtils;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**
+ * This demo demonstrates how to make a one-to-one voice call
+ *
+ * @author cjw
+ */
+@Example(
+ index = 2,
+ group = BASIC,
+ name = R.string.item_joinaudio,
+ actionId = R.id.action_mainFragment_to_joinChannelAudio,
+ tipsId = R.string.joinchannelaudio
+)
+public class JoinChannelAudio extends BaseFragment implements View.OnClickListener {
+ private static final String TAG = JoinChannelAudio.class.getSimpleName();
+ private Spinner channelProfileInput;
+ private Spinner audioProfileInput;
+ private Spinner audioScenarioInput;
+ private Spinner audioRouteInput;
+ private EditText et_channel;
+ private Button mute, join;
+ private SeekBar record, playout, inear;
+ private Switch inEarSwitch;
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+ private AudioSeatManager audioSeatManager;
+
+ private SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (seekBar.getId() == record.getId()) {
+ engine.adjustRecordingSignalVolume(progress);
+ } else if (seekBar.getId() == playout.getId()) {
+ engine.adjustPlaybackSignalVolume(progress);
+ } else if (seekBar.getId() == inear.getId()) {
+ if (progress == 0) {
+ engine.enableInEarMonitoring(false);
+ } else {
+ engine.enableInEarMonitoring(true);
+ engine.setInEarMonitoringVolume(progress);
+ }
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.fragment_joinchannel_audio, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ join = view.findViewById(R.id.btn_join);
+ et_channel = view.findViewById(R.id.et_channel);
+ audioProfileInput = view.findViewById(R.id.audio_profile_spinner);
+ channelProfileInput = view.findViewById(R.id.channel_profile_spinner);
+ audioScenarioInput = view.findViewById(R.id.audio_scenario_spinner);
+ audioRouteInput = view.findViewById(R.id.audio_route_spinner);
+ audioScenarioInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (joined) {
+ int scenario = Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString()));
+ engine.setAudioScenario(scenario);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+ });
+ audioRouteInput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ if (!joined) {
+ return;
+ }
+ boolean isCommunication = getString(R.string.channel_profile_communication).equals(channelProfileInput.getSelectedItem());
+ if (isCommunication) {
+ int route = Constants.AUDIO_ROUTE_EARPIECE;
+ if (getString(R.string.audio_route_earpiece).equals(parent.getSelectedItem())) {
+ route = Constants.AUDIO_ROUTE_EARPIECE;
+ } else if (getString(R.string.audio_route_speakerphone).equals(parent.getSelectedItem())) {
+ route = Constants.AUDIO_ROUTE_SPEAKERPHONE;
+ } else if (getString(R.string.audio_route_headset).equals(parent.getSelectedItem())) {
+ route = Constants.AUDIO_ROUTE_HEADSET;
+ } else if (getString(R.string.audio_route_headset_bluetooth).equals(parent.getSelectedItem())) {
+ route = Constants.AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP;
+ }
+ int ret = engine.setRouteInCommunicationMode(route);
+ showShortToast("setRouteInCommunicationMode route=" + route + ", ret=" + ret);
+ } else {
+ boolean isSpeakerPhone = false;
+ if (getString(R.string.audio_route_earpiece).equals(parent.getSelectedItem())) {
+ isSpeakerPhone = false;
+ } else if (getString(R.string.audio_route_speakerphone).equals(parent.getSelectedItem())) {
+ isSpeakerPhone = true;
+ }
+ int ret = engine.setEnableSpeakerphone(isSpeakerPhone);
+ showShortToast("setEnableSpeakerphone enable=" + isSpeakerPhone + ", ret=" + ret);
+ }
+
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+ });
+ view.findViewById(R.id.btn_join).setOnClickListener(this);
+ mute = view.findViewById(R.id.microphone);
+ mute.setOnClickListener(this);
+ record = view.findViewById(R.id.recordingVol);
+ playout = view.findViewById(R.id.playoutVol);
+ inear = view.findViewById(R.id.inEarMonitorVol);
+ record.setOnSeekBarChangeListener(seekBarChangeListener);
+ playout.setOnSeekBarChangeListener(seekBarChangeListener);
+ inear.setOnSeekBarChangeListener(seekBarChangeListener);
+ inEarSwitch = view.findViewById(R.id.inEarMonitorSwitch);
+ inEarSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ engine.enableInEarMonitoring(isChecked);
+ inear.setEnabled(isChecked);
+ });
+ record.setEnabled(false);
+ playout.setEnabled(false);
+ inear.setEnabled(false);
+ inEarSwitch.setEnabled(false);
+
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02),
+ view.findViewById(R.id.audio_place_03),
+ view.findViewById(R.id.audio_place_04),
+ view.findViewById(R.id.audio_place_05),
+ view.findViewById(R.id.audio_place_06)
+ );
+
+ if (savedInstanceState != null) {
+ joined = savedInstanceState.getBoolean("joined");
+ if (joined) {
+ myUid = savedInstanceState.getInt("myUid");
+ ArrayList seatRemoteUidList = savedInstanceState.getIntegerArrayList("seatRemoteUidList");
+ mute.setEnabled(true);
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ record.setEnabled(true);
+ playout.setEnabled(true);
+ inear.setEnabled(inEarSwitch.isChecked());
+ inEarSwitch.setEnabled(true);
+ audioSeatManager.upLocalSeat(myUid);
+
+ for (Integer uid : seatRemoteUidList) {
+ audioSeatManager.upRemoteSeat(uid);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Check if the context is valid
+ Context context = getContext();
+ if (context == null || engine != null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /*
+ * The context of Android Activity
+ */
+ config.mContext = context.getApplicationContext();
+ /*
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = getString(R.string.agora_app_id);
+ /* Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /*
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString()));
+ config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /*
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ getActivity().onBackPressed();
+ }
+ enableNotifications();
+ }
+
+ private void enableNotifications() {
+ if (NotificationManagerCompat.from(requireContext()).areNotificationsEnabled()) {
+ Log.d(TAG, "Notifications enable!");
+ return;
+ }
+ Log.d(TAG, "Notifications not enable!");
+ new AlertDialog.Builder(requireContext())
+ .setTitle("Tip")
+ .setMessage(R.string.notifications_enable_tip)
+ .setPositiveButton(R.string.setting, (dialog, which) -> {
+ Intent intent = new Intent();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().getPackageName());
+ intent.putExtra(Settings.EXTRA_CHANNEL_ID, requireContext().getApplicationInfo().uid);
+ } else {
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ }
+ startActivity(intent);
+ dialog.dismiss();
+ })
+ .show();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ startRecordingService();
+ }
+
+ private void startRecordingService() {
+ if (joined) {
+ Intent intent = new Intent(requireContext(), LocalRecordingService.class);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ requireContext().startForegroundService(intent);
+ } else {
+ requireContext().startService(intent);
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ // join state
+ outState.putBoolean("joined", joined);
+ outState.putInt("myUid", myUid);
+ outState.putIntegerArrayList("seatRemoteUidList", audioSeatManager.getSeatRemoteUidList());
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ stopRecordingService();
+ }
+
+ private void stopRecordingService() {
+ Intent intent = new Intent(requireContext(), LocalRecordingService.class);
+ requireContext().stopService(intent);
+ }
+
+ @Override
+ protected void onBackPressed() {
+ joined = false;
+ stopRecordingService();
+ /*leaveChannel and Destroy the RtcEngine instance*/
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ super.onBackPressed();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_join) {
+ if (!joined) {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String channelId = et_channel.getText().toString();
+ // Check permission
+ if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) {
+ joinChannel(channelId);
+ audioProfileInput.setEnabled(false);
+ channelProfileInput.setEnabled(false);
+ return;
+ }
+ // Request permission
+ AndPermission.with(this).runtime().permission(
+ Permission.Group.STORAGE,
+ Permission.Group.MICROPHONE
+ ).onGranted(permissions -> {
+ // Permissions Granted
+ joinChannel(channelId);
+ audioProfileInput.setEnabled(false);
+ channelProfileInput.setEnabled(false);
+ }).start();
+ } else {
+ joined = false;
+ /*After joining a channel, the user must call the leaveChannel method to end the
+ * call before joining another channel. This method returns 0 if the user leaves the
+ * channel and releases all resources related to the call. This method call is
+ * asynchronous, and the user has not exited the channel when the method call returns.
+ * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
+ * A successful leaveChannel method call triggers the following callbacks:
+ * 1:The local client: onLeaveChannel.
+ * 2:The remote client: onUserOffline, if the user leaving the channel is in the
+ * Communication channel, or is a BROADCASTER in the Live Broadcast profile.
+ * @returns 0: Success.
+ * < 0: Failure.
+ * PS:
+ * 1:If you call the destroy method immediately after calling the leaveChannel
+ * method, the leaveChannel process interrupts, and the SDK does not trigger
+ * the onLeaveChannel callback.
+ * 2:If you call the leaveChannel method during CDN live streaming, the SDK
+ * triggers the removeInjectStreamUrl method.*/
+ engine.leaveChannel();
+ join.setText(getString(R.string.join));
+ audioRouteInput.setSelection(0);
+ mute.setText(getString(R.string.closemicrophone));
+ mute.setEnabled(false);
+ audioProfileInput.setEnabled(true);
+ channelProfileInput.setEnabled(true);
+ record.setEnabled(false);
+ playout.setEnabled(false);
+ inear.setEnabled(false);
+ inEarSwitch.setEnabled(false);
+ inEarSwitch.setChecked(false);
+ audioSeatManager.downAllSeats();
+ }
+ } else if (v.getId() == R.id.microphone) {
+ mute.setActivated(!mute.isActivated());
+ mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone));
+ /*Turn off / on the microphone, stop / start local audio collection and push streaming.*/
+ engine.muteLocalAudioStream(mute.isActivated());
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.
+ */
+ private void joinChannel(String channelId) {
+ /*In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+
+ int channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ if (getString(R.string.channel_profile_communication).equals(channelProfileInput.getSelectedItem())) {
+ channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION;
+ } else if (getString(R.string.channel_profile_game).equals(channelProfileInput.getSelectedItem())) {
+ channelProfile = Constants.CHANNEL_PROFILE_GAME;
+ } else if (getString(R.string.channel_profile_communication_1v1).equals(channelProfileInput.getSelectedItem())) {
+ channelProfile = Constants.CHANNEL_PROFILE_COMMUNICATION_1v1;
+ } else if (getString(R.string.channel_profile_cloud_gaming).equals(channelProfileInput.getSelectedItem())) {
+ channelProfile = Constants.CHANNEL_PROFILE_CLOUD_GAMING;
+ }
+ engine.setChannelProfile(channelProfile);
+
+
+ int audioProfile = Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfileInput.getSelectedItem().toString()));
+ engine.setAudioProfile(audioProfile);
+
+ int scenario = Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString()));
+ engine.setAudioScenario(scenario);
+
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+
+ /*Please configure accessToken in the string_config file.
+ * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
+ * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
+ * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
+ * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
+ TokenUtils.gen(requireContext(), channelId, 0, ret -> {
+
+ /* Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ int res = engine.joinChannel(ret, channelId, 0, option);
+ if (res != 0) {
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ });
+
+ }
+
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int error) {
+ Log.w(TAG, String.format("onError code %d message %s", error, RtcEngine.getErrorDescription(error)));
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats) {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ runOnUIThread(() -> {
+ mute.setEnabled(true);
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ record.setEnabled(true);
+ playout.setEnabled(true);
+ inear.setEnabled(inEarSwitch.isChecked());
+ inEarSwitch.setEnabled(true);
+ audioSeatManager.upLocalSeat(uid);
+ });
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed) {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ runOnUIThread(() -> {
+ audioSeatManager.upRemoteSeat(uid);
+ });
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason) {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ runOnUIThread(() -> {
+ audioSeatManager.downSeat(uid);
+ });
+ }
+
+ @Override
+ public void onLocalAudioStats(LocalAudioStats stats) {
+ super.onLocalAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("sentSampleRate", stats.sentSampleRate + "");
+ _stats.put("sentBitrate", stats.sentBitrate + " kbps");
+ _stats.put("internalCodec", stats.internalCodec + "");
+ _stats.put("audioDeviceDelay", stats.audioDeviceDelay + " ms");
+ audioSeatManager.getLocalSeat().updateStats(_stats);
+ });
+ }
+
+ @Override
+ public void onRemoteAudioStats(RemoteAudioStats stats) {
+ super.onRemoteAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("numChannels", stats.numChannels + "");
+ _stats.put("receivedBitrate", stats.receivedBitrate + " kbps");
+ _stats.put("audioLossRate", stats.audioLossRate + "");
+ _stats.put("jitterBufferDelay", stats.jitterBufferDelay + " ms");
+ audioSeatManager.getRemoteSeat(stats.uid).updateStats(_stats);
+ });
+ }
+
+ @Override
+ public void onAudioRouteChanged(int routing) {
+ super.onAudioRouteChanged(routing);
+ showShortToast("onAudioRouteChanged : " + routing);
+ runOnUIThread(() -> {
+ String selectedRouteStr = getString(R.string.audio_route_speakerphone);
+ if (routing == Constants.AUDIO_ROUTE_EARPIECE) {
+ selectedRouteStr = getString(R.string.audio_route_earpiece);
+ } else if (routing == Constants.AUDIO_ROUTE_SPEAKERPHONE) {
+ selectedRouteStr = getString(R.string.audio_route_speakerphone);
+ } else if (routing == Constants.AUDIO_ROUTE_HEADSET) {
+ selectedRouteStr = getString(R.string.audio_route_headset);
+ } else if (routing == Constants.AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP) {
+ selectedRouteStr = getString(R.string.audio_route_headset_bluetooth);
+ } else if (routing == Constants.AUDIO_ROUTE_USBDEVICE) {
+ selectedRouteStr = getString(R.string.audio_route_headset_typec);
+ }
+
+ int selection = 0;
+ for (int i = 0; i < audioRouteInput.getAdapter().getCount(); i++) {
+ String routeStr = (String) audioRouteInput.getItemAtPosition(i);
+ if (routeStr.equals(selectedRouteStr)) {
+ selection = i;
+ break;
+ }
+ }
+ audioRouteInput.setSelection(selection);
+ });
+ }
+ };
+
+
+ /**
+ * The service will display a microphone foreground notification,
+ * which can ensure keeping recording when the activity destroyed by system for memory leak or other reasons.
+ * Note: The "android.permission.FOREGROUND_SERVICE" permission is required.
+ * And the android:foregroundServiceType should be microphone.
+ */
+ public static class LocalRecordingService extends Service {
+ private static final int NOTIFICATION_ID = 1234567800;
+ private static final String CHANNEL_ID = "audio_channel_id";
+
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Notification notification = getDefaultNotification();
+
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ this.startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
+ } else {
+ this.startForeground(NOTIFICATION_ID, notification);
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "", ex);
+ }
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private Notification getDefaultNotification() {
+ ApplicationInfo appInfo = this.getApplicationContext().getApplicationInfo();
+ String name = this.getApplicationContext().getPackageManager().getApplicationLabel(appInfo).toString();
+ int icon = appInfo.icon;
+
+ try {
+ Bitmap iconBitMap = BitmapFactory.decodeResource(this.getApplicationContext().getResources(), icon);
+ if (iconBitMap == null || iconBitMap.getByteCount() == 0) {
+ Log.w(TAG, "Couldn't load icon from icon of applicationInfo, use android default");
+ icon = R.mipmap.ic_launcher;
+ }
+ } catch (Exception ex) {
+ Log.w(TAG, "Couldn't load icon from icon of applicationInfo, use android default");
+ icon = R.mipmap.ic_launcher;
+ }
+
+ if (Build.VERSION.SDK_INT >= 26) {
+ NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT);
+ NotificationManager mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
+ mNotificationManager.createNotificationChannel(mChannel);
+ }
+
+ PendingIntent activityPendingIntent;
+ Intent intent = new Intent();
+ intent.setClass(this, MainActivity.class);
+ if (Build.VERSION.SDK_INT >= 23) {
+ activityPendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
+ } else {
+ activityPendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+ }
+
+ Notification.Builder builder = new Notification.Builder(this)
+ .addAction(icon, "Back to app", activityPendingIntent)
+ .setContentText("Agora Recording ...")
+ .setOngoing(true)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setSmallIcon(icon)
+ .setTicker(name)
+ .setWhen(System.currentTimeMillis());
+ if (Build.VERSION.SDK_INT >= 26) {
+ builder.setChannelId(CHANNEL_ID);
+ }
+
+ return builder.build();
+ }
+
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java
new file mode 100755
index 000000000..94a8f4e52
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java
@@ -0,0 +1,385 @@
+package io.agora.api.example.examples.basic;
+
+import static io.agora.api.example.common.model.Examples.BASIC;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.SeekBar;
+import android.widget.Spinner;
+import android.widget.Switch;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.R;
+import io.agora.api.example.annotation.Example;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.api.example.utils.CommonUtil;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.Constants;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**This demo demonstrates how to make a one-to-one voice call
+ * @author cjw*/
+@Example(
+ index = 0,
+ group = BASIC,
+ name = R.string.item_joinaudio_by_token,
+ actionId = R.id.action_mainFragment_to_joinChannelAudioByToken,
+ tipsId = R.string.joinchannelaudioByToken
+)
+public class JoinChannelAudioByToken extends BaseFragment implements View.OnClickListener
+{
+ private static final String TAG = JoinChannelAudioByToken.class.getSimpleName();
+ private EditText et_app_id, et_channel, et_token;
+ private Button join;
+ private RtcEngine engine;
+ private int myUid;
+ private boolean joined = false;
+ private AudioSeatManager audioSeatManager;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ handler = new Handler();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
+ {
+ View view = inflater.inflate(R.layout.fragment_joinchannel_audio_by_token, container, false);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)
+ {
+ super.onViewCreated(view, savedInstanceState);
+ join = view.findViewById(R.id.btn_join);
+ et_app_id = view.findViewById(R.id.et_app_id);
+ et_channel = view.findViewById(R.id.et_channel);
+ et_token = view.findViewById(R.id.et_token);
+ view.findViewById(R.id.btn_join).setOnClickListener(this);
+ audioSeatManager = new AudioSeatManager(
+ view.findViewById(R.id.audio_place_01),
+ view.findViewById(R.id.audio_place_02),
+ view.findViewById(R.id.audio_place_03),
+ view.findViewById(R.id.audio_place_04),
+ view.findViewById(R.id.audio_place_05),
+ view.findViewById(R.id.audio_place_06)
+ );
+ }
+
+ private boolean createRtcEngine(String appId) {
+ try
+ {
+ RtcEngineConfig config = new RtcEngineConfig();
+ /**
+ * The context of Android Activity
+ */
+ config.mContext = requireContext().getApplicationContext();
+ /**
+ * The App ID issued to you by Agora. See How to get the App ID
+ */
+ config.mAppId = appId;
+ /** Sets the channel profile of the Agora RtcEngine.
+ CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
+ Use this profile in one-on-one calls or group calls, where all users can talk freely.
+ CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
+ channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
+ an audience can only receive streams.*/
+ config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
+ /**
+ * IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.
+ */
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ /**
+ * This parameter is for reporting the usages of APIExample to agora background.
+ * Generally, it is not necessary for you to set this parameter.
+ */
+ engine.setParameters("{"
+ + "\"rtc.report_app_scenario\":"
+ + "{"
+ + "\"appScenario\":" + 100 + ","
+ + "\"serviceType\":" + 11 + ","
+ + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
+ + "}"
+ + "}");
+ /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
+ LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (localAccessPointConfiguration != null) {
+ // This api can only be used in the private media server scenario, otherwise some problems may occur.
+ engine.setLocalAccessPoint(localAccessPointConfiguration);
+ }
+
+ return true;
+ }
+ catch (Exception e)
+ {
+ showAlert(e.getMessage());
+ }
+
+ return false;
+ }
+
+ private void destroyRtcEngine(){
+ /**leaveChannel and Destroy the RtcEngine instance*/
+ if(engine != null)
+ {
+ engine.leaveChannel();
+ RtcEngine.destroy();
+ engine = null;
+ }
+ }
+
+ @Override
+ public void onDestroy()
+ {
+ super.onDestroy();
+ destroyRtcEngine();
+ }
+
+ @Override
+ public void onClick(View v)
+ {
+ if (v.getId() == R.id.btn_join)
+ {
+ if (!joined)
+ {
+ CommonUtil.hideInputBoard(getActivity(), et_channel);
+ // call when join button hit
+ String appId = et_app_id.getText().toString();
+ String channelId = et_channel.getText().toString();
+ String token = et_token.getText().toString();
+
+ if(TextUtils.isEmpty(appId)){
+ showLongToast(getString(R.string.app_id_empty));
+ return;
+ }
+
+ if (createRtcEngine(appId)) {
+ joinChannel(channelId, token);
+ }
+ }
+ else
+ {
+ joined = false;
+ join.setText(getString(R.string.join));
+ audioSeatManager.downAllSeats();
+ destroyRtcEngine();
+ }
+ }
+ }
+
+ /**
+ * @param channelId Specify the channel name that you want to join.
+ * Users that input the same channel name join the same channel.*/
+ private void joinChannel(String channelId, String token)
+ {
+ /**In the demo, the default is to enter as the anchor.*/
+ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
+
+ engine.setDefaultAudioRoutetoSpeakerphone(true);
+
+ ChannelMediaOptions option = new ChannelMediaOptions();
+ option.autoSubscribeAudio = true;
+ option.autoSubscribeVideo = true;
+
+ /** Allows a user to join a channel.
+ if you do not specify the uid, we will generate the uid for you*/
+ int res = engine.joinChannel(token, channelId, 0, option);
+ if (res != 0)
+ {
+ engine.leaveChannel();
+ // Usually happens with invalid parameters
+ // Error code description can be found at:
+ // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
+ showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
+ Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
+ return;
+ }
+ // Prevent repeated entry
+ join.setEnabled(false);
+ }
+
+ /**IRtcEngineEventHandler is an abstract class providing default implementation.
+ * The SDK uses this class to report to the app on SDK runtime events.*/
+ private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler()
+ {
+ /**
+ * Error code description can be found at:
+ * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
+ */
+ @Override
+ public void onError(int err) {
+ super.onError(err);
+ showLongToast("onError code=" + err + ", msg=" + RtcEngine.getErrorDescription(err));
+ runOnUIThread(() -> join.setEnabled(true));
+ if(err== Constants.ERR_INVALID_TOKEN){
+ engine.leaveChannel();
+ showAlert(getString(R.string.token_invalid));
+ }else if(err== Constants.ERR_TOKEN_EXPIRED){
+ engine.leaveChannel();
+ showAlert(getString(R.string.token_expired));
+ }
+ }
+
+ /**Occurs when a user leaves the channel.
+ * @param stats With this callback, the application retrieves the channel information,
+ * such as the call duration and statistics.*/
+ @Override
+ public void onLeaveChannel(RtcStats stats)
+ {
+ super.onLeaveChannel(stats);
+ Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
+ //showLongToast(String.format("local user %d leaveChannel!", myUid));
+ }
+
+ /**Occurs when the local user joins a specified channel.
+ * The channel name assignment is based on channelName specified in the joinChannel method.
+ * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
+ * @param channel Channel name
+ * @param uid User ID
+ * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
+ @Override
+ public void onJoinChannelSuccess(String channel, int uid, int elapsed)
+ {
+ Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
+ myUid = uid;
+ joined = true;
+ handler.post(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ join.setEnabled(true);
+ join.setText(getString(R.string.leave));
+ audioSeatManager.upLocalSeat(uid);
+ }
+ });
+ }
+
+ /**Since v2.9.0.
+ * This callback indicates the state change of the remote audio stream.
+ * PS: This callback does not work properly when the number of users (in the Communication profile) or
+ * broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
+ * @param uid ID of the user whose audio state changes.
+ * @param state State of the remote audio
+ * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
+ * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
+ * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
+ * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
+ * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
+ * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
+ * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
+ * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
+ * REMOTE_AUDIO_REASON_INTERNAL(0).
+ * @param reason The reason of the remote audio state change.
+ * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
+ * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
+ * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
+ * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
+ * stream or disables the audio module.
+ * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
+ * stream or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
+ * disables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
+ * or enables the audio module.
+ * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
+ * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
+ * until the SDK triggers this callback.*/
+ @Override
+ public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
+ super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
+ Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
+ * until this callback is triggered.*/
+ @Override
+ public void onUserJoined(int uid, int elapsed)
+ {
+ super.onUserJoined(uid, elapsed);
+ Log.i(TAG, "onUserJoined->" + uid);
+ showLongToast(String.format("user %d joined!", uid));
+ runOnUIThread(() -> {
+ audioSeatManager.upRemoteSeat(uid);
+ });
+ }
+
+ /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
+ * @param uid ID of the user whose audio state changes.
+ * @param reason Reason why the user goes offline:
+ * USER_OFFLINE_QUIT(0): The user left the current channel.
+ * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
+ * packet was received within a certain period of time. If a user quits the
+ * call and the message is not passed to the SDK (due to an unreliable channel),
+ * the SDK assumes the user dropped offline.
+ * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
+ * the host to the audience.*/
+ @Override
+ public void onUserOffline(int uid, int reason)
+ {
+ Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
+ showLongToast(String.format("user %d offline! reason:%d", uid, reason));
+ runOnUIThread(() -> {
+ audioSeatManager.downSeat(uid);
+ });
+ }
+
+ @Override
+ public void onLocalAudioStats(LocalAudioStats stats) {
+ super.onLocalAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("sentSampleRate", stats.sentSampleRate + "");
+ _stats.put("sentBitrate", stats.sentBitrate + " kbps");
+ _stats.put("internalCodec", stats.internalCodec + "");
+ _stats.put("audioDeviceDelay", stats.audioDeviceDelay + " ms");
+ audioSeatManager.getLocalSeat().updateStats(_stats);
+ });
+ }
+
+ @Override
+ public void onRemoteAudioStats(RemoteAudioStats stats) {
+ super.onRemoteAudioStats(stats);
+ runOnUIThread(() -> {
+ Map _stats = new LinkedHashMap<>();
+ _stats.put("numChannels", stats.numChannels + "");
+ _stats.put("receivedBitrate", stats.receivedBitrate + " kbps");
+ _stats.put("audioLossRate", stats.audioLossRate + "");
+ _stats.put("jitterBufferDelay", stats.jitterBufferDelay + " ms");
+ audioSeatManager.getRemoteSeat(stats.uid).updateStats(_stats);
+ });
+ }
+ };
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AudioFileReader.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AudioFileReader.java
new file mode 100644
index 000000000..387463604
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AudioFileReader.java
@@ -0,0 +1,116 @@
+package io.agora.api.example.utils;
+
+import android.content.Context;
+import android.os.Process;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AudioFileReader {
+ private static final String AUDIO_FILE = "output.raw";
+ public static final int SAMPLE_RATE = 44100;
+ public static final int SAMPLE_NUM_OF_CHANNEL = 2;
+ public static final int BITS_PER_SAMPLE = 16;
+
+ public static final float BYTE_PER_SAMPLE = 1.0f * BITS_PER_SAMPLE / 8 * SAMPLE_NUM_OF_CHANNEL;
+ public static final float DURATION_PER_SAMPLE = 1000.0f / SAMPLE_RATE; // ms
+ public static final float SAMPLE_COUNT_PER_MS = SAMPLE_RATE * 1.0f / 1000; // ms
+
+ private static final int BUFFER_SAMPLE_COUNT = (int) (SAMPLE_COUNT_PER_MS * 10); // 10ms sample count
+ private static final int BUFFER_BYTE_SIZE = (int) (BUFFER_SAMPLE_COUNT * BYTE_PER_SAMPLE); // byte
+ private static final long BUFFER_DURATION = (long) (BUFFER_SAMPLE_COUNT * DURATION_PER_SAMPLE); // ms
+
+ private final Context context;
+ private final OnAudioReadListener audioReadListener;
+ private volatile boolean pushing = false;
+ private InnerThread thread;
+ private InputStream inputStream;
+
+ public AudioFileReader(Context context, OnAudioReadListener listener){
+ this.context = context;
+ this.audioReadListener = listener;
+ }
+
+ public void start() {
+ if(thread == null){
+ thread = new InnerThread();
+ thread.start();
+ }
+ }
+
+ public void stop(){
+ pushing = false;
+ if(thread != null){
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } finally {
+ thread = null;
+ }
+ }
+ }
+
+ public interface OnAudioReadListener {
+ void onAudioRead(byte[] buffer, long timestamp);
+ }
+
+ private class InnerThread extends Thread{
+
+ @Override
+ public void run() {
+ super.run();
+ try {
+ inputStream = context.getAssets().open(AUDIO_FILE);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
+ pushing = true;
+
+ long start_time = System.currentTimeMillis();;
+ int sent_audio_frames = 0;
+ while (pushing) {
+ if(audioReadListener != null){
+ audioReadListener.onAudioRead(readBuffer(), System.currentTimeMillis());
+ }
+ ++ sent_audio_frames;
+ long next_frame_start_time = sent_audio_frames * BUFFER_DURATION + start_time;
+ long now = System.currentTimeMillis();
+
+ if(next_frame_start_time > now){
+ long sleep_duration = next_frame_start_time - now;
+ try {
+ Thread.sleep(sleep_duration);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ inputStream = null;
+ }
+ }
+ }
+
+ private byte[] readBuffer() {
+ int byteSize = BUFFER_BYTE_SIZE;
+ byte[] buffer = new byte[byteSize];
+ try {
+ if (inputStream.read(buffer) < 0) {
+ inputStream.reset();
+ return readBuffer();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return buffer;
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/ClassUtils.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/ClassUtils.java
new file mode 100644
index 000000000..0c281272d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/ClassUtils.java
@@ -0,0 +1,285 @@
+package io.agora.api.example.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import dalvik.system.DexFile;
+import io.agora.api.example.BuildConfig;
+
+public class ClassUtils
+{
+ private static final String TAG = ClassUtils.class.getSimpleName();
+ private static final String EXTRACTED_NAME_EXT = ".classes";
+ private static final String EXTRACTED_SUFFIX = ".zip";
+
+ private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
+
+ private static final String PREFS_FILE = "multidex.version";
+ private static final String KEY_DEX_NUMBER = "dex.number";
+
+ private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
+ private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
+
+ private static SharedPreferences getMultiDexPreferences(Context context)
+ {
+ return context.getSharedPreferences(PREFS_FILE, Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? Context.MODE_PRIVATE : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
+ }
+
+ /**
+ * By specifying the package name, scan all ClassName contained under the package
+ *
+ * @param context
+ * @param packageName
+ * @return Collection of all classes
+ */
+ public static Set getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException
+ {
+ final Set classNames = new HashSet<>();
+
+ List paths = getSourcePaths(context);
+ final CountDownLatch parserCtl = new CountDownLatch(paths.size());
+
+ for (final String path : paths)
+ {
+ DefaultPoolExecutor.getInstance().execute(new Runnable()
+ {
+ @Override
+ public void run()
+ {
+ DexFile dexfile = null;
+
+ try
+ {
+ if (path.endsWith(EXTRACTED_SUFFIX))
+ {
+ //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
+ dexfile = DexFile.loadDex(path, path + ".tmp", 0);
+ }
+ else
+ {
+ dexfile = new DexFile(path);
+ }
+
+ Enumeration dexEntries = dexfile.entries();
+ while (dexEntries.hasMoreElements())
+ {
+ String className = dexEntries.nextElement();
+ if (className.startsWith(packageName))
+ {
+ classNames.add(className);
+ }
+ }
+ }
+ catch (Throwable ignore)
+ {
+ Log.e("ARouter", "Scan map file in dex files made error.", ignore);
+ }
+ finally
+ {
+ if (null != dexfile)
+ {
+ try
+ {
+ dexfile.close();
+ }
+ catch (Throwable ignore)
+ {
+ }
+ }
+
+ parserCtl.countDown();
+ }
+ }
+ });
+ }
+
+ parserCtl.await();
+
+ Log.d(TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
+ return classNames;
+ }
+
+ /**
+ * get all the dex path
+ *
+ * @param context the application context
+ * @return all the dex path
+ * @throws PackageManager.NameNotFoundException
+ * @throws IOException
+ */
+ public static List getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException
+ {
+ ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
+ File sourceApk = new File(applicationInfo.sourceDir);
+
+ List sourcePaths = new ArrayList<>();
+ sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
+
+ //the prefix of extracted file, ie: test.classes
+ String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
+
+ /** If MultiDex already supported by VM, we will not to load Classesx.zip from
+ * Secondary Folder, because there is none.*/
+ if (!isVMMultidexCapable())
+ {
+ //the total dex numbers
+ int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
+ File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
+
+ for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++)
+ {
+ //for each dex file, ie: test.classes2.zip, test.classes3.zip...
+ String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
+ File extractedFile = new File(dexDir, fileName);
+ if (extractedFile.isFile())
+ {
+ sourcePaths.add(extractedFile.getAbsolutePath());
+ //we ignore the verify zip part
+ }
+ else
+ {
+ throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
+ }
+ }
+ }
+
+ if (BuildConfig.DEBUG)
+ { // Search instant run support only debuggable
+ sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
+ }
+ return sourcePaths;
+ }
+
+ /**
+ * Get instant run dex path, used to catch the branch usingApkSplits=false.
+ */
+ private static List tryLoadInstantRunDexFile(ApplicationInfo applicationInfo)
+ {
+ List instantRunSourcePaths = new ArrayList<>();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != applicationInfo.splitSourceDirs)
+ {
+ // add the split apk, normally for InstantRun, and newest version.
+ instantRunSourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs));
+ Log.d(TAG, "Found InstantRun support");
+ }
+ else
+ {
+ try
+ {
+ // This man is reflection from Google instant run sdk, he will tell me where the dex files go.
+ Class pathsByInstantRun = Class.forName("com.android.tools.fd.runtime.Paths");
+ Method getDexFileDirectory = pathsByInstantRun.getMethod("getDexFileDirectory", String.class);
+ String instantRunDexPath = (String) getDexFileDirectory.invoke(null, applicationInfo.packageName);
+
+ File instantRunFilePath = new File(instantRunDexPath);
+ if (instantRunFilePath.exists() && instantRunFilePath.isDirectory())
+ {
+ File[] dexFile = instantRunFilePath.listFiles();
+ for (File file : dexFile)
+ {
+ if (null != file && file.exists() && file.isFile() && file.getName().endsWith(".dex"))
+ {
+ instantRunSourcePaths.add(file.getAbsolutePath());
+ }
+ }
+ Log.d(TAG, "Found InstantRun support");
+ }
+
+ }
+ catch (Exception e)
+ {
+ Log.e(TAG, "InstantRun support error, " + e.getMessage());
+ }
+ }
+
+ return instantRunSourcePaths;
+ }
+
+ /**
+ * Identifies if the current VM has a native support for multidex, meaning there is no need for
+ * additional installation by this library.
+ *
+ * @return true if the VM handles multidex
+ */
+ private static boolean isVMMultidexCapable()
+ {
+ boolean isMultidexCapable = false;
+ String vmName = null;
+
+ try
+ {
+ if (isYunOS())
+ { // YunOS need special judgment
+ vmName = "'YunOS'";
+ isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
+ }
+ else
+ { // Native Android system
+ vmName = "'Android'";
+ String versionString = System.getProperty("java.vm.version");
+ if (versionString != null)
+ {
+ Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
+ if (matcher.matches())
+ {
+ try
+ {
+ int major = Integer.parseInt(matcher.group(1));
+ int minor = Integer.parseInt(matcher.group(2));
+ isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
+ || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
+ && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
+ }
+ catch (NumberFormatException ignore)
+ {
+ // let isMultidexCapable be false
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ignore)
+ {
+
+ }
+
+ Log.i(TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
+ return isMultidexCapable;
+ }
+
+ /**
+ * Determine whether the system is a YunOS system
+ */
+ private static boolean isYunOS()
+ {
+ try
+ {
+ String version = System.getProperty("ro.yunos.version");
+ String vmName = System.getProperty("java.vm.name");
+ return (vmName != null && vmName.toLowerCase().contains("lemur"))
+ || (version != null && version.trim().length() > 0);
+ }
+ catch (Exception ignore)
+ {
+ return false;
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/CommonUtil.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/CommonUtil.java
new file mode 100644
index 000000000..b78399dc2
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/CommonUtil.java
@@ -0,0 +1,18 @@
+package io.agora.api.example.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+/**
+ * @author cjw
+ */
+public class CommonUtil {
+
+ public static void hideInputBoard(Activity activity, EditText editText)
+ {
+ InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/DefaultPoolExecutor.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/DefaultPoolExecutor.java
new file mode 100644
index 000000000..324fc3087
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/DefaultPoolExecutor.java
@@ -0,0 +1,97 @@
+package io.agora.api.example.utils;
+
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.RejectedExecutionHandler;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Executors
+ *
+ * @version 1.0
+ * @since 16/4/28 下午4:07
+ */
+public class DefaultPoolExecutor extends ThreadPoolExecutor
+{
+ private static final String TAG = DefaultPoolExecutor.class.getSimpleName();
+ // Thread args
+ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+ private static final int INIT_THREAD_COUNT = CPU_COUNT + 1;
+ private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT;
+ private static final long SURPLUS_THREAD_LIFE = 30L;
+
+ private static DefaultPoolExecutor instance;
+
+ public static DefaultPoolExecutor getInstance()
+ {
+ if (null == instance)
+ {
+ synchronized (DefaultPoolExecutor.class)
+ {
+ if (null == instance)
+ {
+ instance = new DefaultPoolExecutor(
+ INIT_THREAD_COUNT,
+ MAX_THREAD_COUNT,
+ SURPLUS_THREAD_LIFE,
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue(64),
+ new DefaultThreadFactory());
+ }
+ }
+ }
+ return instance;
+ }
+
+ private DefaultPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory)
+ {
+ super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new RejectedExecutionHandler()
+ {
+ @Override
+ public void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
+ {
+ Log.e(TAG, "Task rejected, too many task!");
+ }
+ });
+ }
+
+ /* thread execution complete, handle possible exceptions.
+ * @param r the runnable that has completed
+ * @param t the exception that caused termination, or null if
+ */
+ @Override
+ protected void afterExecute(Runnable r, Throwable t)
+ {
+ super.afterExecute(r, t);
+ if (t == null && r instanceof Future>)
+ {
+ try
+ {
+ ((Future>) r).get();
+ }
+ catch (CancellationException ce)
+ {
+ t = ce;
+ }
+ catch (ExecutionException ee)
+ {
+ t = ee.getCause();
+ }
+ catch (InterruptedException ie)
+ {
+ Thread.currentThread().interrupt(); // ignore/reset
+ }
+ }
+ if (t != null)
+ {
+ Log.w(TAG, "Running task appeared exception! Thread [" + Thread.currentThread().getName() + "], because [" + t.getMessage() + "]\n" + TextUtils.formatStackTrace(t.getStackTrace()));
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/DefaultThreadFactory.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/DefaultThreadFactory.java
new file mode 100644
index 000000000..7a6f99fca
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/DefaultThreadFactory.java
@@ -0,0 +1,53 @@
+package io.agora.api.example.utils;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * DefaultThreadFactory
+ *
+ * @author zhilong Contact me.
+ * @version 1.0
+ * @since 15/12/25 上午10:51
+ */
+public class DefaultThreadFactory implements ThreadFactory
+{
+ private static final String TAG = DefaultThreadFactory.class.getSimpleName();
+
+ private static final AtomicInteger poolNumber = new AtomicInteger(1);
+
+ private final AtomicInteger threadNumber = new AtomicInteger(1);
+ private final ThreadGroup group;
+ private final String namePrefix;
+
+ public DefaultThreadFactory()
+ {
+ SecurityManager s = System.getSecurityManager();
+ group = (s != null) ? s.getThreadGroup() :
+ Thread.currentThread().getThreadGroup();
+ namePrefix = "ARouter task pool No." + poolNumber.getAndIncrement() + ", thread No.";
+ }
+
+ public Thread newThread(@NonNull Runnable runnable)
+ {
+ String threadName = namePrefix + threadNumber.getAndIncrement();
+ Log.i(TAG, "Thread production, name is [" + threadName + "]");
+ Thread thread = new Thread(group, runnable, threadName, 0);
+ if (thread.isDaemon())
+ { //Make non-background thread
+ thread.setDaemon(false);
+ }
+ if (thread.getPriority() != Thread.NORM_PRIORITY)
+ {
+ thread.setPriority(Thread.NORM_PRIORITY);
+ }
+
+ // Catching exceptions in multi-threaded processing
+ thread.setUncaughtExceptionHandler((thread1, ex) -> Log.i(TAG, "Running task appeared exception! Thread [" + thread1.getName() + "], because [" + ex.getMessage() + "]"));
+ return thread;
+ }
+}
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/ErrorUtil.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/ErrorUtil.java
new file mode 100644
index 000000000..303a69188
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/ErrorUtil.java
@@ -0,0 +1,7 @@
+package io.agora.api.example.utils;
+
+/**
+ * @author cjw
+ */
+public class ErrorUtil {
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/FileUtils.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/FileUtils.java
new file mode 100644
index 000000000..f3c1210fa
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/FileUtils.java
@@ -0,0 +1,107 @@
+package io.agora.api.example.utils;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class FileUtils {
+
+ public static final String SEPARATOR = File.separator;
+
+ public static void copyFilesFromAssets(Context context, String assetsPath, String storagePath) {
+ String temp = "";
+
+ if (TextUtils.isEmpty(storagePath)) {
+ return;
+ } else if (storagePath.endsWith(SEPARATOR)) {
+ storagePath = storagePath.substring(0, storagePath.length() - 1);
+ }
+
+ if (TextUtils.isEmpty(assetsPath) || assetsPath.equals(SEPARATOR)) {
+ assetsPath = "";
+ } else if (assetsPath.endsWith(SEPARATOR)) {
+ assetsPath = assetsPath.substring(0, assetsPath.length() - 1);
+ }
+
+ AssetManager assetManager = context.getAssets();
+ try {
+ File file = new File(storagePath);
+ if (!file.exists()) {//如果文件夹不存在,则创建新的文件夹
+ file.mkdirs();
+ }
+
+ // 获取assets目录下的所有文件及目录名
+ String[] fileNames = assetManager.list(assetsPath);
+ if (fileNames.length > 0) {//如果是目录 apk
+ for (String fileName : fileNames) {
+ if (!TextUtils.isEmpty(assetsPath)) {
+ temp = assetsPath + SEPARATOR + fileName;//补全assets资源路径
+ }
+
+ String[] childFileNames = assetManager.list(temp);
+ if (!TextUtils.isEmpty(temp) && childFileNames.length > 0) {//判断是文件还是文件夹:如果是文件夹
+ copyFilesFromAssets(context, temp, storagePath + SEPARATOR + fileName);
+ } else {//如果是文件
+ InputStream inputStream = assetManager.open(temp);
+ readInputStream(storagePath + SEPARATOR + fileName, inputStream);
+ }
+ }
+ } else {//如果是文件 doc_test.txt或者apk/app_test.apk
+ InputStream inputStream = assetManager.open(assetsPath);
+ if (assetsPath.contains(SEPARATOR)) {//apk/app_test.apk
+ assetsPath = assetsPath.substring(assetsPath.lastIndexOf(SEPARATOR), assetsPath.length());
+ }
+ readInputStream(storagePath + SEPARATOR + assetsPath, inputStream);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ /**
+ * 读取输入流中的数据写入输出流
+ *
+ * @param storagePath 目标文件路径
+ * @param inputStream 输入流
+ */
+ public static void readInputStream(String storagePath, InputStream inputStream) {
+ File file = new File(storagePath);
+ try {
+ if (!file.exists()) {
+ // 1.建立通道对象
+ FileOutputStream fos = new FileOutputStream(file);
+ // 2.定义存储空间
+ byte[] buffer = new byte[inputStream.available()];
+ // 3.开始读文件
+ int lenght = 0;
+ while ((lenght = inputStream.read(buffer)) != -1) {// 循环从输入流读取buffer字节
+ // 将Buffer中的数据写到outputStream对象中
+ fos.write(buffer, 0, lenght);
+ }
+ fos.flush();// 刷新缓冲区
+ // 4.关闭流
+ fos.close();
+ inputStream.close();
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TextUtils.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TextUtils.java
new file mode 100644
index 000000000..0f8034b3a
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TextUtils.java
@@ -0,0 +1,15 @@
+package io.agora.api.example.utils;
+
+public class TextUtils {
+ /**
+ * Print thread stack
+ */
+ public static String formatStackTrace(StackTraceElement[] stackTrace) {
+ StringBuilder sb = new StringBuilder();
+ for (StackTraceElement element : stackTrace) {
+ sb.append(" at ").append(element.toString());
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java
new file mode 100644
index 000000000..65b2ee411
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java
@@ -0,0 +1,125 @@
+package io.agora.api.example.utils;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.Objects;
+
+import io.agora.api.example.R;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okhttp3.logging.HttpLoggingInterceptor;
+
+public class TokenUtils {
+ private final String TAG = "TokenGenerator";
+ private final static OkHttpClient client;
+
+ static {
+ HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
+ interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
+ client = new OkHttpClient.Builder()
+ .addInterceptor(interceptor)
+ .build();
+ }
+
+ public static void gen(Context context, String channelName, int uid, OnTokenGenCallback onGetToken){
+ gen(context.getString(R.string.agora_app_id), context.getString(R.string.agora_app_certificate), channelName, uid, ret -> {
+ if(onGetToken != null){
+ runOnUiThread(() -> {
+ onGetToken.onTokenGen(ret);
+ });
+ }
+ }, ret -> {
+ Log.e("TAG", "for requesting token error, use config token instead.");
+ if (onGetToken != null) {
+ runOnUiThread(() -> {
+ onGetToken.onTokenGen(null);
+ });
+ }
+ });
+ }
+
+ private static void runOnUiThread(@NonNull Runnable runnable){
+ if(Thread.currentThread() == Looper.getMainLooper().getThread()){
+ runnable.run();
+ }else{
+ new Handler(Looper.getMainLooper()).post(runnable);
+ }
+ }
+
+ private static void gen(String appId, String certificate, String channelName, int uid, OnTokenGenCallback onGetToken, OnTokenGenCallback onError) {
+ if(TextUtils.isEmpty(appId) || TextUtils.isEmpty(certificate) || TextUtils.isEmpty(channelName)){
+ if(onError != null){
+ onError.onTokenGen(new IllegalArgumentException("appId=" + appId + ", certificate=" + certificate + ", channelName=" + channelName));
+ }
+ return;
+ }
+ JSONObject postBody = new JSONObject();
+ try {
+ postBody.put("appId", appId);
+ postBody.put("appCertificate", certificate);
+ postBody.put("channelName", channelName);
+ postBody.put("expire", 900);// s
+ postBody.put("src", "Android");
+ postBody.put("ts", System.currentTimeMillis() + "");
+ postBody.put("type", 1); // 1: RTC Token ; 2: RTM Token
+ postBody.put("uid", uid + "");
+ } catch (JSONException e) {
+ if(onError != null){
+ onError.onTokenGen(e);
+ }
+ }
+
+ Request request = new Request.Builder()
+ .url("https://test-toolbox.bj2.agoralab.co/v1/token/generate")
+ .addHeader("Content-Type", "application/json")
+ .post(RequestBody.create(postBody.toString(), null))
+ .build();
+ client.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(@NonNull Call call, @NonNull IOException e) {
+ if(onError != null){
+ onError.onTokenGen(e);
+ }
+ }
+
+ @Override
+ public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
+ ResponseBody body = response.body();
+ if (body != null) {
+ try {
+ JSONObject jsonObject = new JSONObject(body.string());
+ JSONObject data = jsonObject.optJSONObject("data");
+ String token = Objects.requireNonNull(data).optString("token");
+ if(onGetToken != null){
+ onGetToken.onTokenGen(token);
+ }
+ } catch (Exception e) {
+ if(onError != null){
+ onError.onTokenGen(e);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ public interface OnTokenGenCallback {
+ void onTokenGen(T ret);
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/YUVUtils.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/YUVUtils.java
new file mode 100644
index 000000000..2cdb1f1f6
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/YUVUtils.java
@@ -0,0 +1,221 @@
+package io.agora.api.example.utils;
+
+import static android.renderscript.Element.RGBA_8888;
+import static android.renderscript.Element.U8;
+import static android.renderscript.Element.U8_4;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageFormat;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.YuvImage;
+import android.renderscript.Allocation;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.renderscript.ScriptIntrinsicYuvToRGB;
+import android.renderscript.Type.Builder;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+public class YUVUtils {
+
+ public static void encodeI420(byte[] i420, int[] argb, int width, int height) {
+ final int frameSize = width * height;
+
+ int yIndex = 0; // Y start index
+ int uIndex = frameSize; // U statt index
+ int vIndex = frameSize * 5 / 4; // V start index: w*h*5/4
+
+ int a, R, G, B, Y, U, V;
+ int index = 0;
+ for (int j = 0; j < height; j++) {
+ for (int i = 0; i < width; i++) {
+ a = (argb[index] & 0xff000000) >> 24; // is not used obviously
+ R = (argb[index] & 0xff0000) >> 16;
+ G = (argb[index] & 0xff00) >> 8;
+ B = (argb[index] & 0xff) >> 0;
+
+ // well known RGB to YUV algorithm
+ Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
+ U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
+ V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
+
+ // I420(YUV420p) -> YYYYYYYY UU VV
+ i420[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
+ if (j % 2 == 0 && i % 2 == 0) {
+ i420[uIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
+ i420[vIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
+ }
+ index++;
+ }
+ }
+ }
+
+ public static void encodeNV21(byte[] yuv420sp, int[] argb, int width, int height) {
+ final int frameSize = width * height;
+
+ int yIndex = 0;
+ int uvIndex = frameSize;
+
+ int a, R, G, B, Y, U, V;
+ int index = 0;
+ for (int j = 0; j < height; j++) {
+ for (int i = 0; i < width; i++) {
+ a = (argb[index] & 0xff000000) >> 24; // a is not used obviously
+ R = (argb[index] & 0xff0000) >> 16;
+ G = (argb[index] & 0xff00) >> 8;
+ B = (argb[index] & 0xff) >> 0;
+
+ // well known RGB to YUV algorithm
+ Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
+ U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
+ V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
+
+ // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
+ // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
+ // pixel AND every other scanline.
+ yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
+ if (j % 2 == 0 && index % 2 == 0) {
+ yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
+ yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
+ }
+ index++;
+ }
+ }
+ }
+
+ public static void swapYU12toYUV420SP(byte[] yu12bytes, byte[] i420bytes, int width, int height, int yStride, int uStride, int vStride) {
+ System.arraycopy(yu12bytes, 0, i420bytes, 0, yStride * height);
+ int startPos = yStride * height;
+ int yv_start_pos_u = startPos;
+ int yv_start_pos_v = startPos + startPos / 4;
+ for (int i = 0; i < startPos / 4; i++) {
+ i420bytes[startPos + 2 * i + 0] = yu12bytes[yv_start_pos_v + i];
+ i420bytes[startPos + 2 * i + 1] = yu12bytes[yv_start_pos_u + i];
+ }
+ }
+
+ public static Bitmap i420ToBitmap(int width, int height, int rotation, int bufferLength, byte[] buffer, int yStride, int uStride, int vStride) {
+ byte[] NV21 = new byte[bufferLength];
+ swapYU12toYUV420SP(buffer, NV21, width, height, yStride, uStride, vStride);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ int[] strides = {yStride, yStride};
+ YuvImage image = new YuvImage(NV21, ImageFormat.NV21, width, height, strides);
+
+ image.compressToJpeg(
+ new Rect(0, 0, image.getWidth(), image.getHeight()),
+ 100, baos);
+
+ // rotate picture when saving to file
+ Matrix matrix = new Matrix();
+ matrix.postRotate(rotation);
+ byte[] bytes = baos.toByteArray();
+ try {
+ baos.close();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
+ }
+
+ public static Bitmap blur(Context context, Bitmap image, float radius) {
+ RenderScript rs = RenderScript.create(context);
+ Bitmap outputBitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
+ Allocation in = Allocation.createFromBitmap(rs, image);
+ Allocation out = Allocation.createFromBitmap(rs, outputBitmap);
+ ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, U8_4(rs));
+ intrinsicBlur.setRadius(radius);
+ intrinsicBlur.setInput(in);
+ intrinsicBlur.forEach(out);
+
+ out.copyTo(outputBitmap);
+ image.recycle();
+ rs.destroy();
+
+ return outputBitmap;
+ }
+
+ public static byte[] bitmapToI420(int inputWidth, int inputHeight, Bitmap scaled) {
+ int[] argb = new int[inputWidth * inputHeight];
+ scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight);
+ byte[] yuv = new byte[inputWidth * inputHeight * 3 / 2];
+ YUVUtils.encodeI420(yuv, argb, inputWidth, inputHeight);
+ scaled.recycle();
+ return yuv;
+ }
+
+ public static byte[] toWrappedI420(ByteBuffer bufferY,
+ ByteBuffer bufferU,
+ ByteBuffer bufferV,
+ int width,
+ int height) {
+ int chromaWidth = (width + 1) / 2;
+ int chromaHeight = (height + 1) / 2;
+ int lengthY = width * height;
+ int lengthU = chromaWidth * chromaHeight;
+ int lengthV = lengthU;
+
+
+ int size = lengthY + lengthU + lengthV;
+
+ byte[] out = new byte[size];
+
+ for (int i = 0; i < size; i++) {
+ if (i < lengthY) {
+ out[i] = bufferY.get(i);
+ } else if (i < lengthY + lengthU) {
+ int j = (i - lengthY) / chromaWidth;
+ int k = (i - lengthY) % chromaWidth;
+ out[i] = bufferU.get(j * width + k);
+ } else {
+ int j = (i - lengthY - lengthU) / chromaWidth;
+ int k = (i - lengthY - lengthU) % chromaWidth;
+ out[i] = bufferV.get(j * width + k);
+ }
+ }
+
+ return out;
+ }
+ /**
+ * I420转nv21
+ */
+ public static byte[] I420ToNV21(byte[] data, int width, int height) {
+ byte[] ret = new byte[data.length];
+ int total = width * height;
+
+ ByteBuffer bufferY = ByteBuffer.wrap(ret, 0, total);
+ ByteBuffer bufferVU = ByteBuffer.wrap(ret, total, total / 2);
+
+ bufferY.put(data, 0, total);
+ for (int i = 0; i < total / 4; i += 1) {
+ bufferVU.put(data[i + total + total / 4]);
+ bufferVU.put(data[total + i]);
+ }
+
+ return ret;
+ }
+
+ public static Bitmap NV21ToBitmap(Context context, byte[] nv21, int width, int height) {
+ RenderScript rs = RenderScript.create(context);
+ ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, U8_4(rs));
+ Builder yuvType = null;
+ yuvType = (new Builder(rs, U8(rs))).setX(nv21.length);
+ Allocation in = Allocation.createTyped(rs, yuvType.create(), 1);
+ Builder rgbaType = (new Builder(rs, RGBA_8888(rs))).setX(width).setY(height);
+ Allocation out = Allocation.createTyped(rs, rgbaType.create(), 1);
+ in.copyFrom(nv21);
+ yuvToRgbIntrinsic.setInput(in);
+ yuvToRgbIntrinsic.forEach(out);
+ Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ out.copyTo(bmpout);
+ return bmpout;
+ }
+
+}
diff --git a/Android/APIExample-Audio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Android/APIExample-Audio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..2b068d114
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/drawable/ic_launcher_background.xml b/Android/APIExample-Audio/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/drawable/ic_local.png b/Android/APIExample-Audio/app/src/main/res/drawable/ic_local.png
new file mode 100644
index 000000000..4968510c0
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/res/drawable/ic_local.png differ
diff --git a/Android/APIExample-Audio/app/src/main/res/drawable/ic_remote.png b/Android/APIExample-Audio/app/src/main/res/drawable/ic_remote.png
new file mode 100644
index 000000000..99c244ef7
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/res/drawable/ic_remote.png differ
diff --git a/Android/APIExample-Audio/app/src/main/res/drawable/ic_speaker.png b/Android/APIExample-Audio/app/src/main/res/drawable/ic_speaker.png
new file mode 100644
index 000000000..b1a45ff89
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/res/drawable/ic_speaker.png differ
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/activity_main.xml b/Android/APIExample-Audio/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..400fb109a
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/activity_setting_layout.xml b/Android/APIExample-Audio/app/src/main/res/layout/activity_setting_layout.xml
new file mode 100644
index 000000000..3c95f8512
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/activity_setting_layout.xml
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/activity_setting_private_cloud.xml b/Android/APIExample-Audio/app/src/main/res/layout/activity_setting_private_cloud.xml
new file mode 100644
index 000000000..483a3be84
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/activity_setting_private_cloud.xml
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/dialog_spatial_sound.xml b/Android/APIExample-Audio/app/src/main/res/layout/dialog_spatial_sound.xml
new file mode 100644
index 000000000..fafa10227
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/dialog_spatial_sound.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_audio_waveform.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_audio_waveform.xml
new file mode 100644
index 000000000..d3ca4fa96
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_audio_waveform.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_custom_audio_render.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_custom_audio_render.xml
new file mode 100644
index 000000000..52f933a6a
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_custom_audio_render.xml
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_custom_audio_source.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_custom_audio_source.xml
new file mode 100644
index 000000000..044f6a45c
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_custom_audio_source.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_joinchannel_audio.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_joinchannel_audio.xml
new file mode 100644
index 000000000..d1c03a23e
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_joinchannel_audio.xml
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_joinchannel_audio_by_token.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_joinchannel_audio_by_token.xml
new file mode 100644
index 000000000..8b5d3b857
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_joinchannel_audio_by_token.xml
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_main.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_main.xml
new file mode 100644
index 000000000..3fea5bcaa
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_main.xml
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_play_audio_files.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_play_audio_files.xml
new file mode 100644
index 000000000..2bd54b6e4
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_play_audio_files.xml
@@ -0,0 +1,340 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_precall_test.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_precall_test.xml
new file mode 100755
index 000000000..28145eb6d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_precall_test.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_raw_audio.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_raw_audio.xml
new file mode 100644
index 000000000..fca514f00
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_raw_audio.xml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_ready_layout.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_ready_layout.xml
new file mode 100644
index 000000000..155790ea9
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_ready_layout.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_rhythm_player.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_rhythm_player.xml
new file mode 100644
index 000000000..1a35380e3
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_rhythm_player.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_spatial_sound.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_spatial_sound.xml
new file mode 100644
index 000000000..a016b69c9
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_spatial_sound.xml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/fragment_voice_effects.xml b/Android/APIExample-Audio/app/src/main/res/layout/fragment_voice_effects.xml
new file mode 100644
index 000000000..b044e858a
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/fragment_voice_effects.xml
@@ -0,0 +1,580 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/layout_main_list_item.xml b/Android/APIExample-Audio/app/src/main/res/layout/layout_main_list_item.xml
new file mode 100644
index 000000000..b80e29520
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/layout_main_list_item.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/layout_main_list_section.xml b/Android/APIExample-Audio/app/src/main/res/layout/layout_main_list_section.xml
new file mode 100644
index 000000000..5060dded4
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/layout_main_list_section.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/layout/widget_audio_only_layout.xml b/Android/APIExample-Audio/app/src/main/res/layout/widget_audio_only_layout.xml
new file mode 100644
index 000000000..101cd508d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/layout/widget_audio_only_layout.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/menu/menu_main_activity.xml b/Android/APIExample-Audio/app/src/main/res/menu/menu_main_activity.xml
new file mode 100644
index 000000000..314359087
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/menu/menu_main_activity.xml
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/APIExample-Audio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..553d5653e
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Android/APIExample-Audio/app/src/main/res/mipmap-xxhdpi/icon_setting.png b/Android/APIExample-Audio/app/src/main/res/mipmap-xxhdpi/icon_setting.png
new file mode 100644
index 000000000..b06b87d48
Binary files /dev/null and b/Android/APIExample-Audio/app/src/main/res/mipmap-xxhdpi/icon_setting.png differ
diff --git a/Android/APIExample-Audio/app/src/main/res/navigation/nav_graph.xml b/Android/APIExample-Audio/app/src/main/res/navigation/nav_graph.xml
new file mode 100755
index 000000000..cdf54ffa1
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml b/Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 000000000..290c739b0
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,89 @@
+
+
+ Agora API Example(Audio)
+ 频道ID
+ 加入
+ 播放
+ 暂停
+ 离开
+ 停止
+ 听筒
+ 扬声器
+ 打开麦克风
+ 关闭麦克风
+
+
+ 设置
+ SDK版本
+ V%s
+ 混音音量
+ 3D音效环绕周期
+ 下一步
+ 发布麦克风
+ 发布本地音频
+
+ 音频互动直播
+ 音频互动直播(Token验证)
+ 通话前质量检测
+ 快速切换频道
+ 音频文件混音
+ 美声与音效
+ 自定义音频采集
+ 自定义音频渲染
+ 原始音频数据
+ 空间音效
+
+ 此示例演示在使用RTC通话中音频路由对第三方播放器的影响。
+ 此示例演示了如何使用SDK加入频道进行纯语音通话的功能。
+ 此示例演示了如何使用SDK加入带Token的频道进行纯语音通话的功能。
+ 此示例演示了如何使用SDK在进入频道前检测网络质量状况。
+ 此示例演示了在语音通话过程中使用MediaOption控制发布麦克风和自采集音频流的功能。
+ 此示例演示了在语音通话过程中如何自定义远端音频流渲染器的功能。
+ 此示例演示了在音视频通话过程中播放并管理audio effect和audio mixing文件。
+ 此示例演示了在音视频通话过程中如何使用API提供的一些人声效果,或使用API自行组合出想要的人声效果。
+ 此示例演示了在音频通话过程中如何通过回调获取裸数据的功能。
+ 此示例演示了音频通话过程中如何使用虚拟节拍器。
+ 音频回写
+ 此示例演示了如何使用空间音效。
+ 开始
+ 点击开始
+ 恢复播放
+ 混音发布音量
+ 混音播放音量
+ 音效音量
+ 请插入耳机体验3d音频效果
+
+ 虚拟节拍器
+ Beats per Measure
+ Beats per Minute
+ Token无效
+ Token已过期
+
+ 私有云
+ IP地址
+ 请输入IP地址
+ 日志上报
+ 日志服务域名/IP
+ 请输入日志服务域名/IP
+ 日志服务端口
+ 请输入日志服务端口
+ 日志服务路径
+ 请输入日志服务路径
+ 使用Https
+ 音频频谱
+
+ 区域
+
+ 音障
+ 房间
+ 静音
+ 保存
+ AppID为空!
+ 默认
+ 扬声器
+ 听筒
+ 耳机
+ 耳机(TypeC)
+ 蓝牙耳机
+ 请打开通知权限,防止后台录音中断
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/values/arrays.xml b/Android/APIExample-Audio/app/src/main/res/values/arrays.xml
new file mode 100644
index 000000000..d29902f2d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/arrays.xml
@@ -0,0 +1,152 @@
+
+
+
+ DEFAULT
+ SPEECH_STANDARD
+ MUSIC_STANDARD
+ MUSIC_STANDARD_STEREO
+ MUSIC_HIGH_QUALITY
+ MUSIC_HIGH_QUALITY_STEREO
+
+
+ DEFAULT
+ GAME_STREAMING
+ CHATROOM
+
+
+ OFF
+ CHAT_BEAUTIFIER_VITALITY
+ CHAT_BEAUTIFIER_FRESH
+ CHAT_BEAUTIFIER_MAGNETIC
+
+
+ OFF
+ TIMBRE_TRANSFORMATION_VIGOROUS
+ TIMBRE_TRANSFORMATION_DEEP
+ TIMBRE_TRANSFORMATION_MELLOW
+ TIMBRE_TRANSFORMATION_FALSETTO
+ TIMBRE_TRANSFORMATION_FULL
+ TIMBRE_TRANSFORMATION_CLEAR
+ TIMBRE_TRANSFORMATION_RESOUNDING
+ TIMBRE_TRANSFORMATION_RINGING
+
+
+ OFF
+ VOICE_CHANGER_EFFECT_UNCLE
+ VOICE_CHANGER_EFFECT_OLDMAN
+ VOICE_CHANGER_EFFECT_BOY
+ VOICE_CHANGER_EFFECT_SISTER
+ VOICE_CHANGER_EFFECT_GIRL
+ VOICE_CHANGER_EFFECT_PIGKING
+ VOICE_CHANGER_EFFECT_HULK
+
+
+ OFF
+ STYLE_TRANSFORMATION_RNB
+ STYLE_TRANSFORMATION_POPULAR
+
+
+ OFF
+ ROOM_ACOUSTICS_KTV
+ ROOM_ACOUSTICS_VOCAL_CONCERT
+ ROOM_ACOUSTICS_STUDIO
+ ROOM_ACOUSTICS_PHONOGRAPH
+ ROOM_ACOUSTICS_VIRTUAL_STEREO
+ ROOM_ACOUSTICS_SPACIAL
+ ROOM_ACOUSTICS_ETHEREAL
+ ROOM_ACOUSTICS_3D_VOICE
+ ROOM_ACOUSTICS_VIRTUAL_SURROUND_SOUND
+
+
+ VOICE_CONVERSION_OFF
+ VOICE_CHANGER_NEUTRAL
+ VOICE_CHANGER_SWEET
+ VOICE_CHANGER_SOLID
+ VOICE_CHANGER_BASS
+ VOICE_CHANGER_CARTOON
+ VOICE_CHANGER_CHILDLIKE
+ VOICE_CHANGER_PHONE_OPERATOR
+ VOICE_CHANGER_MONSTER
+ VOICE_CHANGER_TRANSFORMERS
+ VOICE_CHANGER_GROOT
+ VOICE_CHANGER_DARTH_VADER
+ VOICE_CHANGER_IRON_LADY
+ VOICE_CHANGER_SHIN_CHAN
+ VOICE_CHANGER_GIRLISH_MAN
+ VOICE_CHANGER_CHIPMUNK
+
+
+ AUDIO_EQUALIZATION_BAND_31
+ AUDIO_EQUALIZATION_BAND_62
+ AUDIO_EQUALIZATION_BAND_125
+ AUDIO_EQUALIZATION_BAND_250
+ AUDIO_EQUALIZATION_BAND_500
+ AUDIO_EQUALIZATION_BAND_1K
+ AUDIO_EQUALIZATION_BAND_2K
+ AUDIO_EQUALIZATION_BAND_4K
+ AUDIO_EQUALIZATION_BAND_8K
+ AUDIO_EQUALIZATION_BAND_16K
+
+
+ AUDIO_REVERB_DRY_LEVEL
+ AUDIO_REVERB_WET_LEVEL
+ AUDIO_REVERB_ROOM_SIZE
+ AUDIO_REVERB_WET_DELAY
+ AUDIO_REVERB_STRENGTH
+
+
+ OFF
+ PITCH_CORRECTION
+
+
+ Natural Major
+ Natural Minor
+ Breeze Minor
+
+
+ A Pitch
+ A# Pitch
+ B Pitch
+ C Pitch
+ C# Pitch
+ D Pitch
+ D# Pitch
+ E Pitch
+ F Pitch
+ F# Pitch
+ G Pitch
+ G# Pitch
+
+
+
+ GLOBAL
+ CN
+ NA
+ EU
+ AS
+ JP
+ IN
+
+
+
+ @string/channel_profile_communication
+ @string/channel_profile_live_broadcasting
+ @string/channel_profile_game
+ @string/channel_profile_cloud_gaming
+ @string/channel_profile_communication_1v1
+
+
+
+ @string/audio_route_speakerphone
+ @string/audio_route_earpiece
+ @string/audio_route_headset
+ @string/audio_route_headset_bluetooth
+
+
+
+ OFF
+ AINS_MODE_BALANCED
+ AINS_MODE_AGGRESSIVE
+ AINS_MODE_ULTRALOWLATENCY
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/values/attrs.xml b/Android/APIExample-Audio/app/src/main/res/values/attrs.xml
new file mode 100644
index 000000000..897350610
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/attrs.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample-Audio/app/src/main/res/values/colors.xml b/Android/APIExample-Audio/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..030098fe0
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
diff --git a/Android/APIExample-Audio/app/src/main/res/values/dimens.xml b/Android/APIExample-Audio/app/src/main/res/values/dimens.xml
new file mode 100644
index 000000000..29260009d
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+
+ 16dp
+
diff --git a/Android/APIExample-Audio/app/src/main/res/values/string_configs.xml b/Android/APIExample-Audio/app/src/main/res/values/string_configs.xml
new file mode 100644
index 000000000..1592e3705
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/string_configs.xml
@@ -0,0 +1,39 @@
+
+
+
+ YOUR APP ID
+
+
+
+ YOUR APP CERTIFICATE
+
+
diff --git a/Android/APIExample-Audio/app/src/main/res/values/strings.xml b/Android/APIExample-Audio/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..bc36eba2c
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/strings.xml
@@ -0,0 +1,97 @@
+
+ Agora API Example(Audio)
+ Channel ID
+ Join
+ Leave
+ Stop
+ Play
+ Pause
+ Earpiece
+ Speaker
+ OpenMicrophone
+ CloseMicrophone
+
+ qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890 !#$%()+-:;<=.>?@[]^_{}|~,&
+
+ Setting
+ SDK Version
+ V%s
+ Mixing Volume
+ 3D Voice Circle in Second
+ Next Step
+ Microphone
+ Publish Local Audio
+
+ Live Interactive Audio Streaming
+ Live Interactive Audio Streaming(Token Verify)
+ Quick Switch Channel
+ Pre-call Tests
+ Play Audio Files
+ Set the Voice Beautifier and Effects
+ Audio Waveform
+
+ Custom Audio Sources
+ Custom Audio Render
+ Raw Audio Data
+ Spatial Audio
+
+ This example demonstrates how to use the SDK to join channels for voice only calls.
+ This example demonstrates how to use the SDK to join channels with token for voice only calls.
+ This example demonstrates how to use the SDK to check uplink network condition before joining the channel.
+ This example demonstrates how to use mediaOption API to publish microphone and custom audio source.
+ This example demonstrates how to customize the functions of the remote audio stream renderer during audio and video calls.
+ This example demonstrates how to play and manage audio effect and audio mixing files.
+ This example demonstrates how to use embedded audio effects in SDK.
+ This example shows how to register Audio Observer to engine for extract raw audio data during RTC communication.
+ This example shows how to use virtual rhythm player for during RTC communication.
+ Write Back Audio
+ This example shows how to use spaital sound
+ Start
+ Start
+ Resume
+ Mixing Publish Vol
+ Mixing Playout Vol
+ Audio Effects Vol
+ Please insert headphones to experience the spatial audio effect
+ This example shows the behavior of audio router while communicating with rtc.
+
+
+ Rhythm Player
+ Beats per Measure
+ Beats per Minute
+ Token
+ The token is invalid.
+ The token is expired.
+
+ Private Cloud
+ IP Address
+ please input IP
+ Log Report
+ Log Server Domain
+ Please input log server domain
+ Log Server Port
+ Please input log server port
+ Log Server Path
+ Please input log server path
+ Use Https
+
+ Area
+ Audio Zone
+ Room
+ Mute Audio
+ Save
+ App ID
+ The App ID is empty!
+ COMMUNICATION
+ LIVE_BROADCASTING
+ GAME
+ CLOUD_GAMING
+ COMMUNICATION_1v1
+ default
+ speakerphone
+ earpiece
+ headset
+ headset(TypeC)
+ bluetooth headset
+ Please turn on notification permission to prevent background recording from being interrupted.
+
diff --git a/Android/APIExample-Audio/app/src/main/res/values/styles.xml b/Android/APIExample-Audio/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..5885930df
--- /dev/null
+++ b/Android/APIExample-Audio/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/Android/APIExample-Audio/build.gradle b/Android/APIExample-Audio/build.gradle
new file mode 100644
index 000000000..948fe78a2
--- /dev/null
+++ b/Android/APIExample-Audio/build.gradle
@@ -0,0 +1,9 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '7.2.0' apply false
+ id 'com.android.library' version '7.2.0' apply false
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/Android/APIExample-Audio/ci.env.py b/Android/APIExample-Audio/ci.env.py
new file mode 100644
index 000000000..dd130dd6f
--- /dev/null
+++ b/Android/APIExample-Audio/ci.env.py
@@ -0,0 +1,23 @@
+#!/usr/bin/python
+# -*- coding: UTF-8 -*-
+import os
+import re
+
+
+def main():
+ appId = ""
+ if "AGORA_APP_ID" in os.environ:
+ appId = os.environ["AGORA_APP_ID"]
+ token = ""
+
+ f = open("./app/src/main/res/values/string_configs.xml", 'r+')
+ content = f.read()
+ contentNew = re.sub(r'YOUR APP ID', appId, content)
+ contentNew = re.sub(r'YOUR ACCESS TOKEN', token, contentNew)
+ f.seek(0)
+ f.write(contentNew)
+ f.truncate()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/Android/APIExample-Audio/gradle.properties b/Android/APIExample-Audio/gradle.properties
new file mode 100644
index 000000000..c0ae9b619
--- /dev/null
+++ b/Android/APIExample-Audio/gradle.properties
@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
+# read enable simple filter section on README first before set this flag to TRUE
+simpleFilter = false
\ No newline at end of file
diff --git a/Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.jar b/Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..f6b961fd5
Binary files /dev/null and b/Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.properties b/Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..061df5425
--- /dev/null
+++ b/Android/APIExample-Audio/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu May 28 16:45:32 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https://services.gradle.org/distributions/gradle-7.3.3-bin.zip
diff --git a/Android/APIExample-Audio/gradlew b/Android/APIExample-Audio/gradlew
new file mode 100755
index 000000000..cccdd3d51
--- /dev/null
+++ b/Android/APIExample-Audio/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Android/APIExample-Audio/gradlew.bat b/Android/APIExample-Audio/gradlew.bat
new file mode 100644
index 000000000..f9553162f
--- /dev/null
+++ b/Android/APIExample-Audio/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Android/APIExample-Audio/settings.gradle b/Android/APIExample-Audio/settings.gradle
new file mode 100644
index 000000000..7aa1c9aba
--- /dev/null
+++ b/Android/APIExample-Audio/settings.gradle
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { url "https://jitpack.io" }
+ maven { url "https://maven.aliyun.com/repository/public" }
+ }
+}
+rootProject.name='APIExample-Audio'
+include ':app'
diff --git a/Android/APIExample/.gitignore b/Android/APIExample/.gitignore
new file mode 100644
index 000000000..b0f139bf0
--- /dev/null
+++ b/Android/APIExample/.gitignore
@@ -0,0 +1,24 @@
+*.so
+*.iml
+.gradle
+/local.properties
+/.idea
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+androidTest/
+Test/
+
+# sdk files
+*.so
+agora-rtc-sdk.jar
+AgoraScreenShareExtension.aar
+/release
\ No newline at end of file
diff --git a/Android/APIExample/README.md b/Android/APIExample/README.md
new file mode 100644
index 000000000..56c2c20f2
--- /dev/null
+++ b/Android/APIExample/README.md
@@ -0,0 +1,134 @@
+# API Example Android
+
+*English | [中文](README.zh.md)*
+
+This project presents you a set of API examples to help you understand how to use Agora APIs.
+
+## Prerequisites
+
+- Android Studio 3.0+
+- Physical Android device
+- Android simulator is supported
+
+## Quick Start
+
+This section shows you how to prepare, build, and run the sample application.
+
+### Obtain an App Id
+
+To build and run the sample application, get an App Id:
+
+1. Create a developer account at [agora.io](https://dashboard.agora.io/signin/). Once you finish the signup process, you will be redirected to the Dashboard.
+2. Navigate in the Dashboard tree on the left to **Projects** > **Project List**.
+3. Save the **App Id** from the Dashboard for later use.
+4. Save the **App Certificate** from the Dashboard for later use.
+
+5. Open `Android/APIExample` and edit the `app/src/main/res/values/string-configs.xml` file. Update `YOUR APP ID` with your App Id, update `YOUR APP CERTIFICATE` with the main app certificate from dashboard. Note you can leave the certificate variable `null` if your project has not turned on security token.
+
+ ```
+ // Agora APP ID.
+ YOUR APP ID
+ // Agora APP Certificate. If the project does not have certificates enabled, leave this field blank.
+ YOUR APP CERTIFICATE
+ ```
+
+You are all set. Now connect your Android device and run the project.
+
+### Beauty Configuration
+
+> Third Part Beauty case use
+> the [Agora Beauty API Library](https://github.com/AgoraIO-Community/BeautyAPI)
+
+This project contains third-party beauty integration examples, which cannot be enabled by default
+without configuring resources and certificates. The resource certificate configuration method is as
+follows:
+
+#### SenseTime
+
+1. Contact SenseTime customer service to obtain the download link and certificate of the beauty sdk
+2. Unzip the beauty sdk, and copy the following resources to the corresponding path
+
+| SenseTime Beauty SDK Path | Location |
+|----------------------------------------------------------------------|----------------------------------------------------------|
+| Android/models | app/src/main/assets/beauty_sensetime/models |
+| Android/smaple/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape |
+| Android/smaple/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly |
+| Android/smaple/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip |
+| SenseME.lic | app/src/main/assets/beauty_sensetime/license/SenseME.lic |
+
+#### FaceUnity
+
+1. Contact FaceUnity customer service to obtain beauty resources and certificates
+2. Put the beauty resource and certificate in the corresponding path
+
+| FaceUnity Beauty Resources | Location |
+|-------------------------------------|--------------------------------------------------------------------------------|
+| makeup resource(e.g. naicha.bundle) | app/src/main/assets/beauty_faceunity/makeup |
+| sticker resource(e.g. fashi.bundle) | app/src/main/assets/beauty_faceunity/sticker |
+| authpack.java | app/src/main/java/io/agora/api/example/examples/advanced/beauty/authpack.java |
+
+#### ByteDance
+
+1. Contact ByteDance customer service to obtain the download link and certificate of the beauty sdk
+2. Unzip the ByteDance beauty resource and copy the following files/directories to the corresponding path
+
+| ByteDance Beauty Resources | Location |
+|---------------------------------|--------------------------------------|
+| resource/LicenseBag.bundle | app/src/main/assets/beauty_bytedance |
+| resource/ModelResource.bundle | app/src/main/assets/beauty_bytedance |
+| resource/ComposeMakeup.bundle | app/src/main/assets/beauty_bytedance |
+| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance |
+| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance |
+
+3. Modify the LICENSE_NAME in the app/src/main/java/io/agora/api/example/examples/advanced/beauty/ByteDanceBeauty.java file to the name of the applied certificate file.
+
+
+### For Agora Extension Developers
+
+Since version 4.0.0, Agora SDK provides an Extension Interface Framework. Developers could publish their own video/audio extension to Agora Extension Market. In this project includes a sample SimpleFilter example, by default it is disabled.
+In order to enable it, you could do as follows:
+
+1. Download [opencv](https://agora-adc-artifacts.s3.cn-north-1.amazonaws.com.cn/androidLibs/opencv4.zip) library, unzip it and copy into Android/APIExample/agora-simple-filter/src/main/jniLibs
+2. Download [Agora SDK包](https://doc.shengwang.cn/doc/rtc/android/resources), unzip it and copy c++ .so library (keeps arch folder) to Android/APIExample/agora-simple-filter/src/main/agoraLibs
+
+```text
+Android/APIExample/agora-simple-filter/src/main/agoraLibs
+├── arm64-v8a
+├── armeabi-v7a
+├── x86
+└── x86_64
+```
+
+3. Modify simpleFilter to true in Android/APIExample/gradle.properties
+
+### Stream Encrypt
+
+This project contains custom stream encrypt examples, which cannot be enabled by default.
+The configuration method is as follows:
+
+1. Download [Agora SDK包](https://doc.shengwang.cn/doc/rtc/android/resources), unzip it and copy c++ .so library (keeps arch folder) to Android/APIExample/agora-stream-encrypt/src/main/agoraLibs
+
+```text
+Android/APIExample/agora-stream-encrypt/src/main/agoraLibs
+├── arm64-v8a
+├── armeabi-v7a
+├── x86
+└── x86_64
+```
+
+2. Modify streamEncrypt to true in Android/APIExample/gradle.properties
+
+
+## Contact Us
+
+- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first
+- Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials
+- Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case
+- Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community)
+- You can find full API documentation at [Document Center](https://docs.agora.io/en/)
+- If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io)
+- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues)
+
+## License
+
+The MIT License (MIT)
diff --git a/Android/APIExample/README.zh.md b/Android/APIExample/README.zh.md
new file mode 100644
index 000000000..876de706e
--- /dev/null
+++ b/Android/APIExample/README.zh.md
@@ -0,0 +1,129 @@
+# API Example Android
+
+*[English](README.md) | 中文*
+
+这个开源示例项目演示了Agora视频SDK的部分API使用示例,以帮助开发者更好地理解和运用Agora视频SDK的API。
+
+## 环境准备
+
+- Android Studio 3.0+
+- Android 真机设备
+- 支持模拟器
+
+## 运行示例程序
+
+这个段落主要讲解了如何编译和运行实例程序。
+
+### 创建Agora账号并获取AppId
+
+在编译和启动实例程序前,你需要首先获取一个可用的App Id:
+
+1. 在[agora.io](https://dashboard.agora.io/signin/)创建一个开发者账号
+2. 前往后台页面,点击左部导航栏的 **项目 > 项目列表** 菜单
+3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它
+4. 复制后台的 **App 证书** 并备注,稍后启动应用时会用到它
+
+5. 打开 `Android/APIExample` 并编辑 `app/src/main/res/values/string-configs.xml`,将你的 AppID 、App主证书 分别替换到 `Your App Id` 和 `YOUR APP CERTIFICATE`
+
+ ```
+ // 声网APP ID。
+ YOUR APP ID
+ // 声网APP证书。如果项目没有开启证书鉴权,这个字段留空。
+ YOUR APP CERTIFICATE
+ ```
+
+然后你就可以编译并运行项目了。
+
+### 美颜配置
+
+> 第三方美颜集成使用了[美颜场景化API库](https://github.com/AgoraIO-Community/BeautyAPI)
+
+本项目包含第三方美颜集成示例,在没有配置资源和证书的情况下,默认是无法启用的。资源证书配置方法如下:
+
+#### 商汤美颜
+
+1. 联系商汤客服获取美颜sdk下载链接以及证书
+2. 解压美颜sdk,并将以下资源复制到对应路径
+
+| 商汤SDK文件/目录 | 项目路径 |
+|----------------------------------------------------------------------|----------------------------------------------------------|
+| Android/models | app/src/main/assets/beauty_sensetime/models |
+| Android/smaple/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape |
+| Android/smaple/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly |
+| Android/smaple/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip |
+| SenseME.lic | app/src/main/assets/beauty_sensetime/license/SenseME.lic |
+
+#### 相芯美颜
+
+1. 联系相芯客服获取美颜资源以及证书
+2. 将美颜资源及证书放到对应路径下
+
+| 美颜资源 | 项目路径 |
+|----------------------|--------------------------------------------------------------------------------|
+| 美妆资源(如naicha.bundle) | app/src/main/assets/beauty_faceunity/makeup |
+| 贴纸资源(如fashi.bundle) | app/src/main/assets/beauty_faceunity/sticker |
+| 证书authpack.java | app/src/main/java/io/agora/api/example/examples/advanced/beauty/authpack.java |
+
+#### 字节美颜
+
+1. 联系字节客服获取美颜sdk下载链接以及证书
+2. 解压字节/火山美颜资源并复制以下文件/目录到对应路径下
+
+| 字节SDK文件/目录 | 项目路径 |
+|--------------------------------------------------|-------------------------------------------------------|
+| resource/LicenseBag.bundle | app/src/main/assets/beauty_bytedance |
+| resource/ModelResource.bundle | app/src/main/assets/beauty_bytedance |
+| resource/ComposeMakeup.bundle | app/src/main/assets/beauty_bytedance |
+| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance |
+| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance |
+
+3.
+修改app/src/main/java/io/agora/api/example/examples/advanced/beauty/ByteDanceBeauty.java文件里LICENSE_NAME为申请到的证书文件名
+
+### 对于Agora Extension开发者
+
+从4.0.0SDK开始,Agora SDK支持插件系统和开放的云市场帮助开发者发布自己的音视频插件,本项目包含了一个SimpleFilter示例,默认是禁用的状态,如果需要开启编译和使用需要完成以下步骤:
+
+1. 下载 [opencv](https://agora-adc-artifacts.s3.cn-north-1.amazonaws.com.cn/androidLibs/opencv4.zip) 解压后复制到 Android/APIExample/agora-simple-filter/src/main/jniLibs
+2. 手动下载[Agora SDK包](https://doc.shengwang.cn/doc/rtc/android/resources), 解压后将c++动态库(包括架构文件夹)copy到Android/APIExample/agora-simple-filter/src/main/agoraLibs
+
+```text
+Android/APIExample/agora-simple-filter/src/main/agoraLibs
+├── arm64-v8a
+├── armeabi-v7a
+├── x86
+└── x86_64
+```
+
+3. 修改Android/APIExample/gradle.properties配置文件中simpleFilter值为true
+
+### 自定义加密
+
+本项目包含自定义加密示例,默认是不启用的。配置方法如下:
+
+1. 手动下载[Agora SDK包](https://doc.shengwang.cn/doc/rtc/android/resources), 解压后将c++动态库(包括架构文件夹)copy到Android/APIExample/agora-stream-encrypt/src/main/agoraLibs
+
+```text
+Android/APIExample/agora-stream-encrypt/src/main/agoraLibs
+├── arm64-v8a
+├── armeabi-v7a
+├── x86
+└── x86_64
+```
+
+2. 修改Android/APIExample/gradle.properties配置文件中streamEncrypt值为true
+
+## 联系我们
+
+- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq)
+- 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO)
+- 如果你想了解声网SDK在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase)
+- 如果你想了解声网的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community)
+- 完整的 API 文档见 [文档中心](https://docs.agora.io/cn/)
+- 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问
+- 如果需要售后技术支持, 你可以在 [Agora Dashboard](https://dashboard.agora.io) 提交工单
+- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/API-Examples/issues)
+
+## 代码许可
+
+The MIT License (MIT)
diff --git a/Android/APIExample/agora-simple-filter/.gitignore b/Android/APIExample/agora-simple-filter/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/Android/APIExample/agora-simple-filter/build.gradle b/Android/APIExample/agora-simple-filter/build.gradle
new file mode 100644
index 000000000..560e7feda
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/build.gradle
@@ -0,0 +1,48 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 32
+ buildToolsVersion "32.0.0"
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 32
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+
+ externalNativeBuild {
+ cmake {
+ cppFlags "-std=c++14"
+ abiFilters "armeabi-v7a", "arm64-v8a"
+ arguments "-DANDROID_STL=c++_shared"
+ }
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ externalNativeBuild {
+ cmake {
+ path "src/main/cpp/CMakeLists.txt"
+ version "3.10.2"
+
+ }
+ }
+
+}
+
+dependencies {
+ api fileTree(dir: "libs", include: ["*.jar", "*.aar"])
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+}
diff --git a/Android/APIExample/agora-simple-filter/consumer-rules.pro b/Android/APIExample/agora-simple-filter/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/Android/APIExample/agora-simple-filter/proguard-rules.pro b/Android/APIExample/agora-simple-filter/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/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/Android/APIExample/agora-simple-filter/src/main/AndroidManifest.xml b/Android/APIExample/agora-simple-filter/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..1f3c2d802
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraAtomicOps.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraAtomicOps.h
new file mode 100644
index 000000000..f0c46109b
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraAtomicOps.h
@@ -0,0 +1,73 @@
+// Copyright (c) 2020 Agora.io. All rights reserved
+
+// This program is confidential and proprietary to Agora.io.
+// And may not be copied, reproduced, modified, disclosed to others, published
+// or used, in whole or in part, without the express prior written permission
+// of Agora.io.
+#pragma once
+
+#if defined(_WIN32)
+// clang-format off
+// clang formating would change include order.
+
+// Include WinSock2.h before including to maintain consistency with
+// win32.h. To include win32.h directly, it must be broken out into its own
+// build target.
+#include
+#include
+// clang-format on
+#endif // _WIN32
+
+namespace agora {
+
+class AtomicOps {
+ public:
+#if defined(_WIN32)
+ // Assumes sizeof(int) == sizeof(LONG), which it is on Win32 and Win64.
+ static int Increment(volatile int* i) {
+ return ::InterlockedIncrement(reinterpret_cast(i));
+ }
+ static int Decrement(volatile int* i) {
+ return ::InterlockedDecrement(reinterpret_cast(i));
+ }
+ static int AcquireLoad(volatile const int* i) { return *i; }
+ static void ReleaseStore(volatile int* i, int value) { *i = value; }
+ static int CompareAndSwap(volatile int* i, int old_value, int new_value) {
+ return ::InterlockedCompareExchange(reinterpret_cast(i),
+ new_value, old_value);
+ }
+ // Pointer variants.
+ template
+ static T* AcquireLoadPtr(T* volatile* ptr) {
+ return *ptr;
+ }
+ template
+ static T* CompareAndSwapPtr(T* volatile* ptr, T* old_value, T* new_value) {
+ return static_cast(::InterlockedCompareExchangePointer(
+ reinterpret_cast(ptr), new_value, old_value));
+ }
+#else
+ static int Increment(volatile int* i) { return __sync_add_and_fetch(i, 1); }
+ static int Decrement(volatile int* i) { return __sync_sub_and_fetch(i, 1); }
+ static int AcquireLoad(volatile const int* i) {
+ return __atomic_load_n(i, __ATOMIC_ACQUIRE);
+ }
+ static void ReleaseStore(volatile int* i, int value) {
+ __atomic_store_n(i, value, __ATOMIC_RELEASE);
+ }
+ static int CompareAndSwap(volatile int* i, int old_value, int new_value) {
+ return __sync_val_compare_and_swap(i, old_value, new_value);
+ }
+ // Pointer variants.
+ template
+ static T* AcquireLoadPtr(T* volatile* ptr) {
+ return __atomic_load_n(ptr, __ATOMIC_ACQUIRE);
+ }
+ template
+ static T* CompareAndSwapPtr(T* volatile* ptr, T* old_value, T* new_value) {
+ return __sync_val_compare_and_swap(ptr, old_value, new_value);
+ }
+#endif // _WIN32
+};
+
+} // namespace agora
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h
new file mode 100644
index 000000000..a3b4647a6
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h
@@ -0,0 +1,6211 @@
+//
+// Agora Engine SDK
+//
+// Created by Sting Feng in 2017-11.
+// Copyright (c) 2017 Agora.io. All rights reserved.
+//
+
+// This header file is included by both high level and low level APIs,
+#pragma once // NOLINT(build/header_guard)
+
+#include
+#include
+#include
+#include
+#include
+
+#include "IAgoraParameter.h"
+#include "AgoraMediaBase.h"
+#include "AgoraRefPtr.h"
+#include "AgoraOptional.h"
+
+#define MAX_PATH_260 (260)
+
+#if defined(_WIN32)
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif // !WIN32_LEAN_AND_MEAN
+#if defined(__aarch64__)
+#include
+#endif
+#include
+
+#if defined(AGORARTC_EXPORT)
+#define AGORA_API extern "C" __declspec(dllexport)
+#else
+#define AGORA_API extern "C" __declspec(dllimport)
+#endif // AGORARTC_EXPORT
+
+#define AGORA_CALL __cdecl
+
+#define __deprecated
+
+#elif defined(__APPLE__)
+
+#include
+
+#define AGORA_API extern "C" __attribute__((visibility("default")))
+#define AGORA_CALL
+
+#elif defined(__ANDROID__) || defined(__linux__)
+
+#define AGORA_API extern "C" __attribute__((visibility("default")))
+#define AGORA_CALL
+
+#define __deprecated
+
+#else // !_WIN32 && !__APPLE__ && !(__ANDROID__ || __linux__)
+
+#define AGORA_API extern "C"
+#define AGORA_CALL
+
+#define __deprecated
+
+#endif // _WIN32
+
+#ifndef OPTIONAL_ENUM_SIZE_T
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+#define OPTIONAL_ENUM_SIZE_T enum : size_t
+#else
+#define OPTIONAL_ENUM_SIZE_T enum
+#endif
+#endif
+
+#ifndef OPTIONAL_NULLPTR
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+#define OPTIONAL_NULLPTR nullptr
+#else
+#define OPTIONAL_NULLPTR NULL
+#endif
+#endif
+
+#define INVALID_DISPLAY_ID (-2)
+
+namespace agora {
+namespace util {
+
+template
+class AutoPtr {
+ protected:
+ typedef T value_type;
+ typedef T* pointer_type;
+
+ public:
+ explicit AutoPtr(pointer_type p = OPTIONAL_NULLPTR) : ptr_(p) {}
+
+ ~AutoPtr() {
+ if (ptr_) {
+ ptr_->release();
+ ptr_ = OPTIONAL_NULLPTR;
+ }
+ }
+
+ operator bool() const { return (ptr_ != OPTIONAL_NULLPTR); }
+
+ value_type& operator*() const { return *get(); }
+
+ pointer_type operator->() const { return get(); }
+
+ pointer_type get() const { return ptr_; }
+
+ pointer_type release() {
+ pointer_type ret = ptr_;
+ ptr_ = 0;
+ return ret;
+ }
+
+ void reset(pointer_type ptr = OPTIONAL_NULLPTR) {
+ if (ptr != ptr_ && ptr_) {
+ ptr_->release();
+ }
+
+ ptr_ = ptr;
+ }
+
+ template
+ bool queryInterface(C1* c, C2 iid) {
+ pointer_type p = OPTIONAL_NULLPTR;
+ if (c && !c->queryInterface(iid, reinterpret_cast(&p))) {
+ reset(p);
+ }
+
+ return (p != OPTIONAL_NULLPTR);
+ }
+
+ private:
+ AutoPtr(const AutoPtr&);
+ AutoPtr& operator=(const AutoPtr&);
+
+ private:
+ pointer_type ptr_;
+};
+
+template
+class CopyableAutoPtr : public AutoPtr {
+ typedef typename AutoPtr::pointer_type pointer_type;
+
+ public:
+ explicit CopyableAutoPtr(pointer_type p = 0) : AutoPtr(p) {}
+ CopyableAutoPtr(const CopyableAutoPtr& rhs) { this->reset(rhs.clone()); }
+ CopyableAutoPtr& operator=(const CopyableAutoPtr& rhs) {
+ if (this != &rhs) this->reset(rhs.clone());
+ return *this;
+ }
+ pointer_type clone() const {
+ if (!this->get()) return OPTIONAL_NULLPTR;
+ return this->get()->clone();
+ }
+};
+
+class IString {
+ public:
+ virtual bool empty() const = 0;
+ virtual const char* c_str() = 0;
+ virtual const char* data() = 0;
+ virtual size_t length() = 0;
+ virtual IString* clone() = 0;
+ virtual void release() = 0;
+ virtual ~IString() {}
+};
+typedef CopyableAutoPtr AString;
+
+class IIterator {
+ public:
+ virtual void* current() = 0;
+ virtual const void* const_current() const = 0;
+ virtual bool next() = 0;
+ virtual void release() = 0;
+ virtual ~IIterator() {}
+};
+
+class IContainer {
+ public:
+ virtual IIterator* begin() = 0;
+ virtual size_t size() const = 0;
+ virtual void release() = 0;
+ virtual ~IContainer() {}
+};
+
+template
+class AOutputIterator {
+ IIterator* p;
+
+ public:
+ typedef T value_type;
+ typedef value_type& reference;
+ typedef const value_type& const_reference;
+ typedef value_type* pointer;
+ typedef const value_type* const_pointer;
+ explicit AOutputIterator(IIterator* it = OPTIONAL_NULLPTR) : p(it) {}
+ ~AOutputIterator() {
+ if (p) p->release();
+ }
+ AOutputIterator(const AOutputIterator& rhs) : p(rhs.p) {}
+ AOutputIterator& operator++() {
+ p->next();
+ return *this;
+ }
+ bool operator==(const AOutputIterator& rhs) const {
+ if (p && rhs.p)
+ return p->current() == rhs.p->current();
+ else
+ return valid() == rhs.valid();
+ }
+ bool operator!=(const AOutputIterator& rhs) const { return !this->operator==(rhs); }
+ reference operator*() { return *reinterpret_cast(p->current()); }
+ const_reference operator*() const { return *reinterpret_cast(p->const_current()); }
+ bool valid() const { return p && p->current() != OPTIONAL_NULLPTR; }
+};
+
+template
+class AList {
+ IContainer* container;
+ bool owner;
+
+ public:
+ typedef T value_type;
+ typedef value_type& reference;
+ typedef const value_type& const_reference;
+ typedef value_type* pointer;
+ typedef const value_type* const_pointer;
+ typedef size_t size_type;
+ typedef AOutputIterator iterator;
+ typedef const AOutputIterator const_iterator;
+
+ public:
+ AList() : container(OPTIONAL_NULLPTR), owner(false) {}
+ AList(IContainer* c, bool take_ownership) : container(c), owner(take_ownership) {}
+ ~AList() { reset(); }
+ void reset(IContainer* c = OPTIONAL_NULLPTR, bool take_ownership = false) {
+ if (owner && container) container->release();
+ container = c;
+ owner = take_ownership;
+ }
+ iterator begin() { return container ? iterator(container->begin()) : iterator(OPTIONAL_NULLPTR); }
+ iterator end() { return iterator(OPTIONAL_NULLPTR); }
+ size_type size() const { return container ? container->size() : 0; }
+ bool empty() const { return size() == 0; }
+};
+
+} // namespace util
+
+/**
+ * The channel profile.
+ */
+enum CHANNEL_PROFILE_TYPE {
+ /**
+ * 0: Communication.
+ *
+ * This profile prioritizes smoothness and applies to the one-to-one scenario.
+ */
+ CHANNEL_PROFILE_COMMUNICATION = 0,
+ /**
+ * 1: (Default) Live Broadcast.
+ *
+ * This profile prioritizes supporting a large audience in a live broadcast channel.
+ */
+ CHANNEL_PROFILE_LIVE_BROADCASTING = 1,
+ /**
+ * 2: Gaming.
+ * @deprecated This profile is deprecated.
+ */
+ CHANNEL_PROFILE_GAME __deprecated = 2,
+ /**
+ * 3: Cloud Gaming.
+ *
+ * @deprecated This profile is deprecated.
+ */
+ CHANNEL_PROFILE_CLOUD_GAMING __deprecated = 3,
+
+ /**
+ * 4: Communication 1v1.
+ * @deprecated This profile is deprecated.
+ */
+ CHANNEL_PROFILE_COMMUNICATION_1v1 __deprecated = 4,
+};
+
+/**
+ * The warning codes.
+ */
+enum WARN_CODE_TYPE {
+ /**
+ * 8: The specified view is invalid. To use the video function, you need to specify
+ * a valid view.
+ */
+ WARN_INVALID_VIEW = 8,
+ /**
+ * 16: Fails to initialize the video function, probably due to a lack of
+ * resources. Users fail to see each other, but can still communicate with voice.
+ */
+ WARN_INIT_VIDEO = 16,
+ /**
+ * 20: The request is pending, usually because some module is not ready,
+ * and the SDK postpones processing the request.
+ */
+ WARN_PENDING = 20,
+ /**
+ * 103: No channel resources are available, probably because the server cannot
+ * allocate any channel resource.
+ */
+ WARN_NO_AVAILABLE_CHANNEL = 103,
+ /**
+ * 104: A timeout occurs when looking for the channel. When joining a channel,
+ * the SDK looks up the specified channel. This warning usually occurs when the
+ * network condition is too poor to connect to the server.
+ */
+ WARN_LOOKUP_CHANNEL_TIMEOUT = 104,
+ /**
+ * 105: The server rejects the request to look for the channel. The server
+ * cannot process this request or the request is illegal.
+ */
+ WARN_LOOKUP_CHANNEL_REJECTED = 105,
+ /**
+ * 106: A timeout occurs when opening the channel. Once the specific channel
+ * is found, the SDK opens the channel. This warning usually occurs when the
+ * network condition is too poor to connect to the server.
+ */
+ WARN_OPEN_CHANNEL_TIMEOUT = 106,
+ /**
+ * 107: The server rejects the request to open the channel. The server
+ * cannot process this request or the request is illegal.
+ */
+ WARN_OPEN_CHANNEL_REJECTED = 107,
+
+ // sdk: 100~1000
+ /**
+ * 111: A timeout occurs when switching the live video.
+ */
+ WARN_SWITCH_LIVE_VIDEO_TIMEOUT = 111,
+ /**
+ * 118: A timeout occurs when setting the user role.
+ */
+ WARN_SET_CLIENT_ROLE_TIMEOUT = 118,
+ /**
+ * 121: The ticket to open the channel is invalid.
+ */
+ WARN_OPEN_CHANNEL_INVALID_TICKET = 121,
+ /**
+ * 122: The SDK is trying connecting to another server.
+ */
+ WARN_OPEN_CHANNEL_TRY_NEXT_VOS = 122,
+ /**
+ * 131: The channel connection cannot be recovered.
+ */
+ WARN_CHANNEL_CONNECTION_UNRECOVERABLE = 131,
+ /**
+ * 132: The SDK connection IP has changed.
+ */
+ WARN_CHANNEL_CONNECTION_IP_CHANGED = 132,
+ /**
+ * 133: The SDK connection port has changed.
+ */
+ WARN_CHANNEL_CONNECTION_PORT_CHANGED = 133,
+ /** 134: The socket error occurs, try to rejoin channel.
+ */
+ WARN_CHANNEL_SOCKET_ERROR = 134,
+ /**
+ * 701: An error occurs when opening the file for audio mixing.
+ */
+ WARN_AUDIO_MIXING_OPEN_ERROR = 701,
+ /**
+ * 1014: Audio Device Module: An exception occurs in the playback device.
+ */
+ WARN_ADM_RUNTIME_PLAYOUT_WARNING = 1014,
+ /**
+ * 1016: Audio Device Module: A warning occurs in the recording device.
+ */
+ WARN_ADM_RUNTIME_RECORDING_WARNING = 1016,
+ /**
+ * 1019: Audio Device Module: No valid audio data is collected.
+ */
+ WARN_ADM_RECORD_AUDIO_SILENCE = 1019,
+ /**
+ * 1020: Audio Device Module: The playback device fails to start.
+ */
+ WARN_ADM_PLAYOUT_MALFUNCTION = 1020,
+ /**
+ * 1021: Audio Device Module: The recording device fails to start.
+ */
+ WARN_ADM_RECORD_MALFUNCTION = 1021,
+ /**
+ * 1031: Audio Device Module: The recorded audio volume is too low.
+ */
+ WARN_ADM_RECORD_AUDIO_LOWLEVEL = 1031,
+ /**
+ * 1032: Audio Device Module: The playback audio volume is too low.
+ */
+ WARN_ADM_PLAYOUT_AUDIO_LOWLEVEL = 1032,
+ /**
+ * 1040: Audio device module: An exception occurs with the audio drive.
+ * Choose one of the following solutions:
+ * - Disable or re-enable the audio device.
+ * - Re-enable your device.
+ * - Update the sound card drive.
+ */
+ WARN_ADM_WINDOWS_NO_DATA_READY_EVENT = 1040,
+ /**
+ * 1051: Audio Device Module: The SDK detects howling.
+ */
+ WARN_APM_HOWLING = 1051,
+ /**
+ * 1052: Audio Device Module: The audio device is in a glitching state.
+ */
+ WARN_ADM_GLITCH_STATE = 1052,
+ /**
+ * 1053: Audio Device Module: The settings are improper.
+ */
+ WARN_ADM_IMPROPER_SETTINGS = 1053,
+ /**
+ * 1322: No recording device.
+ */
+ WARN_ADM_WIN_CORE_NO_RECORDING_DEVICE = 1322,
+ /**
+ * 1323: Audio device module: No available playback device.
+ * You can try plugging in the audio device.
+ */
+ WARN_ADM_WIN_CORE_NO_PLAYOUT_DEVICE = 1323,
+ /**
+ * 1324: Audio device module: The capture device is released improperly.
+ * Choose one of the following solutions:
+ * - Disable or re-enable the audio device.
+ * - Re-enable your audio device.
+ * - Update the sound card drive.
+ */
+ WARN_ADM_WIN_CORE_IMPROPER_CAPTURE_RELEASE = 1324,
+};
+
+/**
+ * The error codes.
+ */
+enum ERROR_CODE_TYPE {
+ /**
+ * 0: No error occurs.
+ */
+ ERR_OK = 0,
+ // 1~1000
+ /**
+ * 1: A general error occurs (no specified reason).
+ */
+ ERR_FAILED = 1,
+ /**
+ * 2: The argument is invalid. For example, the specific channel name
+ * includes illegal characters.
+ */
+ ERR_INVALID_ARGUMENT = 2,
+ /**
+ * 3: The SDK module is not ready. Choose one of the following solutions:
+ * - Check the audio device.
+ * - Check the completeness of the app.
+ * - Reinitialize the RTC engine.
+ */
+ ERR_NOT_READY = 3,
+ /**
+ * 4: The SDK does not support this function.
+ */
+ ERR_NOT_SUPPORTED = 4,
+ /**
+ * 5: The request is rejected.
+ */
+ ERR_REFUSED = 5,
+ /**
+ * 6: The buffer size is not big enough to store the returned data.
+ */
+ ERR_BUFFER_TOO_SMALL = 6,
+ /**
+ * 7: The SDK is not initialized before calling this method.
+ */
+ ERR_NOT_INITIALIZED = 7,
+ /**
+ * 8: The state is invalid.
+ */
+ ERR_INVALID_STATE = 8,
+ /**
+ * 9: No permission. This is for internal use only, and does
+ * not return to the app through any method or callback.
+ */
+ ERR_NO_PERMISSION = 9,
+ /**
+ * 10: An API timeout occurs. Some API methods require the SDK to return the
+ * execution result, and this error occurs if the request takes too long
+ * (more than 10 seconds) for the SDK to process.
+ */
+ ERR_TIMEDOUT = 10,
+ /**
+ * 11: The request is cancelled. This is for internal use only,
+ * and does not return to the app through any method or callback.
+ */
+ ERR_CANCELED = 11,
+ /**
+ * 12: The method is called too often. This is for internal use
+ * only, and does not return to the app through any method or
+ * callback.
+ */
+ ERR_TOO_OFTEN = 12,
+ /**
+ * 13: The SDK fails to bind to the network socket. This is for internal
+ * use only, and does not return to the app through any method or
+ * callback.
+ */
+ ERR_BIND_SOCKET = 13,
+ /**
+ * 14: The network is unavailable. This is for internal use only, and
+ * does not return to the app through any method or callback.
+ */
+ ERR_NET_DOWN = 14,
+ /**
+ * 17: The request to join the channel is rejected. This error usually occurs
+ * when the user is already in the channel, and still calls the method to join
+ * the channel, for example, \ref agora::rtc::IRtcEngine::joinChannel "joinChannel()".
+ */
+ ERR_JOIN_CHANNEL_REJECTED = 17,
+ /**
+ * 18: The request to leave the channel is rejected. This error usually
+ * occurs when the user has already left the channel, and still calls the
+ * method to leave the channel, for example, \ref agora::rtc::IRtcEngine::leaveChannel
+ * "leaveChannel".
+ */
+ ERR_LEAVE_CHANNEL_REJECTED = 18,
+ /**
+ * 19: The resources have been occupied and cannot be reused.
+ */
+ ERR_ALREADY_IN_USE = 19,
+ /**
+ * 20: The SDK gives up the request due to too many requests. This is for
+ * internal use only, and does not return to the app through any method or callback.
+ */
+ ERR_ABORTED = 20,
+ /**
+ * 21: On Windows, specific firewall settings can cause the SDK to fail to
+ * initialize and crash.
+ */
+ ERR_INIT_NET_ENGINE = 21,
+ /**
+ * 22: The app uses too much of the system resource and the SDK
+ * fails to allocate any resource.
+ */
+ ERR_RESOURCE_LIMITED = 22,
+ /**
+ * 101: The App ID is invalid, usually because the data format of the App ID is incorrect.
+ *
+ * Solution: Check the data format of your App ID. Ensure that you use the correct App ID to initialize the Agora service.
+ */
+ ERR_INVALID_APP_ID = 101,
+ /**
+ * 102: The specified channel name is invalid. Please try to rejoin the
+ * channel with a valid channel name.
+ */
+ ERR_INVALID_CHANNEL_NAME = 102,
+ /**
+ * 103: Fails to get server resources in the specified region. Please try to
+ * specify another region when calling \ref agora::rtc::IRtcEngine::initialize
+ * "initialize".
+ */
+ ERR_NO_SERVER_RESOURCES = 103,
+ /**
+ * 109: The token has expired, usually for the following reasons:
+ * - Timeout for token authorization: Once a token is generated, you must use it to access the
+ * Agora service within 24 hours. Otherwise, the token times out and you can no longer use it.
+ * - The token privilege expires: To generate a token, you need to set a timestamp for the token
+ * privilege to expire. For example, If you set it as seven days, the token expires seven days after
+ * its usage. In that case, you can no longer access the Agora service. The users cannot make calls,
+ * or are kicked out of the channel.
+ *
+ * Solution: Regardless of whether token authorization times out or the token privilege expires,
+ * you need to generate a new token on your server, and try to join the channel.
+ */
+ ERR_TOKEN_EXPIRED = 109,
+ /**
+ * 110: The token is invalid, usually for one of the following reasons:
+ * - Did not provide a token when joining a channel in a situation where the project has enabled the
+ * App Certificate.
+ * - Tried to join a channel with a token in a situation where the project has not enabled the App
+ * Certificate.
+ * - The App ID, user ID and channel name that you use to generate the token on the server do not match
+ * those that you use when joining a channel.
+ *
+ * Solution:
+ * - Before joining a channel, check whether your project has enabled the App certificate. If yes, you
+ * must provide a token when joining a channel; if no, join a channel without a token.
+ * - When using a token to join a channel, ensure that the App ID, user ID, and channel name that you
+ * use to generate the token is the same as the App ID that you use to initialize the Agora service, and
+ * the user ID and channel name that you use to join the channel.
+ */
+ ERR_INVALID_TOKEN = 110,
+ /**
+ * 111: The internet connection is interrupted. This applies to the Agora Web
+ * SDK only.
+ */
+ ERR_CONNECTION_INTERRUPTED = 111, // only used in web sdk
+ /**
+ * 112: The internet connection is lost. This applies to the Agora Web SDK
+ * only.
+ */
+ ERR_CONNECTION_LOST = 112, // only used in web sdk
+ /**
+ * 113: The user is not in the channel when calling the
+ * \ref agora::rtc::IRtcEngine::sendStreamMessage "sendStreamMessage()" method.
+ */
+ ERR_NOT_IN_CHANNEL = 113,
+ /**
+ * 114: The data size is over 1024 bytes when the user calls the
+ * \ref agora::rtc::IRtcEngine::sendStreamMessage "sendStreamMessage()" method.
+ */
+ ERR_SIZE_TOO_LARGE = 114,
+ /**
+ * 115: The bitrate of the sent data exceeds the limit of 6 Kbps when the
+ * user calls the \ref agora::rtc::IRtcEngine::sendStreamMessage "sendStreamMessage()".
+ */
+ ERR_BITRATE_LIMIT = 115,
+ /**
+ * 116: Too many data streams (over 5) are created when the user
+ * calls the \ref agora::rtc::IRtcEngine::createDataStream "createDataStream()" method.
+ */
+ ERR_TOO_MANY_DATA_STREAMS = 116,
+ /**
+ * 117: A timeout occurs for the data stream transmission.
+ */
+ ERR_STREAM_MESSAGE_TIMEOUT = 117,
+ /**
+ * 119: Switching the user role fails. Please try to rejoin the channel.
+ */
+ ERR_SET_CLIENT_ROLE_NOT_AUTHORIZED = 119,
+ /**
+ * 120: Decryption fails. The user may have tried to join the channel with a wrong
+ * password. Check your settings or try rejoining the channel.
+ */
+ ERR_DECRYPTION_FAILED = 120,
+ /**
+ * 121: The user ID is invalid.
+ */
+ ERR_INVALID_USER_ID = 121,
+ /**
+ * 123: The app is banned by the server.
+ */
+ ERR_CLIENT_IS_BANNED_BY_SERVER = 123,
+ /**
+ * 130: Encryption is enabled when the user calls the
+ * \ref agora::rtc::IRtcEngine::addPublishStreamUrl "addPublishStreamUrl()" method
+ * (CDN live streaming does not support encrypted streams).
+ */
+ ERR_ENCRYPTED_STREAM_NOT_ALLOWED_PUBLISH = 130,
+
+ /**
+ * 131: License credential is invalid
+ */
+ ERR_LICENSE_CREDENTIAL_INVALID = 131,
+
+ /**
+ * 134: The user account is invalid, usually because the data format of the user account is incorrect.
+ */
+ ERR_INVALID_USER_ACCOUNT = 134,
+
+ /** 157: The necessary dynamical library is not integrated. For example, if you call
+ * the \ref agora::rtc::IRtcEngine::enableDeepLearningDenoise "enableDeepLearningDenoise" but do not integrate the dynamical
+ * library for the deep-learning noise reduction into your project, the SDK reports this error code.
+ *
+ */
+ ERR_MODULE_NOT_FOUND = 157,
+
+ // Licensing, keep the license error code same as the main version
+ ERR_CERT_RAW = 157,
+ ERR_CERT_JSON_PART = 158,
+ ERR_CERT_JSON_INVAL = 159,
+ ERR_CERT_JSON_NOMEM = 160,
+ ERR_CERT_CUSTOM = 161,
+ ERR_CERT_CREDENTIAL = 162,
+ ERR_CERT_SIGN = 163,
+ ERR_CERT_FAIL = 164,
+ ERR_CERT_BUF = 165,
+ ERR_CERT_NULL = 166,
+ ERR_CERT_DUEDATE = 167,
+ ERR_CERT_REQUEST = 168,
+
+ // PcmSend Error num
+ ERR_PCMSEND_FORMAT = 200, // unsupport pcm format
+ ERR_PCMSEND_BUFFEROVERFLOW = 201, // buffer overflow, the pcm send rate too quickly
+
+ /// @cond
+ // signaling: 400~600
+ ERR_LOGIN_ALREADY_LOGIN = 428,
+
+ /// @endcond
+ // 1001~2000
+ /**
+ * 1001: Fails to load the media engine.
+ */
+ ERR_LOAD_MEDIA_ENGINE = 1001,
+ /**
+ * 1005: Audio device module: A general error occurs in the Audio Device Module (no specified
+ * reason). Check if the audio device is used by another app, or try
+ * rejoining the channel.
+ */
+ ERR_ADM_GENERAL_ERROR = 1005,
+ /**
+ * 1008: Audio Device Module: An error occurs in initializing the playback
+ * device.
+ */
+ ERR_ADM_INIT_PLAYOUT = 1008,
+ /**
+ * 1009: Audio Device Module: An error occurs in starting the playback device.
+ */
+ ERR_ADM_START_PLAYOUT = 1009,
+ /**
+ * 1010: Audio Device Module: An error occurs in stopping the playback device.
+ */
+ ERR_ADM_STOP_PLAYOUT = 1010,
+ /**
+ * 1011: Audio Device Module: An error occurs in initializing the recording
+ * device.
+ */
+ ERR_ADM_INIT_RECORDING = 1011,
+ /**
+ * 1012: Audio Device Module: An error occurs in starting the recording device.
+ */
+ ERR_ADM_START_RECORDING = 1012,
+ /**
+ * 1013: Audio Device Module: An error occurs in stopping the recording device.
+ */
+ ERR_ADM_STOP_RECORDING = 1013,
+ /**
+ * 1501: Video Device Module: The camera is not authorized.
+ */
+ ERR_VDM_CAMERA_NOT_AUTHORIZED = 1501,
+};
+
+enum LICENSE_ERROR_TYPE {
+ /**
+ * 1: Invalid license
+ */
+ LICENSE_ERR_INVALID = 1,
+ /**
+ * 2: License expired
+ */
+ LICENSE_ERR_EXPIRE = 2,
+ /**
+ * 3: Exceed license minutes limit
+ */
+ LICENSE_ERR_MINUTES_EXCEED = 3,
+ /**
+ * 4: License use in limited period
+ */
+ LICENSE_ERR_LIMITED_PERIOD = 4,
+ /**
+ * 5: Same license used in different devices at the same time
+ */
+ LICENSE_ERR_DIFF_DEVICES = 5,
+ /**
+ * 99: SDK internal error
+ */
+ LICENSE_ERR_INTERNAL = 99,
+};
+
+/**
+ * The operational permission of the SDK on the audio session.
+ */
+enum AUDIO_SESSION_OPERATION_RESTRICTION {
+ /**
+ * 0: No restriction; the SDK can change the audio session.
+ */
+ AUDIO_SESSION_OPERATION_RESTRICTION_NONE = 0,
+ /**
+ * 1: The SDK cannot change the audio session category.
+ */
+ AUDIO_SESSION_OPERATION_RESTRICTION_SET_CATEGORY = 1,
+ /**
+ * 2: The SDK cannot change the audio session category, mode, or categoryOptions.
+ */
+ AUDIO_SESSION_OPERATION_RESTRICTION_CONFIGURE_SESSION = 1 << 1,
+ /**
+ * 4: The SDK keeps the audio session active when the user leaves the
+ * channel, for example, to play an audio file in the background.
+ */
+ AUDIO_SESSION_OPERATION_RESTRICTION_DEACTIVATE_SESSION = 1 << 2,
+ /**
+ * 128: Completely restricts the operational permission of the SDK on the
+ * audio session; the SDK cannot change the audio session.
+ */
+ AUDIO_SESSION_OPERATION_RESTRICTION_ALL = 1 << 7,
+};
+
+typedef const char* user_id_t;
+typedef void* view_t;
+
+/**
+ * The definition of the UserInfo struct.
+ */
+struct UserInfo {
+ /**
+ * ID of the user.
+ */
+ util::AString userId;
+ /**
+ * Whether the user has enabled audio:
+ * - true: The user has enabled audio.
+ * - false: The user has disabled audio.
+ */
+ bool hasAudio;
+ /**
+ * Whether the user has enabled video:
+ * - true: The user has enabled video.
+ * - false: The user has disabled video.
+ */
+ bool hasVideo;
+
+ UserInfo() : hasAudio(false), hasVideo(false) {}
+};
+
+typedef util::AList UserList;
+
+// Shared between Agora Service and Rtc Engine
+namespace rtc {
+
+/**
+ * Reasons for a user being offline.
+ */
+enum USER_OFFLINE_REASON_TYPE {
+ /**
+ * 0: The user leaves the current channel.
+ */
+ USER_OFFLINE_QUIT = 0,
+ /**
+ * 1: The SDK times out and the user drops offline because no data packet was received within a certain
+ * period of time. If a user quits the call and the message is not passed to the SDK (due to an
+ * unreliable channel), the SDK assumes that the user drops offline.
+ */
+ USER_OFFLINE_DROPPED = 1,
+ /**
+ * 2: The user switches the client role from the host to the audience.
+ */
+ USER_OFFLINE_BECOME_AUDIENCE = 2,
+};
+
+enum INTERFACE_ID_TYPE {
+ AGORA_IID_AUDIO_DEVICE_MANAGER = 1,
+ AGORA_IID_VIDEO_DEVICE_MANAGER = 2,
+ AGORA_IID_PARAMETER_ENGINE = 3,
+ AGORA_IID_MEDIA_ENGINE = 4,
+ AGORA_IID_AUDIO_ENGINE = 5,
+ AGORA_IID_VIDEO_ENGINE = 6,
+ AGORA_IID_RTC_CONNECTION = 7,
+ AGORA_IID_SIGNALING_ENGINE = 8,
+ AGORA_IID_MEDIA_ENGINE_REGULATOR = 9,
+ AGORA_IID_CLOUD_SPATIAL_AUDIO = 10,
+ AGORA_IID_LOCAL_SPATIAL_AUDIO = 11,
+ AGORA_IID_STATE_SYNC = 13,
+ AGORA_IID_META_SERVICE = 14,
+ AGORA_IID_MUSIC_CONTENT_CENTER = 15,
+ AGORA_IID_H265_TRANSCODER = 16,
+};
+
+/**
+ * The network quality types.
+ */
+enum QUALITY_TYPE {
+ /**
+ * 0: The network quality is unknown.
+ * @deprecated This member is deprecated.
+ */
+ QUALITY_UNKNOWN __deprecated = 0,
+ /**
+ * 1: The quality is excellent.
+ */
+ QUALITY_EXCELLENT = 1,
+ /**
+ * 2: The quality is quite good, but the bitrate may be slightly
+ * lower than excellent.
+ */
+ QUALITY_GOOD = 2,
+ /**
+ * 3: Users can feel the communication slightly impaired.
+ */
+ QUALITY_POOR = 3,
+ /**
+ * 4: Users cannot communicate smoothly.
+ */
+ QUALITY_BAD = 4,
+ /**
+ * 5: Users can barely communicate.
+ */
+ QUALITY_VBAD = 5,
+ /**
+ * 6: Users cannot communicate at all.
+ */
+ QUALITY_DOWN = 6,
+ /**
+ * 7: (For future use) The network quality cannot be detected.
+ */
+ QUALITY_UNSUPPORTED = 7,
+ /**
+ * 8: Detecting the network quality.
+ */
+ QUALITY_DETECTING = 8,
+};
+
+/**
+ * Content fit modes.
+ */
+enum FIT_MODE_TYPE {
+ /**
+ * 1: Uniformly scale the video until it fills the visible boundaries (cropped).
+ * One dimension of the video may have clipped contents.
+ */
+ MODE_COVER = 1,
+
+ /**
+ * 2: Uniformly scale the video until one of its dimension fits the boundary
+ * (zoomed to fit). Areas that are not filled due to disparity in the aspect
+ * ratio are filled with black.
+ */
+ MODE_CONTAIN = 2,
+};
+
+/**
+ * The rotation information.
+ */
+enum VIDEO_ORIENTATION {
+ /**
+ * 0: Rotate the video by 0 degree clockwise.
+ */
+ VIDEO_ORIENTATION_0 = 0,
+ /**
+ * 90: Rotate the video by 90 degrees clockwise.
+ */
+ VIDEO_ORIENTATION_90 = 90,
+ /**
+ * 180: Rotate the video by 180 degrees clockwise.
+ */
+ VIDEO_ORIENTATION_180 = 180,
+ /**
+ * 270: Rotate the video by 270 degrees clockwise.
+ */
+ VIDEO_ORIENTATION_270 = 270
+};
+
+/**
+ * The video frame rate.
+ */
+enum FRAME_RATE {
+ /**
+ * 1: 1 fps.
+ */
+ FRAME_RATE_FPS_1 = 1,
+ /**
+ * 7: 7 fps.
+ */
+ FRAME_RATE_FPS_7 = 7,
+ /**
+ * 10: 10 fps.
+ */
+ FRAME_RATE_FPS_10 = 10,
+ /**
+ * 15: 15 fps.
+ */
+ FRAME_RATE_FPS_15 = 15,
+ /**
+ * 24: 24 fps.
+ */
+ FRAME_RATE_FPS_24 = 24,
+ /**
+ * 30: 30 fps.
+ */
+ FRAME_RATE_FPS_30 = 30,
+ /**
+ * 60: 60 fps. Applies to Windows and macOS only.
+ */
+ FRAME_RATE_FPS_60 = 60,
+};
+
+enum FRAME_WIDTH {
+ FRAME_WIDTH_960 = 960,
+};
+
+enum FRAME_HEIGHT {
+ FRAME_HEIGHT_540 = 540,
+};
+
+
+/**
+ * Types of the video frame.
+ */
+enum VIDEO_FRAME_TYPE {
+ /** 0: A black frame. */
+ VIDEO_FRAME_TYPE_BLANK_FRAME = 0,
+ /** 3: Key frame. */
+ VIDEO_FRAME_TYPE_KEY_FRAME = 3,
+ /** 4: Delta frame. */
+ VIDEO_FRAME_TYPE_DELTA_FRAME = 4,
+ /** 5: The B frame.*/
+ VIDEO_FRAME_TYPE_B_FRAME = 5,
+ /** 6: A discarded frame. */
+ VIDEO_FRAME_TYPE_DROPPABLE_FRAME = 6,
+ /** Unknown frame. */
+ VIDEO_FRAME_TYPE_UNKNOW
+};
+
+/**
+ * Video output orientation modes.
+ */
+enum ORIENTATION_MODE {
+ /**
+ * 0: The output video always follows the orientation of the captured video. The receiver takes
+ * the rotational information passed on from the video encoder. This mode applies to scenarios
+ * where video orientation can be adjusted on the receiver:
+ * - If the captured video is in landscape mode, the output video is in landscape mode.
+ * - If the captured video is in portrait mode, the output video is in portrait mode.
+ */
+ ORIENTATION_MODE_ADAPTIVE = 0,
+ /**
+ * 1: Landscape mode. In this mode, the SDK always outputs videos in landscape (horizontal) mode.
+ * If the captured video is in portrait mode, the video encoder crops it to fit the output. Applies
+ * to situations where the receiving end cannot process the rotational information. For example,
+ * CDN live streaming.
+ */
+ ORIENTATION_MODE_FIXED_LANDSCAPE = 1,
+ /**
+ * 2: Portrait mode. In this mode, the SDK always outputs video in portrait (portrait) mode. If
+ * the captured video is in landscape mode, the video encoder crops it to fit the output. Applies
+ * to situations where the receiving end cannot process the rotational information. For example,
+ * CDN live streaming.
+ */
+ ORIENTATION_MODE_FIXED_PORTRAIT = 2,
+};
+
+/**
+ * (For future use) Video degradation preferences under limited bandwidth.
+ */
+enum DEGRADATION_PREFERENCE {
+ /**
+ * 0: (Default) Prefers to reduce the video frame rate while maintaining video quality during video
+ * encoding under limited bandwidth. This degradation preference is suitable for scenarios where
+ * video quality is prioritized.
+ * @note In the COMMUNICATION channel profile, the resolution of the video sent may change, so
+ * remote users need to handle this issue.
+ */
+ MAINTAIN_QUALITY = 0,
+ /**
+ * 1: Prefers to reduce the video quality while maintaining the video frame rate during video
+ * encoding under limited bandwidth. This degradation preference is suitable for scenarios where
+ * smoothness is prioritized and video quality is allowed to be reduced.
+ */
+ MAINTAIN_FRAMERATE = 1,
+ /**
+ * 2: Reduces the video frame rate and video quality simultaneously during video encoding under
+ * limited bandwidth. MAINTAIN_BALANCED has a lower reduction than MAINTAIN_QUALITY and MAINTAIN_FRAMERATE,
+ * and this preference is suitable for scenarios where both smoothness and video quality are a
+ * priority.
+ */
+ MAINTAIN_BALANCED = 2,
+ /**
+ * 3: Degrade framerate in order to maintain resolution.
+ */
+ MAINTAIN_RESOLUTION = 3,
+ /**
+ * 4: Disable VQC adjustion.
+ */
+ DISABLED = 100,
+};
+
+/**
+ * The definition of the VideoDimensions struct.
+ */
+struct VideoDimensions {
+ /**
+ * The width of the video, in pixels.
+ */
+ int width;
+ /**
+ * The height of the video, in pixels.
+ */
+ int height;
+ VideoDimensions() : width(640), height(480) {}
+ VideoDimensions(int w, int h) : width(w), height(h) {}
+ bool operator==(const VideoDimensions& rhs) const {
+ return width == rhs.width && height == rhs.height;
+ }
+};
+
+/**
+ * (Recommended) 0: Standard bitrate mode.
+ *
+ * In this mode, the video bitrate is twice the base bitrate.
+ */
+const int STANDARD_BITRATE = 0;
+
+/**
+ * -1: Compatible bitrate mode.
+ *
+ * In this mode, the video bitrate is the same as the base bitrate.. If you choose
+ * this mode in the live-broadcast profile, the video frame rate may be lower
+ * than the set value.
+ */
+const int COMPATIBLE_BITRATE = -1;
+
+/**
+ * -1: (For future use) The default minimum bitrate.
+ */
+const int DEFAULT_MIN_BITRATE = -1;
+
+/**
+ * -2: (For future use) Set minimum bitrate the same as target bitrate.
+ */
+const int DEFAULT_MIN_BITRATE_EQUAL_TO_TARGET_BITRATE = -2;
+
+/**
+ * screen sharing supported capability level.
+ */
+enum SCREEN_CAPTURE_FRAMERATE_CAPABILITY {
+ SCREEN_CAPTURE_FRAMERATE_CAPABILITY_15_FPS = 0,
+ SCREEN_CAPTURE_FRAMERATE_CAPABILITY_30_FPS = 1,
+ SCREEN_CAPTURE_FRAMERATE_CAPABILITY_60_FPS = 2,
+};
+
+/**
+ * Video codec capability levels.
+ */
+enum VIDEO_CODEC_CAPABILITY_LEVEL {
+ /** No specified level */
+ CODEC_CAPABILITY_LEVEL_UNSPECIFIED = -1,
+ /** Only provide basic support for the codec type */
+ CODEC_CAPABILITY_LEVEL_BASIC_SUPPORT = 5,
+ /** Can process 1080p video at a rate of approximately 30 fps. */
+ CODEC_CAPABILITY_LEVEL_1080P30FPS = 10,
+ /** Can process 1080p video at a rate of approximately 60 fps. */
+ CODEC_CAPABILITY_LEVEL_1080P60FPS = 20,
+ /** Can process 4k video at a rate of approximately 30 fps. */
+ CODEC_CAPABILITY_LEVEL_4K60FPS = 30,
+};
+
+/**
+ * The video codec types.
+ */
+enum VIDEO_CODEC_TYPE {
+ VIDEO_CODEC_NONE = 0,
+ /**
+ * 1: Standard VP8.
+ */
+ VIDEO_CODEC_VP8 = 1,
+ /**
+ * 2: Standard H.264.
+ */
+ VIDEO_CODEC_H264 = 2,
+ /**
+ * 3: Standard H.265.
+ */
+ VIDEO_CODEC_H265 = 3,
+ /**
+ * 6: Generic. This type is used for transmitting raw video data, such as encrypted video frames.
+ * The SDK returns this type of video frames in callbacks, and you need to decode and render the frames yourself.
+ */
+ VIDEO_CODEC_GENERIC = 6,
+ /**
+ * 7: Generic H264.
+ */
+ VIDEO_CODEC_GENERIC_H264 = 7,
+ /**
+ * 12: AV1.
+ */
+ VIDEO_CODEC_AV1 = 12,
+ /**
+ * 5: VP9.
+ */
+ VIDEO_CODEC_VP9 = 13,
+ /**
+ * 20: Generic JPEG. This type consumes minimum computing resources and applies to IoT devices.
+ */
+ VIDEO_CODEC_GENERIC_JPEG = 20,
+};
+
+/**
+ * The CC (Congestion Control) mode options.
+ */
+enum TCcMode {
+ /**
+ * Enable CC mode.
+ */
+ CC_ENABLED,
+ /**
+ * Disable CC mode.
+ */
+ CC_DISABLED,
+};
+
+/**
+ * The configuration for creating a local video track with an encoded image sender.
+ */
+struct SenderOptions {
+ /**
+ * Whether to enable CC mode. See #TCcMode.
+ */
+ TCcMode ccMode;
+ /**
+ * The codec type used for the encoded images: \ref agora::rtc::VIDEO_CODEC_TYPE "VIDEO_CODEC_TYPE".
+ */
+ VIDEO_CODEC_TYPE codecType;
+
+ /**
+ * Target bitrate (Kbps) for video encoding.
+ *
+ * Choose one of the following options:
+ *
+ * - \ref agora::rtc::STANDARD_BITRATE "STANDARD_BITRATE": (Recommended) Standard bitrate.
+ * - Communication profile: The encoding bitrate equals the base bitrate.
+ * - Live-broadcast profile: The encoding bitrate is twice the base bitrate.
+ * - \ref agora::rtc::COMPATIBLE_BITRATE "COMPATIBLE_BITRATE": Compatible bitrate. The bitrate stays the same
+ * regardless of the profile.
+ *
+ * The Communication profile prioritizes smoothness, while the Live Broadcast
+ * profile prioritizes video quality (requiring a higher bitrate). Agora
+ * recommends setting the bitrate mode as \ref agora::rtc::STANDARD_BITRATE "STANDARD_BITRATE" or simply to
+ * address this difference.
+ *
+ * The following table lists the recommended video encoder configurations,
+ * where the base bitrate applies to the communication profile. Set your
+ * bitrate based on this table. If the bitrate you set is beyond the proper
+ * range, the SDK automatically sets it to within the range.
+
+ | Resolution | Frame Rate (fps) | Base Bitrate (Kbps, for Communication) | Live Bitrate (Kbps, for Live Broadcast)|
+ |------------------------|------------------|----------------------------------------|----------------------------------------|
+ | 160 × 120 | 15 | 65 | 130 |
+ | 120 × 120 | 15 | 50 | 100 |
+ | 320 × 180 | 15 | 140 | 280 |
+ | 180 × 180 | 15 | 100 | 200 |
+ | 240 × 180 | 15 | 120 | 240 |
+ | 320 × 240 | 15 | 200 | 400 |
+ | 240 × 240 | 15 | 140 | 280 |
+ | 424 × 240 | 15 | 220 | 440 |
+ | 640 × 360 | 15 | 400 | 800 |
+ | 360 × 360 | 15 | 260 | 520 |
+ | 640 × 360 | 30 | 600 | 1200 |
+ | 360 × 360 | 30 | 400 | 800 |
+ | 480 × 360 | 15 | 320 | 640 |
+ | 480 × 360 | 30 | 490 | 980 |
+ | 640 × 480 | 15 | 500 | 1000 |
+ | 480 × 480 | 15 | 400 | 800 |
+ | 640 × 480 | 30 | 750 | 1500 |
+ | 480 × 480 | 30 | 600 | 1200 |
+ | 848 × 480 | 15 | 610 | 1220 |
+ | 848 × 480 | 30 | 930 | 1860 |
+ | 640 × 480 | 10 | 400 | 800 |
+ | 1280 × 720 | 15 | 1130 | 2260 |
+ | 1280 × 720 | 30 | 1710 | 3420 |
+ | 960 × 720 | 15 | 910 | 1820 |
+ | 960 × 720 | 30 | 1380 | 2760 |
+ | 1920 × 1080 | 15 | 2080 | 4160 |
+ | 1920 × 1080 | 30 | 3150 | 6300 |
+ | 1920 × 1080 | 60 | 4780 | 6500 |
+ | 2560 × 1440 | 30 | 4850 | 6500 |
+ | 2560 × 1440 | 60 | 6500 | 6500 |
+ | 3840 × 2160 | 30 | 6500 | 6500 |
+ | 3840 × 2160 | 60 | 6500 | 6500 |
+ */
+ int targetBitrate;
+
+ SenderOptions()
+ : ccMode(CC_ENABLED),
+ codecType(VIDEO_CODEC_H265),
+ targetBitrate(6500) {}
+};
+
+/**
+ * Audio codec types.
+ */
+enum AUDIO_CODEC_TYPE {
+ /**
+ * 1: OPUS.
+ */
+ AUDIO_CODEC_OPUS = 1,
+ // kIsac = 2,
+ /**
+ * 3: PCMA.
+ */
+ AUDIO_CODEC_PCMA = 3,
+ /**
+ * 4: PCMU.
+ */
+ AUDIO_CODEC_PCMU = 4,
+ /**
+ * 5: G722.
+ */
+ AUDIO_CODEC_G722 = 5,
+ // kIlbc = 6,
+ /** 7: AAC. */
+ // AUDIO_CODEC_AAC = 7,
+ /**
+ * 8: AAC LC.
+ */
+ AUDIO_CODEC_AACLC = 8,
+ /**
+ * 9: HE AAC.
+ */
+ AUDIO_CODEC_HEAAC = 9,
+ /**
+ * 10: JC1.
+ */
+ AUDIO_CODEC_JC1 = 10,
+ /**
+ * 11: HE-AAC v2.
+ */
+ AUDIO_CODEC_HEAAC2 = 11,
+ /**
+ * 12: LPCNET.
+ */
+ AUDIO_CODEC_LPCNET = 12,
+};
+
+/**
+ * Audio encoding types of the audio encoded frame observer.
+ */
+enum AUDIO_ENCODING_TYPE {
+ /**
+ * AAC encoding format, 16000 Hz sampling rate, bass quality. A file with an audio duration of 10
+ * minutes is approximately 1.2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_16000_LOW = 0x010101,
+ /**
+ * AAC encoding format, 16000 Hz sampling rate, medium sound quality. A file with an audio duration
+ * of 10 minutes is approximately 2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_16000_MEDIUM = 0x010102,
+ /**
+ * AAC encoding format, 32000 Hz sampling rate, bass quality. A file with an audio duration of 10
+ * minutes is approximately 1.2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_32000_LOW = 0x010201,
+ /**
+ * AAC encoding format, 32000 Hz sampling rate, medium sound quality. A file with an audio duration
+ * of 10 minutes is approximately 2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_32000_MEDIUM = 0x010202,
+ /**
+ * AAC encoding format, 32000 Hz sampling rate, high sound quality. A file with an audio duration of
+ * 10 minutes is approximately 3.5 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_32000_HIGH = 0x010203,
+ /**
+ * AAC encoding format, 48000 Hz sampling rate, medium sound quality. A file with an audio duration
+ * of 10 minutes is approximately 2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_48000_MEDIUM = 0x010302,
+ /**
+ * AAC encoding format, 48000 Hz sampling rate, high sound quality. A file with an audio duration
+ * of 10 minutes is approximately 3.5 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_AAC_48000_HIGH = 0x010303,
+ /**
+ * OPUS encoding format, 16000 Hz sampling rate, bass quality. A file with an audio duration of 10
+ * minutes is approximately 2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_OPUS_16000_LOW = 0x020101,
+ /**
+ * OPUS encoding format, 16000 Hz sampling rate, medium sound quality. A file with an audio duration
+ * of 10 minutes is approximately 2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_OPUS_16000_MEDIUM = 0x020102,
+ /**
+ * OPUS encoding format, 48000 Hz sampling rate, medium sound quality. A file with an audio duration
+ * of 10 minutes is approximately 2 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_OPUS_48000_MEDIUM = 0x020302,
+ /**
+ * OPUS encoding format, 48000 Hz sampling rate, high sound quality. A file with an audio duration of
+ * 10 minutes is approximately 3.5 MB after encoding.
+ */
+ AUDIO_ENCODING_TYPE_OPUS_48000_HIGH = 0x020303,
+};
+
+/**
+ * The adaptation mode of the watermark.
+ */
+enum WATERMARK_FIT_MODE {
+ /**
+ * Use the `positionInLandscapeMode` and `positionInPortraitMode` values you set in #WatermarkOptions.
+ * The settings in `WatermarkRatio` are invalid.
+ */
+ FIT_MODE_COVER_POSITION,
+ /**
+ * Use the value you set in `WatermarkRatio`. The settings in `positionInLandscapeMode` and `positionInPortraitMode`
+ * in `WatermarkOptions` are invalid.
+ */
+ FIT_MODE_USE_IMAGE_RATIO
+};
+
+/**
+ * The advanced settings of encoded audio frame.
+ */
+struct EncodedAudioFrameAdvancedSettings {
+ EncodedAudioFrameAdvancedSettings()
+ : speech(true),
+ sendEvenIfEmpty(true) {}
+
+ /**
+ * Determines whether the audio source is speech.
+ * - true: (Default) The audio source is speech.
+ * - false: The audio source is not speech.
+ */
+ bool speech;
+ /**
+ * Whether to send the audio frame even when it is empty.
+ * - true: (Default) Send the audio frame even when it is empty.
+ * - false: Do not send the audio frame when it is empty.
+ */
+ bool sendEvenIfEmpty;
+};
+
+/**
+ * The definition of the EncodedAudioFrameInfo struct.
+ */
+struct EncodedAudioFrameInfo {
+ EncodedAudioFrameInfo()
+ : codec(AUDIO_CODEC_AACLC),
+ sampleRateHz(0),
+ samplesPerChannel(0),
+ numberOfChannels(0),
+ captureTimeMs(0) {}
+
+ EncodedAudioFrameInfo(const EncodedAudioFrameInfo& rhs)
+ : codec(rhs.codec),
+ sampleRateHz(rhs.sampleRateHz),
+ samplesPerChannel(rhs.samplesPerChannel),
+ numberOfChannels(rhs.numberOfChannels),
+ advancedSettings(rhs.advancedSettings),
+ captureTimeMs(rhs.captureTimeMs) {}
+ /**
+ * The audio codec: #AUDIO_CODEC_TYPE.
+ */
+ AUDIO_CODEC_TYPE codec;
+ /**
+ * The sample rate (Hz) of the audio frame.
+ */
+ int sampleRateHz;
+ /**
+ * The number of samples per audio channel.
+ *
+ * If this value is not set, it is 1024 for AAC, or 960 for OPUS by default.
+ */
+ int samplesPerChannel;
+ /**
+ * The number of audio channels of the audio frame.
+ */
+ int numberOfChannels;
+ /**
+ * The advanced settings of the audio frame.
+ */
+ EncodedAudioFrameAdvancedSettings advancedSettings;
+
+ /**
+ * This is a input parameter which means the timestamp for capturing the audio frame.
+ */
+ int64_t captureTimeMs;
+};
+/**
+ * The definition of the AudioPcmDataInfo struct.
+ */
+struct AudioPcmDataInfo {
+ AudioPcmDataInfo() : samplesPerChannel(0), channelNum(0), samplesOut(0), elapsedTimeMs(0), ntpTimeMs(0) {}
+
+ AudioPcmDataInfo(const AudioPcmDataInfo& rhs)
+ : samplesPerChannel(rhs.samplesPerChannel),
+ channelNum(rhs.channelNum),
+ samplesOut(rhs.samplesOut),
+ elapsedTimeMs(rhs.elapsedTimeMs),
+ ntpTimeMs(rhs.ntpTimeMs) {}
+
+ /**
+ * The sample count of the PCM data that you expect.
+ */
+ size_t samplesPerChannel;
+
+ int16_t channelNum;
+
+ // Output
+ /**
+ * The number of output samples.
+ */
+ size_t samplesOut;
+ /**
+ * The rendering time (ms).
+ */
+ int64_t elapsedTimeMs;
+ /**
+ * The NTP (Network Time Protocol) timestamp (ms).
+ */
+ int64_t ntpTimeMs;
+};
+/**
+ * Packetization modes. Applies to H.264 only.
+ */
+enum H264PacketizeMode {
+ /**
+ * Non-interleaved mode. See RFC 6184.
+ */
+ NonInterleaved = 0, // Mode 1 - STAP-A, FU-A is allowed
+ /**
+ * Single NAL unit mode. See RFC 6184.
+ */
+ SingleNalUnit, // Mode 0 - only single NALU allowed
+};
+
+/**
+ * Video stream types.
+ */
+enum VIDEO_STREAM_TYPE {
+ /**
+ * 0: The high-quality video stream, which has a higher resolution and bitrate.
+ */
+ VIDEO_STREAM_HIGH = 0,
+ /**
+ * 1: The low-quality video stream, which has a lower resolution and bitrate.
+ */
+ VIDEO_STREAM_LOW = 1,
+};
+
+struct VideoSubscriptionOptions {
+ /**
+ * The type of the video stream to subscribe to.
+ *
+ * The default value is `VIDEO_STREAM_HIGH`, which means the high-quality
+ * video stream.
+ */
+ Optional type;
+ /**
+ * Whether to subscribe to encoded video data only:
+ * - `true`: Subscribe to encoded video data only.
+ * - `false`: (Default) Subscribe to decoded video data.
+ */
+ Optional encodedFrameOnly;
+
+ VideoSubscriptionOptions() {}
+};
+
+
+/** The maximum length of the user account.
+ */
+enum MAX_USER_ACCOUNT_LENGTH_TYPE
+{
+ /** The maximum length of the user account is 256 bytes.
+ */
+ MAX_USER_ACCOUNT_LENGTH = 256
+};
+
+/**
+ * The definition of the EncodedVideoFrameInfo struct, which contains the information of the external encoded video frame.
+ */
+struct EncodedVideoFrameInfo {
+ EncodedVideoFrameInfo()
+ : uid(0),
+ codecType(VIDEO_CODEC_H264),
+ width(0),
+ height(0),
+ framesPerSecond(0),
+ frameType(VIDEO_FRAME_TYPE_BLANK_FRAME),
+ rotation(VIDEO_ORIENTATION_0),
+ trackId(0),
+ captureTimeMs(0),
+ decodeTimeMs(0),
+ streamType(VIDEO_STREAM_HIGH) {}
+
+ EncodedVideoFrameInfo(const EncodedVideoFrameInfo& rhs)
+ : uid(rhs.uid),
+ codecType(rhs.codecType),
+ width(rhs.width),
+ height(rhs.height),
+ framesPerSecond(rhs.framesPerSecond),
+ frameType(rhs.frameType),
+ rotation(rhs.rotation),
+ trackId(rhs.trackId),
+ captureTimeMs(rhs.captureTimeMs),
+ decodeTimeMs(rhs.decodeTimeMs),
+ streamType(rhs.streamType) {}
+
+ EncodedVideoFrameInfo& operator=(const EncodedVideoFrameInfo& rhs) {
+ if (this == &rhs) return *this;
+ uid = rhs.uid;
+ codecType = rhs.codecType;
+ width = rhs.width;
+ height = rhs.height;
+ framesPerSecond = rhs.framesPerSecond;
+ frameType = rhs.frameType;
+ rotation = rhs.rotation;
+ trackId = rhs.trackId;
+ captureTimeMs = rhs.captureTimeMs;
+ decodeTimeMs = rhs.decodeTimeMs;
+ streamType = rhs.streamType;
+ return *this;
+ }
+
+ /**
+ * ID of the user that pushes the the external encoded video frame..
+ */
+ uid_t uid;
+ /**
+ * The codec type of the local video stream. See #VIDEO_CODEC_TYPE. The default value is `VIDEO_CODEC_H265 (3)`.
+ */
+ VIDEO_CODEC_TYPE codecType;
+ /**
+ * The width (px) of the video frame.
+ */
+ int width;
+ /**
+ * The height (px) of the video frame.
+ */
+ int height;
+ /**
+ * The number of video frames per second.
+ * When this parameter is not 0, you can use it to calculate the Unix timestamp of the external
+ * encoded video frames.
+ */
+ int framesPerSecond;
+ /**
+ * The video frame type: #VIDEO_FRAME_TYPE.
+ */
+ VIDEO_FRAME_TYPE frameType;
+ /**
+ * The rotation information of the video frame: #VIDEO_ORIENTATION.
+ */
+ VIDEO_ORIENTATION rotation;
+ /**
+ * The track ID of the video frame.
+ */
+ int trackId; // This can be reserved for multiple video tracks, we need to create different ssrc
+ // and additional payload for later implementation.
+ /**
+ * This is a input parameter which means the timestamp for capturing the video.
+ */
+ int64_t captureTimeMs;
+ /**
+ * The timestamp for decoding the video.
+ */
+ int64_t decodeTimeMs;
+ /**
+ * The stream type of video frame.
+ */
+ VIDEO_STREAM_TYPE streamType;
+
+};
+
+/**
+* Video compression preference.
+*/
+enum COMPRESSION_PREFERENCE {
+ /**
+ * (Default) Low latency is preferred, usually used in real-time communication where low latency is the number one priority.
+ */
+ PREFER_LOW_LATENCY,
+ /**
+ * Prefer quality in sacrifice of a degree of latency, usually around 30ms ~ 150ms, depends target fps
+ */
+ PREFER_QUALITY,
+};
+
+/**
+* The video encoder type preference.
+*/
+enum ENCODING_PREFERENCE {
+ /**
+ *Default .
+ */
+ PREFER_AUTO = -1,
+ /**
+ * Software encoding.
+ */
+ PREFER_SOFTWARE = 0,
+ /**
+ * Hardware encoding
+ */
+ PREFER_HARDWARE = 1,
+};
+
+/**
+ * The definition of the AdvanceOptions struct.
+ */
+struct AdvanceOptions {
+
+ /**
+ * The video encoder type preference..
+ */
+ ENCODING_PREFERENCE encodingPreference;
+
+ /**
+ * Video compression preference.
+ */
+ COMPRESSION_PREFERENCE compressionPreference;
+
+ AdvanceOptions() : encodingPreference(PREFER_AUTO),
+ compressionPreference(PREFER_LOW_LATENCY) {}
+
+ AdvanceOptions(ENCODING_PREFERENCE encoding_preference,
+ COMPRESSION_PREFERENCE compression_preference) :
+ encodingPreference(encoding_preference),
+ compressionPreference(compression_preference) {}
+
+ bool operator==(const AdvanceOptions& rhs) const {
+ return encodingPreference == rhs.encodingPreference &&
+ compressionPreference == rhs.compressionPreference;
+ }
+
+};
+
+/**
+ * Video mirror mode types.
+ */
+enum VIDEO_MIRROR_MODE_TYPE {
+ /**
+ * 0: The mirror mode determined by the SDK.
+ */
+ VIDEO_MIRROR_MODE_AUTO = 0,
+ /**
+ * 1: Enable the mirror mode.
+ */
+ VIDEO_MIRROR_MODE_ENABLED = 1,
+ /**
+ * 2: Disable the mirror mode.
+ */
+ VIDEO_MIRROR_MODE_DISABLED = 2,
+};
+
+
+/** Supported codec type bit mask. */
+enum CODEC_CAP_MASK {
+ /** 0: No codec support. */
+ CODEC_CAP_MASK_NONE = 0,
+
+ /** bit 1: Hardware decoder support flag. */
+ CODEC_CAP_MASK_HW_DEC = 1 << 0,
+
+ /** bit 2: Hardware encoder support flag. */
+ CODEC_CAP_MASK_HW_ENC = 1 << 1,
+
+ /** bit 3: Software decoder support flag. */
+ CODEC_CAP_MASK_SW_DEC = 1 << 2,
+
+ /** bit 4: Software encoder support flag. */
+ CODEC_CAP_MASK_SW_ENC = 1 << 3,
+};
+
+struct CodecCapLevels {
+ VIDEO_CODEC_CAPABILITY_LEVEL hwDecodingLevel;
+ VIDEO_CODEC_CAPABILITY_LEVEL swDecodingLevel;
+
+ CodecCapLevels(): hwDecodingLevel(CODEC_CAPABILITY_LEVEL_UNSPECIFIED), swDecodingLevel(CODEC_CAPABILITY_LEVEL_UNSPECIFIED) {}
+};
+
+/** The codec support information. */
+struct CodecCapInfo {
+ /** The codec type: #VIDEO_CODEC_TYPE. */
+ VIDEO_CODEC_TYPE codecType;
+ /** The codec support flag. */
+ int codecCapMask;
+ /** The codec capability level, estimated based on the device hardware.*/
+ CodecCapLevels codecLevels;
+
+ CodecCapInfo(): codecType(VIDEO_CODEC_NONE), codecCapMask(0) {}
+};
+
+/**
+ * The definition of the VideoEncoderConfiguration struct.
+ */
+struct VideoEncoderConfiguration {
+ /**
+ * The video encoder code type: #VIDEO_CODEC_TYPE.
+ */
+ VIDEO_CODEC_TYPE codecType;
+ /**
+ * The video dimension: VideoDimensions.
+ */
+ VideoDimensions dimensions;
+ /**
+ * The frame rate of the video. You can set it manually, or choose one from #FRAME_RATE.
+ */
+ int frameRate;
+ /**
+ * The bitrate (Kbps) of the video.
+ *
+ * Refer to the **Video Bitrate Table** below and set your bitrate. If you set a bitrate beyond the
+ * proper range, the SDK automatically adjusts it to a value within the range. You can also choose
+ * from the following options:
+ *
+ * - #STANDARD_BITRATE: (Recommended) Standard bitrate mode. In this mode, the bitrates differ between
+ * the Live Broadcast and Communication profiles:
+ * - In the Communication profile, the video bitrate is the same as the base bitrate.
+ * - In the Live Broadcast profile, the video bitrate is twice the base bitrate.
+ * - #COMPATIBLE_BITRATE: Compatible bitrate mode. The compatible bitrate mode. In this mode, the bitrate
+ * stays the same regardless of the profile. If you choose this mode for the Live Broadcast profile,
+ * the video frame rate may be lower than the set value.
+ *
+ * Agora uses different video codecs for different profiles to optimize the user experience. For example,
+ * the communication profile prioritizes the smoothness while the live-broadcast profile prioritizes the
+ * video quality (a higher bitrate). Therefore, We recommend setting this parameter as #STANDARD_BITRATE.
+ *
+ * | Resolution | Frame Rate (fps) | Base Bitrate (Kbps) | Live Bitrate (Kbps)|
+ * |------------------------|------------------|---------------------|--------------------|
+ * | 160 * 120 | 15 | 65 | 110 |
+ * | 120 * 120 | 15 | 50 | 90 |
+ * | 320 * 180 | 15 | 140 | 240 |
+ * | 180 * 180 | 15 | 100 | 160 |
+ * | 240 * 180 | 15 | 120 | 200 |
+ * | 320 * 240 | 15 | 200 | 300 |
+ * | 240 * 240 | 15 | 140 | 240 |
+ * | 424 * 240 | 15 | 220 | 370 |
+ * | 640 * 360 | 15 | 400 | 680 |
+ * | 360 * 360 | 15 | 260 | 440 |
+ * | 640 * 360 | 30 | 600 | 1030 |
+ * | 360 * 360 | 30 | 400 | 670 |
+ * | 480 * 360 | 15 | 320 | 550 |
+ * | 480 * 360 | 30 | 490 | 830 |
+ * | 640 * 480 | 15 | 500 | 750 |
+ * | 480 * 480 | 15 | 400 | 680 |
+ * | 640 * 480 | 30 | 750 | 1130 |
+ * | 480 * 480 | 30 | 600 | 1030 |
+ * | 848 * 480 | 15 | 610 | 920 |
+ * | 848 * 480 | 30 | 930 | 1400 |
+ * | 640 * 480 | 10 | 400 | 600 |
+ * | 960 * 540 | 15 | 750 | 1100 |
+ * | 960 * 540 | 30 | 1110 | 1670 |
+ * | 1280 * 720 | 15 | 1130 | 1600 |
+ * | 1280 * 720 | 30 | 1710 | 2400 |
+ * | 960 * 720 | 15 | 910 | 1280 |
+ * | 960 * 720 | 30 | 1380 | 2000 |
+ * | 1920 * 1080 | 15 | 2080 | 2500 |
+ * | 1920 * 1080 | 30 | 3150 | 3780 |
+ * | 1920 * 1080 | 60 | 4780 | 5730 |
+ * | 2560 * 1440 | 30 | 4850 | 4850 |
+ * | 2560 * 1440 | 60 | 7350 | 7350 |
+ * | 3840 * 2160 | 30 | 8910 | 8910 |
+ * | 3840 * 2160 | 60 | 13500 | 13500 |
+ */
+ int bitrate;
+
+ /**
+ * The minimum encoding bitrate (Kbps).
+ *
+ * The Agora SDK automatically adjusts the encoding bitrate to adapt to the
+ * network conditions.
+ *
+ * Using a value greater than the default value forces the video encoder to
+ * output high-quality images but may cause more packet loss and hence
+ * sacrifice the smoothness of the video transmission. That said, unless you
+ * have special requirements for image quality, Agora does not recommend
+ * changing this value.
+ *
+ * @note
+ * This parameter applies to the live-broadcast profile only.
+ */
+ int minBitrate;
+ /**
+ * The video orientation mode: #ORIENTATION_MODE.
+ */
+ ORIENTATION_MODE orientationMode;
+ /**
+ * The video degradation preference under limited bandwidth: #DEGRADATION_PREFERENCE.
+ */
+ DEGRADATION_PREFERENCE degradationPreference;
+
+ /**
+ * The mirror mode is disabled by default
+ * If mirror_type is set to VIDEO_MIRROR_MODE_ENABLED, then the video frame would be mirrored before encoding.
+ */
+ VIDEO_MIRROR_MODE_TYPE mirrorMode;
+
+ /**
+ * The advanced options for the video encoder configuration. See AdvanceOptions.
+ */
+ AdvanceOptions advanceOptions;
+
+ VideoEncoderConfiguration(const VideoDimensions& d, int f, int b, ORIENTATION_MODE m, VIDEO_MIRROR_MODE_TYPE mirror = VIDEO_MIRROR_MODE_DISABLED)
+ : codecType(VIDEO_CODEC_H265),
+ dimensions(d),
+ frameRate(f),
+ bitrate(b),
+ minBitrate(DEFAULT_MIN_BITRATE),
+ orientationMode(m),
+ degradationPreference(MAINTAIN_QUALITY),
+ mirrorMode(mirror),
+ advanceOptions(PREFER_AUTO, PREFER_LOW_LATENCY) {}
+ VideoEncoderConfiguration(int width, int height, int f, int b, ORIENTATION_MODE m, VIDEO_MIRROR_MODE_TYPE mirror = VIDEO_MIRROR_MODE_DISABLED)
+ : codecType(VIDEO_CODEC_H265),
+ dimensions(width, height),
+ frameRate(f),
+ bitrate(b),
+ minBitrate(DEFAULT_MIN_BITRATE),
+ orientationMode(m),
+ degradationPreference(MAINTAIN_QUALITY),
+ mirrorMode(mirror),
+ advanceOptions(PREFER_AUTO, PREFER_LOW_LATENCY) {}
+ VideoEncoderConfiguration(const VideoEncoderConfiguration& config)
+ : codecType(config.codecType),
+ dimensions(config.dimensions),
+ frameRate(config.frameRate),
+ bitrate(config.bitrate),
+ minBitrate(config.minBitrate),
+ orientationMode(config.orientationMode),
+ degradationPreference(config.degradationPreference),
+ mirrorMode(config.mirrorMode),
+ advanceOptions(config.advanceOptions) {}
+ VideoEncoderConfiguration()
+ : codecType(VIDEO_CODEC_H265),
+ dimensions(FRAME_WIDTH_960, FRAME_HEIGHT_540),
+ frameRate(FRAME_RATE_FPS_15),
+ bitrate(STANDARD_BITRATE),
+ minBitrate(DEFAULT_MIN_BITRATE),
+ orientationMode(ORIENTATION_MODE_ADAPTIVE),
+ degradationPreference(MAINTAIN_QUALITY),
+ mirrorMode(VIDEO_MIRROR_MODE_DISABLED),
+ advanceOptions(PREFER_AUTO, PREFER_LOW_LATENCY) {}
+
+ VideoEncoderConfiguration& operator=(const VideoEncoderConfiguration& rhs) {
+ if (this == &rhs) return *this;
+ codecType = rhs.codecType;
+ dimensions = rhs.dimensions;
+ frameRate = rhs.frameRate;
+ bitrate = rhs.bitrate;
+ minBitrate = rhs.minBitrate;
+ orientationMode = rhs.orientationMode;
+ degradationPreference = rhs.degradationPreference;
+ mirrorMode = rhs.mirrorMode;
+ advanceOptions = rhs.advanceOptions;
+ return *this;
+ }
+};
+
+/**
+ * The configurations for the data stream.
+ */
+struct DataStreamConfig {
+ /**
+ * Whether to synchronize the data packet with the published audio packet.
+ * - `true`: Synchronize the data packet with the audio packet.
+ * - `false`: Do not synchronize the data packet with the audio packet.
+ *
+ * When you set the data packet to synchronize with the audio, then if the data packet delay is
+ * within the audio delay, the SDK triggers the `onStreamMessage` callback when the synchronized
+ * audio packet is played out. Do not set this parameter as true if you need the receiver to receive
+ * the data packet immediately. Agora recommends that you set this parameter to `true` only when you
+ * need to implement specific functions, for example lyric synchronization.
+ */
+ bool syncWithAudio;
+ /**
+ * Whether the SDK guarantees that the receiver receives the data in the sent order.
+ * - `true`: Guarantee that the receiver receives the data in the sent order.
+ * - `false`: Do not guarantee that the receiver receives the data in the sent order.
+ *
+ * Do not set this parameter as `true` if you need the receiver to receive the data packet immediately.
+ */
+ bool ordered;
+};
+
+/**
+ * The definition of SIMULCAST_STREAM_MODE
+ */
+enum SIMULCAST_STREAM_MODE {
+ /*
+ * disable simulcast stream until receive request for enable simulcast stream by other broadcaster
+ */
+ AUTO_SIMULCAST_STREAM = -1,
+ /*
+ * disable simulcast stream
+ */
+ DISABLE_SIMULCAST_STREAM = 0,
+ /*
+ * always enable simulcast stream
+ */
+ ENABLE_SIMULCAST_STREAM = 1,
+};
+
+/**
+ * The configuration of the low-quality video stream.
+ */
+struct SimulcastStreamConfig {
+ /**
+ * The video frame dimension: VideoDimensions. The default value is 160 × 120.
+ */
+ VideoDimensions dimensions;
+ /**
+ * The video bitrate (Kbps), represented by an instantaneous value. The default value of the log level is 5.
+ */
+ int kBitrate;
+ /**
+ * he capture frame rate (fps) of the local video. The default value is 5.
+ */
+ int framerate;
+ SimulcastStreamConfig() : dimensions(160, 120), kBitrate(65), framerate(5) {}
+ bool operator==(const SimulcastStreamConfig& rhs) const {
+ return dimensions == rhs.dimensions && kBitrate == rhs.kBitrate && framerate == rhs.framerate;
+ }
+};
+
+/**
+ * The location of the target area relative to the screen or window. If you do not set this parameter,
+ * the SDK selects the whole screen or window.
+ */
+struct Rectangle {
+ /**
+ * The horizontal offset from the top-left corner.
+ */
+ int x;
+ /**
+ * The vertical offset from the top-left corner.
+ */
+ int y;
+ /**
+ * The width of the region.
+ */
+ int width;
+ /**
+ * The height of the region.
+ */
+ int height;
+
+ Rectangle() : x(0), y(0), width(0), height(0) {}
+ Rectangle(int xx, int yy, int ww, int hh) : x(xx), y(yy), width(ww), height(hh) {}
+};
+
+/**
+ * The position and size of the watermark on the screen.
+ *
+ * The position and size of the watermark on the screen are determined by `xRatio`, `yRatio`, and `widthRatio`:
+ * - (`xRatio`, `yRatio`) refers to the coordinates of the upper left corner of the watermark, which determines
+ * the distance from the upper left corner of the watermark to the upper left corner of the screen.
+ * The `widthRatio` determines the width of the watermark.
+ */
+struct WatermarkRatio {
+ /**
+ * The x-coordinate of the upper left corner of the watermark. The horizontal position relative to
+ * the origin, where the upper left corner of the screen is the origin, and the x-coordinate is the
+ * upper left corner of the watermark. The value range is [0.0,1.0], and the default value is 0.
+ */
+ float xRatio;
+ /**
+ * The y-coordinate of the upper left corner of the watermark. The vertical position relative to the
+ * origin, where the upper left corner of the screen is the origin, and the y-coordinate is the upper
+ * left corner of the screen. The value range is [0.0,1.0], and the default value is 0.
+ */
+ float yRatio;
+ /**
+ * The width of the watermark. The SDK calculates the height of the watermark proportionally according
+ * to this parameter value to ensure that the enlarged or reduced watermark image is not distorted.
+ * The value range is [0,1], and the default value is 0, which means no watermark is displayed.
+ */
+ float widthRatio;
+
+ WatermarkRatio() : xRatio(0.0), yRatio(0.0), widthRatio(0.0) {}
+ WatermarkRatio(float x, float y, float width) : xRatio(x), yRatio(y), widthRatio(width) {}
+};
+
+/**
+ * Configurations of the watermark image.
+ */
+struct WatermarkOptions {
+ /**
+ * Whether or not the watermark image is visible in the local video preview:
+ * - true: (Default) The watermark image is visible in preview.
+ * - false: The watermark image is not visible in preview.
+ */
+ bool visibleInPreview;
+ /**
+ * When the adaptation mode of the watermark is `FIT_MODE_COVER_POSITION`, it is used to set the
+ * area of the watermark image in landscape mode. See #FIT_MODE_COVER_POSITION for details.
+ */
+ Rectangle positionInLandscapeMode;
+ /**
+ * When the adaptation mode of the watermark is `FIT_MODE_COVER_POSITION`, it is used to set the
+ * area of the watermark image in portrait mode. See #FIT_MODE_COVER_POSITION for details.
+ */
+ Rectangle positionInPortraitMode;
+ /**
+ * When the watermark adaptation mode is `FIT_MODE_USE_IMAGE_RATIO`, this parameter is used to set
+ * the watermark coordinates. See WatermarkRatio for details.
+ */
+ WatermarkRatio watermarkRatio;
+ /**
+ * The adaptation mode of the watermark. See #WATERMARK_FIT_MODE for details.
+ */
+ WATERMARK_FIT_MODE mode;
+
+ WatermarkOptions()
+ : visibleInPreview(true),
+ positionInLandscapeMode(0, 0, 0, 0),
+ positionInPortraitMode(0, 0, 0, 0),
+ mode(FIT_MODE_COVER_POSITION) {}
+};
+
+/**
+ * The definition of the RtcStats struct.
+ */
+struct RtcStats {
+ /**
+ * The call duration (s), represented by an aggregate value.
+ */
+ unsigned int duration;
+ /**
+ * The total number of bytes transmitted, represented by an aggregate value.
+ */
+ unsigned int txBytes;
+ /**
+ * The total number of bytes received, represented by an aggregate value.
+ */
+ unsigned int rxBytes;
+ /**
+ * The total number of audio bytes sent (bytes), represented by an aggregate value.
+ */
+ unsigned int txAudioBytes;
+ /**
+ * The total number of video bytes sent (bytes), represented by an aggregate value.
+ */
+ unsigned int txVideoBytes;
+ /**
+ * The total number of audio bytes received (bytes), represented by an aggregate value.
+ */
+ unsigned int rxAudioBytes;
+ /**
+ * The total number of video bytes received (bytes), represented by an aggregate value.
+ */
+ unsigned int rxVideoBytes;
+ /**
+ * The transmission bitrate (Kbps), represented by an instantaneous value.
+ */
+ unsigned short txKBitRate;
+ /**
+ * The receiving bitrate (Kbps), represented by an instantaneous value.
+ */
+ unsigned short rxKBitRate;
+ /**
+ * Audio receiving bitrate (Kbps), represented by an instantaneous value.
+ */
+ unsigned short rxAudioKBitRate;
+ /**
+ * The audio transmission bitrate (Kbps), represented by an instantaneous value.
+ */
+ unsigned short txAudioKBitRate;
+ /**
+ * The video receive bitrate (Kbps), represented by an instantaneous value.
+ */
+ unsigned short rxVideoKBitRate;
+ /**
+ * The video transmission bitrate (Kbps), represented by an instantaneous value.
+ */
+ unsigned short txVideoKBitRate;
+ /**
+ * The VOS client-server latency (ms).
+ */
+ unsigned short lastmileDelay;
+ /**
+ * The number of users in the channel.
+ */
+ unsigned int userCount;
+ /**
+ * The app CPU usage (%).
+ * @note
+ * - The value of `cpuAppUsage` is always reported as 0 in the `onLeaveChannel` callback.
+ * - As of Android 8.1, you cannot get the CPU usage from this attribute due to system limitations.
+ */
+ double cpuAppUsage;
+ /**
+ * The system CPU usage (%).
+ *
+ * For Windows, in the multi-kernel environment, this member represents the average CPU usage. The
+ * value = (100 - System Idle Progress in Task Manager)/100.
+ * @note
+ * - The value of `cpuTotalUsage` is always reported as 0 in the `onLeaveChannel` callback.
+ * - As of Android 8.1, you cannot get the CPU usage from this attribute due to system limitations.
+ */
+ double cpuTotalUsage;
+ /**
+ * The round-trip time delay from the client to the local router.
+ * @note On Android, to get `gatewayRtt`, ensure that you add the `android.permission.ACCESS_WIFI_STATE`
+ * permission after `` in the `AndroidManifest.xml` file in your project.
+ */
+ int gatewayRtt;
+ /**
+ * The memory usage ratio of the app (%).
+ * @note This value is for reference only. Due to system limitations, you may not get this value.
+ */
+ double memoryAppUsageRatio;
+ /**
+ * The memory usage ratio of the system (%).
+ * @note This value is for reference only. Due to system limitations, you may not get this value.
+ */
+ double memoryTotalUsageRatio;
+ /**
+ * The memory usage of the app (KB).
+ * @note This value is for reference only. Due to system limitations, you may not get this value.
+ */
+ int memoryAppUsageInKbytes;
+ /**
+ * The time elapsed from the when the app starts connecting to an Agora channel
+ * to when the connection is established. 0 indicates that this member does not apply.
+ */
+ int connectTimeMs;
+ /**
+ * The duration (ms) between the app starting connecting to an Agora channel
+ * and the first audio packet is received. 0 indicates that this member does not apply.
+ */
+ int firstAudioPacketDuration;
+ /**
+ * The duration (ms) between the app starting connecting to an Agora channel
+ * and the first video packet is received. 0 indicates that this member does not apply.
+ */
+ int firstVideoPacketDuration;
+ /**
+ * The duration (ms) between the app starting connecting to an Agora channel
+ * and the first video key frame is received. 0 indicates that this member does not apply.
+ */
+ int firstVideoKeyFramePacketDuration;
+ /**
+ * The number of video packets before the first video key frame is received.
+ * 0 indicates that this member does not apply.
+ */
+ int packetsBeforeFirstKeyFramePacket;
+ /**
+ * The duration (ms) between the last time unmute audio and the first audio packet is received.
+ * 0 indicates that this member does not apply.
+ */
+ int firstAudioPacketDurationAfterUnmute;
+ /**
+ * The duration (ms) between the last time unmute video and the first video packet is received.
+ * 0 indicates that this member does not apply.
+ */
+ int firstVideoPacketDurationAfterUnmute;
+ /**
+ * The duration (ms) between the last time unmute video and the first video key frame is received.
+ * 0 indicates that this member does not apply.
+ */
+ int firstVideoKeyFramePacketDurationAfterUnmute;
+ /**
+ * The duration (ms) between the last time unmute video and the first video key frame is decoded.
+ * 0 indicates that this member does not apply.
+ */
+ int firstVideoKeyFrameDecodedDurationAfterUnmute;
+ /**
+ * The duration (ms) between the last time unmute video and the first video key frame is rendered.
+ * 0 indicates that this member does not apply.
+ */
+ int firstVideoKeyFrameRenderedDurationAfterUnmute;
+ /**
+ * The packet loss rate of sender(broadcaster).
+ */
+ int txPacketLossRate;
+ /**
+ * The packet loss rate of receiver(audience).
+ */
+ int rxPacketLossRate;
+ RtcStats()
+ : duration(0),
+ txBytes(0),
+ rxBytes(0),
+ txAudioBytes(0),
+ txVideoBytes(0),
+ rxAudioBytes(0),
+ rxVideoBytes(0),
+ txKBitRate(0),
+ rxKBitRate(0),
+ rxAudioKBitRate(0),
+ txAudioKBitRate(0),
+ rxVideoKBitRate(0),
+ txVideoKBitRate(0),
+ lastmileDelay(0),
+ userCount(0),
+ cpuAppUsage(0.0),
+ cpuTotalUsage(0.0),
+ gatewayRtt(0),
+ memoryAppUsageRatio(0.0),
+ memoryTotalUsageRatio(0.0),
+ memoryAppUsageInKbytes(0),
+ connectTimeMs(0),
+ firstAudioPacketDuration(0),
+ firstVideoPacketDuration(0),
+ firstVideoKeyFramePacketDuration(0),
+ packetsBeforeFirstKeyFramePacket(0),
+ firstAudioPacketDurationAfterUnmute(0),
+ firstVideoPacketDurationAfterUnmute(0),
+ firstVideoKeyFramePacketDurationAfterUnmute(0),
+ firstVideoKeyFrameDecodedDurationAfterUnmute(0),
+ firstVideoKeyFrameRenderedDurationAfterUnmute(0),
+ txPacketLossRate(0),
+ rxPacketLossRate(0) {}
+};
+
+/**
+ * User role types.
+ */
+enum CLIENT_ROLE_TYPE {
+ /**
+ * 1: Broadcaster. A broadcaster can both send and receive streams.
+ */
+ CLIENT_ROLE_BROADCASTER = 1,
+ /**
+ * 2: Audience. An audience member can only receive streams.
+ */
+ CLIENT_ROLE_AUDIENCE = 2,
+};
+
+/**
+ * Quality change of the local video in terms of target frame rate and target bit rate since last count.
+ */
+enum QUALITY_ADAPT_INDICATION {
+ /**
+ * 0: The quality of the local video stays the same.
+ */
+ ADAPT_NONE = 0,
+ /**
+ * 1: The quality improves because the network bandwidth increases.
+ */
+ ADAPT_UP_BANDWIDTH = 1,
+ /**
+ * 2: The quality worsens because the network bandwidth decreases.
+ */
+ ADAPT_DOWN_BANDWIDTH = 2,
+};
+
+/**
+ * The latency level of an audience member in interactive live streaming. This enum takes effect only
+ * when the user role is set to `CLIENT_ROLE_AUDIENCE`.
+ */
+enum AUDIENCE_LATENCY_LEVEL_TYPE
+{
+ /**
+ * 1: Low latency.
+ */
+ AUDIENCE_LATENCY_LEVEL_LOW_LATENCY = 1,
+ /**
+ * 2: Ultra low latency.
+ */
+ AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY = 2,
+};
+
+/**
+ * The detailed options of a user.
+ */
+struct ClientRoleOptions
+{
+ /**
+ * The latency level of an audience member in interactive live streaming. See `AUDIENCE_LATENCY_LEVEL_TYPE`.
+ */
+ AUDIENCE_LATENCY_LEVEL_TYPE audienceLatencyLevel;
+
+ ClientRoleOptions()
+ : audienceLatencyLevel(AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY) {}
+};
+
+/**
+ * Quality of experience (QoE) of the local user when receiving a remote audio stream.
+ */
+enum EXPERIENCE_QUALITY_TYPE {
+ /** 0: QoE of the local user is good. */
+ EXPERIENCE_QUALITY_GOOD = 0,
+ /** 1: QoE of the local user is poor. */
+ EXPERIENCE_QUALITY_BAD = 1,
+};
+
+/**
+ * Reasons why the QoE of the local user when receiving a remote audio stream is poor.
+ */
+enum EXPERIENCE_POOR_REASON {
+ /**
+ * 0: No reason, indicating good QoE of the local user.
+ */
+ EXPERIENCE_REASON_NONE = 0,
+ /**
+ * 1: The remote user's network quality is poor.
+ */
+ REMOTE_NETWORK_QUALITY_POOR = 1,
+ /**
+ * 2: The local user's network quality is poor.
+ */
+ LOCAL_NETWORK_QUALITY_POOR = 2,
+ /**
+ * 4: The local user's Wi-Fi or mobile network signal is weak.
+ */
+ WIRELESS_SIGNAL_POOR = 4,
+ /**
+ * 8: The local user enables both Wi-Fi and bluetooth, and their signals interfere with each other.
+ * As a result, audio transmission quality is undermined.
+ */
+ WIFI_BLUETOOTH_COEXIST = 8,
+};
+
+/**
+ * Audio AINS mode
+ */
+enum AUDIO_AINS_MODE {
+ /**
+ * AINS mode with soft suppression level.
+ */
+ AINS_MODE_BALANCED = 0,
+ /**
+ * AINS mode with high suppression level.
+ */
+ AINS_MODE_AGGRESSIVE = 1,
+ /**
+ * AINS mode with high suppression level and ultra-low-latency
+ */
+ AINS_MODE_ULTRALOWLATENCY = 2
+};
+
+/**
+ * Audio profile types.
+ */
+enum AUDIO_PROFILE_TYPE {
+ /**
+ * 0: The default audio profile.
+ * - For the Communication profile:
+ * - Windows: A sample rate of 16 kHz, audio encoding, mono, and a bitrate of up to 16 Kbps.
+ * - Android/macOS/iOS: A sample rate of 32 kHz, audio encoding, mono, and a bitrate of up to 18 Kbps.
+ * of up to 16 Kbps.
+ * - For the Live-broadcast profile: A sample rate of 48 kHz, music encoding, mono, and a bitrate of up to 64 Kbps.
+ */
+ AUDIO_PROFILE_DEFAULT = 0,
+ /**
+ * 1: A sample rate of 32 kHz, audio encoding, mono, and a bitrate of up to 18 Kbps.
+ */
+ AUDIO_PROFILE_SPEECH_STANDARD = 1,
+ /**
+ * 2: A sample rate of 48 kHz, music encoding, mono, and a bitrate of up to 64 Kbps.
+ */
+ AUDIO_PROFILE_MUSIC_STANDARD = 2,
+ /**
+ * 3: A sample rate of 48 kHz, music encoding, stereo, and a bitrate of up to 80 Kbps.
+ *
+ * To implement stereo audio, you also need to call `setAdvancedAudioOptions` and set `audioProcessingChannels`
+ * to `AUDIO_PROCESSING_STEREO` in `AdvancedAudioOptions`.
+ */
+ AUDIO_PROFILE_MUSIC_STANDARD_STEREO = 3,
+ /**
+ * 4: A sample rate of 48 kHz, music encoding, mono, and a bitrate of up to 96 Kbps.
+ */
+ AUDIO_PROFILE_MUSIC_HIGH_QUALITY = 4,
+ /**
+ * 5: A sample rate of 48 kHz, music encoding, stereo, and a bitrate of up to 128 Kbps.
+ *
+ * To implement stereo audio, you also need to call `setAdvancedAudioOptions` and set `audioProcessingChannels`
+ * to `AUDIO_PROCESSING_STEREO` in `AdvancedAudioOptions`.
+ */
+ AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO = 5,
+ /**
+ * 6: A sample rate of 16 kHz, audio encoding, mono, and Acoustic Echo Cancellation (AES) enabled.
+ */
+ AUDIO_PROFILE_IOT = 6,
+ AUDIO_PROFILE_NUM = 7
+};
+
+/**
+ * The audio scenario.
+ */
+enum AUDIO_SCENARIO_TYPE {
+ /**
+ * 0: Automatic scenario, where the SDK chooses the appropriate audio quality according to the
+ * user role and audio route.
+ */
+ AUDIO_SCENARIO_DEFAULT = 0,
+ /**
+ * 3: (Recommended) The live gaming scenario, which needs to enable gaming
+ * audio effects in the speaker. Choose this scenario to achieve high-fidelity
+ * music playback.
+ */
+ AUDIO_SCENARIO_GAME_STREAMING = 3,
+ /**
+ * 5: The chatroom scenario, which needs to keep recording when setClientRole to audience.
+ * Normally, app developer can also use mute api to achieve the same result,
+ * and we implement this 'non-orthogonal' behavior only to make API backward compatible.
+ */
+ AUDIO_SCENARIO_CHATROOM = 5,
+ /**
+ * 7: Real-time chorus scenario, where users have good network conditions and require ultra-low latency.
+ */
+ AUDIO_SCENARIO_CHORUS = 7,
+ /**
+ * 8: Meeting
+ */
+ AUDIO_SCENARIO_MEETING = 8,
+ /**
+ * 9: The number of enumerations.
+ */
+ AUDIO_SCENARIO_NUM = 9,
+};
+
+/**
+ * The format of the video frame.
+ */
+struct VideoFormat {
+ OPTIONAL_ENUM_SIZE_T {
+ /** The maximum value (px) of the width. */
+ kMaxWidthInPixels = 3840,
+ /** The maximum value (px) of the height. */
+ kMaxHeightInPixels = 2160,
+ /** The maximum value (fps) of the frame rate. */
+ kMaxFps = 60,
+ };
+
+ /**
+ * The width (px) of the video.
+ */
+ int width; // Number of pixels.
+ /**
+ * The height (px) of the video.
+ */
+ int height; // Number of pixels.
+ /**
+ * The video frame rate (fps).
+ */
+ int fps;
+ VideoFormat() : width(FRAME_WIDTH_960), height(FRAME_HEIGHT_540), fps(FRAME_RATE_FPS_15) {}
+ VideoFormat(int w, int h, int f) : width(w), height(h), fps(f) {}
+
+ bool operator<(const VideoFormat& fmt) const {
+ if (height != fmt.height) {
+ return height < fmt.height;
+ } else if (width != fmt.width) {
+ return width < fmt.width;
+ } else {
+ return fps < fmt.fps;
+ }
+ }
+ bool operator==(const VideoFormat& fmt) const {
+ return width == fmt.width && height == fmt.height && fps == fmt.fps;
+ }
+ bool operator!=(const VideoFormat& fmt) const {
+ return !operator==(fmt);
+ }
+};
+
+/**
+ * Video content hints.
+ */
+enum VIDEO_CONTENT_HINT {
+ /**
+ * (Default) No content hint. In this case, the SDK balances smoothness with sharpness.
+ */
+ CONTENT_HINT_NONE,
+ /**
+ * Choose this option if you prefer smoothness or when
+ * you are sharing motion-intensive content such as a video clip, movie, or video game.
+ *
+ *
+ */
+ CONTENT_HINT_MOTION,
+ /**
+ * Choose this option if you prefer sharpness or when you are
+ * sharing montionless content such as a picture, PowerPoint slide, ot text.
+ *
+ */
+ CONTENT_HINT_DETAILS
+};
+/**
+ * The screen sharing scenario.
+ */
+enum SCREEN_SCENARIO_TYPE {
+ /**
+ * 1: Document. This scenario prioritizes the video quality of screen sharing and reduces the
+ * latency of the shared video for the receiver. If you share documents, slides, and tables,
+ * you can set this scenario.
+ */
+ SCREEN_SCENARIO_DOCUMENT = 1,
+ /**
+ * 2: Game. This scenario prioritizes the smoothness of screen sharing. If you share games, you
+ * can set this scenario.
+ */
+ SCREEN_SCENARIO_GAMING = 2,
+ /**
+ * 3: Video. This scenario prioritizes the smoothness of screen sharing. If you share movies or
+ * live videos, you can set this scenario.
+ */
+ SCREEN_SCENARIO_VIDEO = 3,
+ /**
+ * 4: Remote control. This scenario prioritizes the video quality of screen sharing and reduces
+ * the latency of the shared video for the receiver. If you share the device desktop being
+ * remotely controlled, you can set this scenario.
+ */
+ SCREEN_SCENARIO_RDC = 4,
+};
+
+
+/**
+ * The video application scenario type.
+ */
+enum VIDEO_APPLICATION_SCENARIO_TYPE {
+ /**
+ * 0: Default Scenario.
+ */
+ APPLICATION_SCENARIO_GENERAL = 0,
+ /**
+ * 1: Meeting Scenario. This scenario is the best QoE practice of meeting application.
+ */
+ APPLICATION_SCENARIO_MEETING = 1,
+};
+
+/**
+ * The video QoE preference type.
+ */
+enum VIDEO_QOE_PREFERENCE_TYPE {
+ /**
+ * 1: Default QoE type, balance the delay, picture quality and fluency.
+ */
+ VIDEO_QOE_PREFERENCE_BALANCE = 1,
+ /**
+ * 2: lower the e2e delay.
+ */
+ VIDEO_QOE_PREFERENCE_DELAY_FIRST = 2,
+ /**
+ * 3: picture quality.
+ */
+ VIDEO_QOE_PREFERENCE_PICTURE_QUALITY_FIRST = 3,
+ /**
+ * 4: more fluency.
+ */
+ VIDEO_QOE_PREFERENCE_FLUENCY_FIRST = 4,
+
+};
+
+/**
+ * The brightness level of the video image captured by the local camera.
+ */
+enum CAPTURE_BRIGHTNESS_LEVEL_TYPE {
+ /** -1: The SDK does not detect the brightness level of the video image.
+ * Wait a few seconds to get the brightness level from `CAPTURE_BRIGHTNESS_LEVEL_TYPE` in the next callback.
+ */
+ CAPTURE_BRIGHTNESS_LEVEL_INVALID = -1,
+ /** 0: The brightness level of the video image is normal.
+ */
+ CAPTURE_BRIGHTNESS_LEVEL_NORMAL = 0,
+ /** 1: The brightness level of the video image is too bright.
+ */
+ CAPTURE_BRIGHTNESS_LEVEL_BRIGHT = 1,
+ /** 2: The brightness level of the video image is too dark.
+ */
+ CAPTURE_BRIGHTNESS_LEVEL_DARK = 2,
+};
+
+/**
+ * Local audio states.
+ */
+enum LOCAL_AUDIO_STREAM_STATE {
+ /**
+ * 0: The local audio is in the initial state.
+ */
+ LOCAL_AUDIO_STREAM_STATE_STOPPED = 0,
+ /**
+ * 1: The capturing device starts successfully.
+ */
+ LOCAL_AUDIO_STREAM_STATE_RECORDING = 1,
+ /**
+ * 2: The first audio frame encodes successfully.
+ */
+ LOCAL_AUDIO_STREAM_STATE_ENCODING = 2,
+ /**
+ * 3: The local audio fails to start.
+ */
+ LOCAL_AUDIO_STREAM_STATE_FAILED = 3
+};
+
+/**
+ * Local audio state error codes.
+ */
+enum LOCAL_AUDIO_STREAM_REASON {
+ /**
+ * 0: The local audio is normal.
+ */
+ LOCAL_AUDIO_STREAM_REASON_OK = 0,
+ /**
+ * 1: No specified reason for the local audio failure. Remind your users to try to rejoin the channel.
+ */
+ LOCAL_AUDIO_STREAM_REASON_FAILURE = 1,
+ /**
+ * 2: No permission to use the local audio device. Remind your users to grant permission.
+ */
+ LOCAL_AUDIO_STREAM_REASON_DEVICE_NO_PERMISSION = 2,
+ /**
+ * 3: (Android and iOS only) The local audio capture device is used. Remind your users to check
+ * whether another application occupies the microphone. Local audio capture automatically resume
+ * after the microphone is idle for about five seconds. You can also try to rejoin the channel
+ * after the microphone is idle.
+ */
+ LOCAL_AUDIO_STREAM_REASON_DEVICE_BUSY = 3,
+ /**
+ * 4: The local audio capture failed.
+ */
+ LOCAL_AUDIO_STREAM_REASON_RECORD_FAILURE = 4,
+ /**
+ * 5: The local audio encoding failed.
+ */
+ LOCAL_AUDIO_STREAM_REASON_ENCODE_FAILURE = 5,
+ /** 6: The SDK cannot find the local audio recording device.
+ */
+ LOCAL_AUDIO_STREAM_REASON_NO_RECORDING_DEVICE = 6,
+ /** 7: The SDK cannot find the local audio playback device.
+ */
+ LOCAL_AUDIO_STREAM_REASON_NO_PLAYOUT_DEVICE = 7,
+ /**
+ * 8: The local audio capturing is interrupted by the system call.
+ */
+ LOCAL_AUDIO_STREAM_REASON_INTERRUPTED = 8,
+ /** 9: An invalid audio capture device ID.
+ */
+ LOCAL_AUDIO_STREAM_REASON_RECORD_INVALID_ID = 9,
+ /** 10: An invalid audio playback device ID.
+ */
+ LOCAL_AUDIO_STREAM_REASON_PLAYOUT_INVALID_ID = 10,
+};
+
+/** Local video state types.
+ */
+enum LOCAL_VIDEO_STREAM_STATE {
+ /**
+ * 0: The local video is in the initial state.
+ */
+ LOCAL_VIDEO_STREAM_STATE_STOPPED = 0,
+ /**
+ * 1: The local video capturing device starts successfully. The SDK also reports this state when
+ * you call `startScreenCaptureByWindowId` to share a maximized window.
+ */
+ LOCAL_VIDEO_STREAM_STATE_CAPTURING = 1,
+ /**
+ * 2: The first video frame is successfully encoded.
+ */
+ LOCAL_VIDEO_STREAM_STATE_ENCODING = 2,
+ /**
+ * 3: Fails to start the local video.
+ */
+ LOCAL_VIDEO_STREAM_STATE_FAILED = 3
+};
+
+/**
+ * Local video state error codes.
+ */
+enum LOCAL_VIDEO_STREAM_REASON {
+ /**
+ * 0: The local video is normal.
+ */
+ LOCAL_VIDEO_STREAM_REASON_OK = 0,
+ /**
+ * 1: No specified reason for the local video failure.
+ */
+ LOCAL_VIDEO_STREAM_REASON_FAILURE = 1,
+ /**
+ * 2: No permission to use the local video capturing device. Remind the user to grant permission
+ * and rejoin the channel.
+ */
+ LOCAL_VIDEO_STREAM_REASON_DEVICE_NO_PERMISSION = 2,
+ /**
+ * 3: The local video capturing device is in use. Remind the user to check whether another
+ * application occupies the camera.
+ */
+ LOCAL_VIDEO_STREAM_REASON_DEVICE_BUSY = 3,
+ /**
+ * 4: The local video capture fails. Remind the user to check whether the video capture device
+ * is working properly or the camera is occupied by another application, and then to rejoin the
+ * channel.
+ */
+ LOCAL_VIDEO_STREAM_REASON_CAPTURE_FAILURE = 4,
+ /**
+ * 5: The local video encoder is not supported.
+ */
+ LOCAL_VIDEO_STREAM_REASON_CODEC_NOT_SUPPORT = 5,
+ /**
+ * 6: (iOS only) The app is in the background. Remind the user that video capture cannot be
+ * performed normally when the app is in the background.
+ */
+ LOCAL_VIDEO_STREAM_REASON_CAPTURE_INBACKGROUND = 6,
+ /**
+ * 7: (iOS only) The current application window is running in Slide Over, Split View, or Picture
+ * in Picture mode, and another app is occupying the camera. Remind the user that the application
+ * cannot capture video properly when the app is running in Slide Over, Split View, or Picture in
+ * Picture mode and another app is occupying the camera.
+ */
+ LOCAL_VIDEO_STREAM_REASON_CAPTURE_MULTIPLE_FOREGROUND_APPS = 7,
+ /**
+ * 8: Fails to find a local video capture device. Remind the user to check whether the camera is
+ * connected to the device properly or the camera is working properly, and then to rejoin the
+ * channel.
+ */
+ LOCAL_VIDEO_STREAM_REASON_DEVICE_NOT_FOUND = 8,
+ /**
+ * 9: (macOS only) The video capture device currently in use is disconnected (such as being
+ * unplugged).
+ */
+ LOCAL_VIDEO_STREAM_REASON_DEVICE_DISCONNECTED = 9,
+ /**
+ * 10: (macOS and Windows only) The SDK cannot find the video device in the video device list.
+ * Check whether the ID of the video device is valid.
+ */
+ LOCAL_VIDEO_STREAM_REASON_DEVICE_INVALID_ID = 10,
+ /**
+ * 101: The current video capture device is unavailable due to excessive system pressure.
+ */
+ LOCAL_VIDEO_STREAM_REASON_DEVICE_SYSTEM_PRESSURE = 101,
+ /**
+ * 11: (macOS only) The shared window is minimized when you call `startScreenCaptureByWindowId`
+ * to share a window. The SDK cannot share a minimized window. You can cancel the minimization
+ * of this window at the application layer, for example by maximizing this window.
+ */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_MINIMIZED = 11,
+ /**
+ * 12: (macOS and Windows only) The error code indicates that a window shared by the window ID
+ * has been closed or a full-screen window shared by the window ID has exited full-screen mode.
+ * After exiting full-screen mode, remote users cannot see the shared window. To prevent remote
+ * users from seeing a black screen, Agora recommends that you immediately stop screen sharing.
+ *
+ * Common scenarios for reporting this error code:
+ * - When the local user closes the shared window, the SDK reports this error code.
+ * - The local user shows some slides in full-screen mode first, and then shares the windows of
+ * the slides. After the user exits full-screen mode, the SDK reports this error code.
+ * - The local user watches a web video or reads a web document in full-screen mode first, and
+ * then shares the window of the web video or document. After the user exits full-screen mode,
+ * the SDK reports this error code.
+ */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_CLOSED = 12,
+ /** 13: The local screen capture window is occluded. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_OCCLUDED = 13,
+ /** 20: The local screen capture window is not supported. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_NOT_SUPPORTED = 20,
+ /** 21: The screen capture fails. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_FAILURE = 21,
+ /** 22: No permision to capture screen. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_NO_PERMISSION = 22,
+ /**
+ * 24: (Windows Only) An unexpected error (possibly due to window block failure) occurs during the screen
+ * sharing process, resulting in performance degradation. However, the screen sharing process itself is
+ * functioning normally.
+ */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_AUTO_FALLBACK = 24,
+ /** 25: (Windows only) The local screen capture window is currently hidden and not visible on the desktop. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_HIDDEN = 25,
+ /** 26: (Windows only) The local screen capture window is recovered from its hidden state. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_RECOVER_FROM_HIDDEN = 26,
+ /** 27:(Windows only) The window is recovered from miniminzed */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_WINDOW_RECOVER_FROM_MINIMIZED = 27,
+ /**
+ * 28: The screen capture paused.
+ *
+ * Common scenarios for reporting this error code:
+ * - When the desktop switch to the secure desktop such as UAC dialog or the Winlogon desktop on
+ * Windows platform, the SDK reports this error code.
+ */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_PAUSED = 28,
+ /** 29: The screen capture is resumed. */
+ LOCAL_VIDEO_STREAM_REASON_SCREEN_CAPTURE_RESUMED = 29,
+
+};
+
+/**
+ * Remote audio states.
+ */
+enum REMOTE_AUDIO_STATE
+{
+ /**
+ * 0: The remote audio is in the default state. The SDK reports this state in the case of
+ * `REMOTE_AUDIO_REASON_LOCAL_MUTED(3)`, `REMOTE_AUDIO_REASON_REMOTE_MUTED(5)`, or
+ * `REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7)`.
+ */
+ REMOTE_AUDIO_STATE_STOPPED = 0, // Default state, audio is started or remote user disabled/muted audio stream
+ /**
+ * 1: The first remote audio packet is received.
+ */
+ REMOTE_AUDIO_STATE_STARTING = 1, // The first audio frame packet has been received
+ /**
+ * 2: The remote audio stream is decoded and plays normally. The SDK reports this state in the case of
+ * `REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2)`, `REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4)`, or
+ * `REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6)`.
+ */
+ REMOTE_AUDIO_STATE_DECODING = 2, // The first remote audio frame has been decoded or fronzen state ends
+ /**
+ * 3: The remote audio is frozen. The SDK reports this state in the case of
+ * `REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1)`.
+ */
+ REMOTE_AUDIO_STATE_FROZEN = 3, // Remote audio is frozen, probably due to network issue
+ /**
+ * 4: The remote audio fails to start. The SDK reports this state in the case of
+ * `REMOTE_AUDIO_REASON_INTERNAL(0)`.
+ */
+ REMOTE_AUDIO_STATE_FAILED = 4, // Remote audio play failed
+};
+
+/**
+ * Reasons for the remote audio state change.
+ */
+enum REMOTE_AUDIO_STATE_REASON
+{
+ /**
+ * 0: The SDK reports this reason when the video state changes.
+ */
+ REMOTE_AUDIO_REASON_INTERNAL = 0,
+ /**
+ * 1: Network congestion.
+ */
+ REMOTE_AUDIO_REASON_NETWORK_CONGESTION = 1,
+ /**
+ * 2: Network recovery.
+ */
+ REMOTE_AUDIO_REASON_NETWORK_RECOVERY = 2,
+ /**
+ * 3: The local user stops receiving the remote audio stream or
+ * disables the audio module.
+ */
+ REMOTE_AUDIO_REASON_LOCAL_MUTED = 3,
+ /**
+ * 4: The local user resumes receiving the remote audio stream or
+ * enables the audio module.
+ */
+ REMOTE_AUDIO_REASON_LOCAL_UNMUTED = 4,
+ /**
+ * 5: The remote user stops sending the audio stream or disables the
+ * audio module.
+ */
+ REMOTE_AUDIO_REASON_REMOTE_MUTED = 5,
+ /**
+ * 6: The remote user resumes sending the audio stream or enables the
+ * audio module.
+ */
+ REMOTE_AUDIO_REASON_REMOTE_UNMUTED = 6,
+ /**
+ * 7: The remote user leaves the channel.
+ */
+ REMOTE_AUDIO_REASON_REMOTE_OFFLINE = 7,
+};
+
+/**
+ * The state of the remote video.
+ */
+enum REMOTE_VIDEO_STATE {
+ /**
+ * 0: The remote video is in the default state. The SDK reports this state in the case of
+ * `REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED (3)`, `REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED (5)`,
+ * `REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE (7)`, or `REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK (8)`.
+ */
+ REMOTE_VIDEO_STATE_STOPPED = 0,
+ /**
+ * 1: The first remote video packet is received.
+ */
+ REMOTE_VIDEO_STATE_STARTING = 1,
+ /**
+ * 2: The remote video stream is decoded and plays normally. The SDK reports this state in the case of
+ * `REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2)`, `REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED (4)`,
+ * `REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED (6)`, or `REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY (9)`.
+ */
+ REMOTE_VIDEO_STATE_DECODING = 2,
+ /** 3: The remote video is frozen, probably due to
+ * #REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION (1).
+ */
+ REMOTE_VIDEO_STATE_FROZEN = 3,
+ /** 4: The remote video fails to start. The SDK reports this state in the case of
+ * `REMOTE_VIDEO_STATE_REASON_INTERNAL (0)`.
+ */
+ REMOTE_VIDEO_STATE_FAILED = 4,
+};
+/**
+ * The reason for the remote video state change.
+ */
+enum REMOTE_VIDEO_STATE_REASON {
+ /**
+ * 0: The SDK reports this reason when the video state changes.
+ */
+ REMOTE_VIDEO_STATE_REASON_INTERNAL = 0,
+ /**
+ * 1: Network congestion.
+ */
+ REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION = 1,
+ /**
+ * 2: Network recovery.
+ */
+ REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY = 2,
+ /**
+ * 3: The local user stops receiving the remote video stream or disables the video module.
+ */
+ REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED = 3,
+ /**
+ * 4: The local user resumes receiving the remote video stream or enables the video module.
+ */
+ REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED = 4,
+ /**
+ * 5: The remote user stops sending the video stream or disables the video module.
+ */
+ REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED = 5,
+ /**
+ * 6: The remote user resumes sending the video stream or enables the video module.
+ */
+ REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED = 6,
+ /**
+ * 7: The remote user leaves the channel.
+ */
+ REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE = 7,
+ /** 8: The remote audio-and-video stream falls back to the audio-only stream
+ * due to poor network conditions.
+ */
+ REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK = 8,
+ /** 9: The remote audio-only stream switches back to the audio-and-video
+ * stream after the network conditions improve.
+ */
+ REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY = 9,
+ /** (Internal use only) 10: The remote video stream type change to low stream type
+ */
+ REMOTE_VIDEO_STATE_REASON_VIDEO_STREAM_TYPE_CHANGE_TO_LOW = 10,
+ /** (Internal use only) 11: The remote video stream type change to high stream type
+ */
+ REMOTE_VIDEO_STATE_REASON_VIDEO_STREAM_TYPE_CHANGE_TO_HIGH = 11,
+ /** (iOS only) 12: The app of the remote user is in background.
+ */
+ REMOTE_VIDEO_STATE_REASON_SDK_IN_BACKGROUND = 12,
+
+ /** 13: The remote video stream is not supported by the decoder
+ */
+ REMOTE_VIDEO_STATE_REASON_CODEC_NOT_SUPPORT = 13,
+
+};
+
+/**
+ * The remote user state information.
+ */
+enum REMOTE_USER_STATE {
+ /**
+ * The remote user has muted the audio.
+ */
+ USER_STATE_MUTE_AUDIO = (1 << 0),
+ /**
+ * The remote user has muted the video.
+ */
+ USER_STATE_MUTE_VIDEO = (1 << 1),
+ /**
+ * The remote user has enabled the video, which includes video capturing and encoding.
+ */
+ USER_STATE_ENABLE_VIDEO = (1 << 4),
+ /**
+ * The remote user has enabled the local video capturing.
+ */
+ USER_STATE_ENABLE_LOCAL_VIDEO = (1 << 8),
+};
+
+/**
+ * The definition of the VideoTrackInfo struct, which contains information of
+ * the video track.
+ */
+struct VideoTrackInfo {
+ VideoTrackInfo()
+ : isLocal(false), ownerUid(0), trackId(0), channelId(OPTIONAL_NULLPTR)
+ , streamType(VIDEO_STREAM_HIGH), codecType(VIDEO_CODEC_H265)
+ , encodedFrameOnly(false), sourceType(VIDEO_SOURCE_CAMERA_PRIMARY)
+ , observationPosition(agora::media::base::POSITION_POST_CAPTURER) {}
+ /**
+ * Whether the video track is local or remote.
+ * - true: The video track is local.
+ * - false: The video track is remote.
+ */
+ bool isLocal;
+ /**
+ * ID of the user who publishes the video track.
+ */
+ uid_t ownerUid;
+ /**
+ * ID of the video track.
+ */
+ track_id_t trackId;
+ /**
+ * The channel ID of the video track.
+ */
+ const char* channelId;
+ /**
+ * The video stream type: #VIDEO_STREAM_TYPE.
+ */
+ VIDEO_STREAM_TYPE streamType;
+ /**
+ * The video codec type: #VIDEO_CODEC_TYPE.
+ */
+ VIDEO_CODEC_TYPE codecType;
+ /**
+ * Whether the video track contains encoded video frame only.
+ * - true: The video track contains encoded video frame only.
+ * - false: The video track does not contain encoded video frame only.
+ */
+ bool encodedFrameOnly;
+ /**
+ * The video source type: #VIDEO_SOURCE_TYPE
+ */
+ VIDEO_SOURCE_TYPE sourceType;
+ /**
+ * the frame position for the video observer: #VIDEO_MODULE_POSITION
+ */
+ uint32_t observationPosition;
+};
+
+/**
+ * The downscale level of the remote video stream . The higher the downscale level, the more the video downscales.
+ */
+enum REMOTE_VIDEO_DOWNSCALE_LEVEL {
+ /**
+ * No downscale.
+ */
+ REMOTE_VIDEO_DOWNSCALE_LEVEL_NONE,
+ /**
+ * Downscale level 1.
+ */
+ REMOTE_VIDEO_DOWNSCALE_LEVEL_1,
+ /**
+ * Downscale level 2.
+ */
+ REMOTE_VIDEO_DOWNSCALE_LEVEL_2,
+ /**
+ * Downscale level 3.
+ */
+ REMOTE_VIDEO_DOWNSCALE_LEVEL_3,
+ /**
+ * Downscale level 4.
+ */
+ REMOTE_VIDEO_DOWNSCALE_LEVEL_4,
+};
+
+/**
+ * The volume information of users.
+ */
+struct AudioVolumeInfo {
+ /**
+ * User ID of the speaker.
+ * - In the local user's callback, `uid` = 0.
+ * - In the remote users' callback, `uid` is the user ID of a remote user whose instantaneous
+ * volume is one of the three highest.
+ */
+ uid_t uid;
+ /**
+ * The volume of the user. The value ranges between 0 (the lowest volume) and 255 (the highest
+ * volume). If the user calls `startAudioMixing`, the value of volume is the volume after audio
+ * mixing.
+ */
+ unsigned int volume; // [0,255]
+ /**
+ * Voice activity status of the local user.
+ * - 0: The local user is not speaking.
+ * - 1: The local user is speaking.
+ * @note
+ * - The `vad` parameter does not report the voice activity status of remote users. In a remote
+ * user's callback, the value of `vad` is always 1.
+ * - To use this parameter, you must set `reportVad` to true when calling `enableAudioVolumeIndication`.
+ */
+ unsigned int vad;
+ /**
+ * The voice pitch (Hz) of the local user. The value ranges between 0.0 and 4000.0.
+ * @note The `voicePitch` parameter does not report the voice pitch of remote users. In the
+ * remote users' callback, the value of `voicePitch` is always 0.0.
+ */
+ double voicePitch;
+
+ AudioVolumeInfo() : uid(0), volume(0), vad(0), voicePitch(0.0) {}
+};
+
+/**
+ * The audio device information.
+ */
+struct DeviceInfo {
+ /*
+ * Whether the audio device supports ultra-low-latency capture and playback:
+ * - `true`: The device supports ultra-low-latency capture and playback.
+ * - `false`: The device does not support ultra-low-latency capture and playback.
+ */
+ bool isLowLatencyAudioSupported;
+
+ DeviceInfo() : isLowLatencyAudioSupported(false) {}
+};
+
+/**
+ * The definition of the IPacketObserver struct.
+ */
+class IPacketObserver {
+ public:
+ virtual ~IPacketObserver() {}
+ /**
+ * The definition of the Packet struct.
+ */
+ struct Packet {
+ /**
+ * The buffer address of the sent or received data.
+ * @note Agora recommends setting `buffer` to a value larger than 2048 bytes. Otherwise, you
+ * may encounter undefined behaviors (such as crashes).
+ */
+ const unsigned char* buffer;
+ /**
+ * The buffer size of the sent or received data.
+ */
+ unsigned int size;
+
+ Packet() : buffer(OPTIONAL_NULLPTR), size(0) {}
+ };
+ /**
+ * Occurs when the SDK is ready to send the audio packet.
+ * @param packet The audio packet to be sent: Packet.
+ * @return Whether to send the audio packet:
+ * - true: Send the packet.
+ * - false: Do not send the packet, in which case the audio packet will be discarded.
+ */
+ virtual bool onSendAudioPacket(Packet& packet) = 0;
+ /**
+ * Occurs when the SDK is ready to send the video packet.
+ * @param packet The video packet to be sent: Packet.
+ * @return Whether to send the video packet:
+ * - true: Send the packet.
+ * - false: Do not send the packet, in which case the audio packet will be discarded.
+ */
+ virtual bool onSendVideoPacket(Packet& packet) = 0;
+ /**
+ * Occurs when the audio packet is received.
+ * @param packet The received audio packet: Packet.
+ * @return Whether to process the audio packet:
+ * - true: Process the packet.
+ * - false: Do not process the packet, in which case the audio packet will be discarded.
+ */
+ virtual bool onReceiveAudioPacket(Packet& packet) = 0;
+ /**
+ * Occurs when the video packet is received.
+ * @param packet The received video packet: Packet.
+ * @return Whether to process the audio packet:
+ * - true: Process the packet.
+ * - false: Do not process the packet, in which case the video packet will be discarded.
+ */
+ virtual bool onReceiveVideoPacket(Packet& packet) = 0;
+};
+
+/**
+ * Audio sample rate types.
+ */
+enum AUDIO_SAMPLE_RATE_TYPE {
+ /**
+ * 32000: 32 KHz.
+ */
+ AUDIO_SAMPLE_RATE_32000 = 32000,
+ /**
+ * 44100: 44.1 KHz.
+ */
+ AUDIO_SAMPLE_RATE_44100 = 44100,
+ /**
+ * 48000: 48 KHz.
+ */
+ AUDIO_SAMPLE_RATE_48000 = 48000,
+};
+/**
+ * The codec type of the output video.
+ */
+enum VIDEO_CODEC_TYPE_FOR_STREAM {
+ /**
+ * 1: H.264.
+ */
+ VIDEO_CODEC_H264_FOR_STREAM = 1,
+ /**
+ * 2: H.265.
+ */
+ VIDEO_CODEC_H265_FOR_STREAM = 2,
+};
+
+/**
+ * Video codec profile types.
+ */
+enum VIDEO_CODEC_PROFILE_TYPE {
+ /**
+ * 66: Baseline video codec profile. Generally used in video calls on mobile phones.
+ */
+ VIDEO_CODEC_PROFILE_BASELINE = 66,
+ /**
+ * 77: Main video codec profile. Generally used in mainstream electronics, such as MP4 players, portable video players, PSP, and iPads.
+ */
+ VIDEO_CODEC_PROFILE_MAIN = 77,
+ /**
+ * 100: High video codec profile. Generally used in high-resolution broadcasts or television.
+ */
+ VIDEO_CODEC_PROFILE_HIGH = 100,
+};
+
+
+/**
+ * Self-defined audio codec profile.
+ */
+enum AUDIO_CODEC_PROFILE_TYPE {
+ /**
+ * 0: LC-AAC.
+ */
+ AUDIO_CODEC_PROFILE_LC_AAC = 0,
+ /**
+ * 1: HE-AAC.
+ */
+ AUDIO_CODEC_PROFILE_HE_AAC = 1,
+ /**
+ * 2: HE-AAC v2.
+ */
+ AUDIO_CODEC_PROFILE_HE_AAC_V2 = 2,
+};
+
+/**
+ * Local audio statistics.
+ */
+struct LocalAudioStats
+{
+ /**
+ * The number of audio channels.
+ */
+ int numChannels;
+ /**
+ * The sampling rate (Hz) of sending the local user's audio stream.
+ */
+ int sentSampleRate;
+ /**
+ * The average bitrate (Kbps) of sending the local user's audio stream.
+ */
+ int sentBitrate;
+ /**
+ * The internal payload codec.
+ */
+ int internalCodec;
+ /**
+ * The packet loss rate (%) from the local client to the Agora server before applying the anti-packet loss strategies.
+ */
+ unsigned short txPacketLossRate;
+ /**
+ * The audio delay of the device, contains record and playout delay
+ */
+ int audioDeviceDelay;
+ /**
+ * The playout delay of the device
+ */
+ int audioPlayoutDelay;
+ /**
+ * The signal delay estimated from audio in-ear monitoring (ms).
+ */
+ int earMonitorDelay;
+ /**
+ * The signal delay estimated during the AEC process from nearin and farin (ms).
+ */
+ int aecEstimatedDelay;
+};
+
+
+/**
+ * States of the Media Push.
+ */
+enum RTMP_STREAM_PUBLISH_STATE {
+ /**
+ * 0: The Media Push has not started or has ended. This state is also triggered after you remove a RTMP or RTMPS stream from the CDN by calling `removePublishStreamUrl`.
+ */
+ RTMP_STREAM_PUBLISH_STATE_IDLE = 0,
+ /**
+ * 1: The SDK is connecting to Agora's streaming server and the CDN server. This state is triggered after you call the `addPublishStreamUrl` method.
+ */
+ RTMP_STREAM_PUBLISH_STATE_CONNECTING = 1,
+ /**
+ * 2: The RTMP or RTMPS streaming publishes. The SDK successfully publishes the RTMP or RTMPS streaming and returns this state.
+ */
+ RTMP_STREAM_PUBLISH_STATE_RUNNING = 2,
+ /**
+ * 3: The RTMP or RTMPS streaming is recovering. When exceptions occur to the CDN, or the streaming is interrupted, the SDK tries to resume RTMP or RTMPS streaming and returns this state.
+ * - If the SDK successfully resumes the streaming, #RTMP_STREAM_PUBLISH_STATE_RUNNING (2) returns.
+ * - If the streaming does not resume within 60 seconds or server errors occur, #RTMP_STREAM_PUBLISH_STATE_FAILURE (4) returns. You can also reconnect to the server by calling the `removePublishStreamUrl` and `addPublishStreamUrl` methods.
+ */
+ RTMP_STREAM_PUBLISH_STATE_RECOVERING = 3,
+ /**
+ * 4: The RTMP or RTMPS streaming fails. See the `errCode` parameter for the detailed error information. You can also call the `addPublishStreamUrl` method to publish the RTMP or RTMPS streaming again.
+ */
+ RTMP_STREAM_PUBLISH_STATE_FAILURE = 4,
+ /**
+ * 5: The SDK is disconnecting to Agora's streaming server and the CDN server. This state is triggered after you call the `removePublishStreamUrl` method.
+ */
+ RTMP_STREAM_PUBLISH_STATE_DISCONNECTING = 5,
+};
+
+/**
+ * Error codes of the RTMP or RTMPS streaming.
+ */
+enum RTMP_STREAM_PUBLISH_REASON {
+ /**
+ * 0: The RTMP or RTMPS streaming publishes successfully.
+ */
+ RTMP_STREAM_PUBLISH_REASON_OK = 0,
+ /**
+ * 1: Invalid argument used. If, for example, you do not call the `setLiveTranscoding` method to configure the LiveTranscoding parameters before calling the addPublishStreamUrl method,
+ * the SDK returns this error. Check whether you set the parameters in the `setLiveTranscoding` method properly.
+ */
+ RTMP_STREAM_PUBLISH_REASON_INVALID_ARGUMENT = 1,
+ /**
+ * 2: The RTMP or RTMPS streaming is encrypted and cannot be published.
+ */
+ RTMP_STREAM_PUBLISH_REASON_ENCRYPTED_STREAM_NOT_ALLOWED = 2,
+ /**
+ * 3: Timeout for the RTMP or RTMPS streaming. Call the `addPublishStreamUrl` method to publish the streaming again.
+ */
+ RTMP_STREAM_PUBLISH_REASON_CONNECTION_TIMEOUT = 3,
+ /**
+ * 4: An error occurs in Agora's streaming server. Call the `addPublishStreamUrl` method to publish the streaming again.
+ */
+ RTMP_STREAM_PUBLISH_REASON_INTERNAL_SERVER_ERROR = 4,
+ /**
+ * 5: An error occurs in the CDN server.
+ */
+ RTMP_STREAM_PUBLISH_REASON_RTMP_SERVER_ERROR = 5,
+ /**
+ * 6: The RTMP or RTMPS streaming publishes too frequently.
+ */
+ RTMP_STREAM_PUBLISH_REASON_TOO_OFTEN = 6,
+ /**
+ * 7: The host publishes more than 10 URLs. Delete the unnecessary URLs before adding new ones.
+ */
+ RTMP_STREAM_PUBLISH_REASON_REACH_LIMIT = 7,
+ /**
+ * 8: The host manipulates other hosts' URLs. Check your app logic.
+ */
+ RTMP_STREAM_PUBLISH_REASON_NOT_AUTHORIZED = 8,
+ /**
+ * 9: Agora's server fails to find the RTMP or RTMPS streaming.
+ */
+ RTMP_STREAM_PUBLISH_REASON_STREAM_NOT_FOUND = 9,
+ /**
+ * 10: The format of the RTMP or RTMPS streaming URL is not supported. Check whether the URL format is correct.
+ */
+ RTMP_STREAM_PUBLISH_REASON_FORMAT_NOT_SUPPORTED = 10,
+ /**
+ * 11: The user role is not host, so the user cannot use the CDN live streaming function. Check your application code logic.
+ */
+ RTMP_STREAM_PUBLISH_REASON_NOT_BROADCASTER = 11, // Note: match to ERR_PUBLISH_STREAM_NOT_BROADCASTER in AgoraBase.h
+ /**
+ * 13: The `updateRtmpTranscoding` or `setLiveTranscoding` method is called to update the transcoding configuration in a scenario where there is streaming without transcoding. Check your application code logic.
+ */
+ RTMP_STREAM_PUBLISH_REASON_TRANSCODING_NO_MIX_STREAM = 13, // Note: match to ERR_PUBLISH_STREAM_TRANSCODING_NO_MIX_STREAM in AgoraBase.h
+ /**
+ * 14: Errors occurred in the host's network.
+ */
+ RTMP_STREAM_PUBLISH_REASON_NET_DOWN = 14, // Note: match to ERR_NET_DOWN in AgoraBase.h
+ /**
+ * 15: Your App ID does not have permission to use the CDN live streaming function.
+ */
+ RTMP_STREAM_PUBLISH_REASON_INVALID_APPID = 15, // Note: match to ERR_PUBLISH_STREAM_APPID_INVALID in AgoraBase.h
+ /** invalid privilege. */
+ RTMP_STREAM_PUBLISH_REASON_INVALID_PRIVILEGE = 16,
+ /**
+ * 100: The streaming has been stopped normally. After you call `removePublishStreamUrl` to stop streaming, the SDK returns this value.
+ */
+ RTMP_STREAM_UNPUBLISH_REASON_OK = 100,
+};
+
+/** Events during the RTMP or RTMPS streaming. */
+enum RTMP_STREAMING_EVENT {
+ /**
+ * 1: An error occurs when you add a background image or a watermark image to the RTMP or RTMPS stream.
+ */
+ RTMP_STREAMING_EVENT_FAILED_LOAD_IMAGE = 1,
+ /**
+ * 2: The streaming URL is already being used for CDN live streaming. If you want to start new streaming, use a new streaming URL.
+ */
+ RTMP_STREAMING_EVENT_URL_ALREADY_IN_USE = 2,
+ /**
+ * 3: The feature is not supported.
+ */
+ RTMP_STREAMING_EVENT_ADVANCED_FEATURE_NOT_SUPPORT = 3,
+ /**
+ * 4: Client request too frequently.
+ */
+ RTMP_STREAMING_EVENT_REQUEST_TOO_OFTEN = 4,
+};
+
+/**
+ * Image properties.
+ */
+typedef struct RtcImage {
+ /**
+ *The HTTP/HTTPS URL address of the image in the live video. The maximum length of this parameter is 1024 bytes.
+ */
+ const char* url;
+ /**
+ * The x coordinate (pixel) of the image on the video frame (taking the upper left corner of the video frame as the origin).
+ */
+ int x;
+ /**
+ * The y coordinate (pixel) of the image on the video frame (taking the upper left corner of the video frame as the origin).
+ */
+ int y;
+ /**
+ * The width (pixel) of the image on the video frame.
+ */
+ int width;
+ /**
+ * The height (pixel) of the image on the video frame.
+ */
+ int height;
+ /**
+ * The layer index of the watermark or background image. When you use the watermark array to add
+ * a watermark or multiple watermarks, you must pass a value to `zOrder` in the range [1,255];
+ * otherwise, the SDK reports an error. In other cases, zOrder can optionally be passed in the
+ * range [0,255], with 0 being the default value. 0 means the bottom layer and 255 means the top
+ * layer.
+ */
+ int zOrder;
+ /** The transparency level of the image. The value ranges between 0.0 and 1.0:
+ *
+ * - 0.0: Completely transparent.
+ * - 1.0: (Default) Opaque.
+ */
+ double alpha;
+
+ RtcImage() : url(OPTIONAL_NULLPTR), x(0), y(0), width(0), height(0), zOrder(0), alpha(1.0) {}
+} RtcImage;
+/**
+ * The configuration for advanced features of the RTMP or RTMPS streaming with transcoding.
+ *
+ * If you want to enable the advanced features of streaming with transcoding, contact support@agora.io.
+ */
+struct LiveStreamAdvancedFeature {
+ LiveStreamAdvancedFeature() : featureName(OPTIONAL_NULLPTR), opened(false) {}
+ LiveStreamAdvancedFeature(const char* feat_name, bool open) : featureName(feat_name), opened(open) {}
+ /** The advanced feature for high-quality video with a lower bitrate. */
+ // static const char* LBHQ = "lbhq";
+ /** The advanced feature for the optimized video encoder. */
+ // static const char* VEO = "veo";
+
+ /**
+ * The feature names, including LBHQ (high-quality video with a lower bitrate) and VEO (optimized video encoder).
+ */
+ const char* featureName;
+
+ /**
+ * Whether to enable the advanced features of streaming with transcoding:
+ * - `true`: Enable the advanced feature.
+ * - `false`: (Default) Disable the advanced feature.
+ */
+ bool opened;
+} ;
+
+/**
+ * Connection state types.
+ */
+enum CONNECTION_STATE_TYPE
+{
+ /**
+ * 1: The SDK is disconnected from the Agora edge server. The state indicates the SDK is in one of the following phases:
+ * - The initial state before calling the `joinChannel` method.
+ * - The app calls the `leaveChannel` method.
+ */
+ CONNECTION_STATE_DISCONNECTED = 1,
+ /**
+ * 2: The SDK is connecting to the Agora edge server. This state indicates that the SDK is
+ * establishing a connection with the specified channel after the app calls `joinChannel`.
+ * - If the SDK successfully joins the channel, it triggers the `onConnectionStateChanged`
+ * callback and the connection state switches to `CONNECTION_STATE_CONNECTED`.
+ * - After the connection is established, the SDK also initializes the media and triggers
+ * `onJoinChannelSuccess` when everything is ready.
+ */
+ CONNECTION_STATE_CONNECTING = 2,
+ /**
+ * 3: The SDK is connected to the Agora edge server. This state also indicates that the user
+ * has joined a channel and can now publish or subscribe to a media stream in the channel.
+ * If the connection to the Agora edge server is lost because, for example, the network is down
+ * or switched, the SDK automatically tries to reconnect and triggers `onConnectionStateChanged`
+ * that indicates the connection state switches to `CONNECTION_STATE_RECONNECTING`.
+ */
+ CONNECTION_STATE_CONNECTED = 3,
+ /**
+ * 4: The SDK keeps reconnecting to the Agora edge server. The SDK keeps rejoining the channel
+ * after being disconnected from a joined channel because of network issues.
+ * - If the SDK cannot rejoin the channel within 10 seconds, it triggers `onConnectionLost`,
+ * stays in the `CONNECTION_STATE_RECONNECTING` state, and keeps rejoining the channel.
+ * - If the SDK fails to rejoin the channel 20 minutes after being disconnected from the Agora
+ * edge server, the SDK triggers the `onConnectionStateChanged` callback, switches to the
+ * `CONNECTION_STATE_FAILED` state, and stops rejoining the channel.
+ */
+ CONNECTION_STATE_RECONNECTING = 4,
+ /**
+ * 5: The SDK fails to connect to the Agora edge server or join the channel. This state indicates
+ * that the SDK stops trying to rejoin the channel. You must call `leaveChannel` to leave the
+ * channel.
+ * - You can call `joinChannel` to rejoin the channel.
+ * - If the SDK is banned from joining the channel by the Agora edge server through the RESTful
+ * API, the SDK triggers the `onConnectionStateChanged` callback.
+ */
+ CONNECTION_STATE_FAILED = 5,
+};
+
+/**
+ * Transcoding configurations of each host.
+ */
+struct TranscodingUser {
+ /**
+ * The user ID of the host.
+ */
+ uid_t uid;
+ /**
+ * The x coordinate (pixel) of the host's video on the output video frame (taking the upper left corner of the video frame as the origin). The value range is [0, width], where width is the `width` set in `LiveTranscoding`.
+ */
+ int x;
+ /**
+ * The y coordinate (pixel) of the host's video on the output video frame (taking the upper left corner of the video frame as the origin). The value range is [0, height], where height is the `height` set in `LiveTranscoding`.
+ */
+ int y;
+ /**
+ * The width (pixel) of the host's video.
+ */
+ int width;
+ /**
+ * The height (pixel) of the host's video.
+ */
+ int height;
+ /**
+ * The layer index number of the host's video. The value range is [0, 100].
+ * - 0: (Default) The host's video is the bottom layer.
+ * - 100: The host's video is the top layer.
+ *
+ * If the value is beyond this range, the SDK reports the error code `ERR_INVALID_ARGUMENT`.
+ */
+ int zOrder;
+ /**
+ * The transparency of the host's video. The value range is [0.0, 1.0].
+ * - 0.0: Completely transparent.
+ * - 1.0: (Default) Opaque.
+ */
+ double alpha;
+ /**
+ * The audio channel used by the host's audio in the output audio. The default value is 0, and the value range is [0, 5].
+ * - `0`: (Recommended) The defaut setting, which supports dual channels at most and depends on the upstream of the host.
+ * - `1`: The host's audio uses the FL audio channel. If the host's upstream uses multiple audio channels, the Agora server mixes them into mono first.
+ * - `2`: The host's audio uses the FC audio channel. If the host's upstream uses multiple audio channels, the Agora server mixes them into mono first.
+ * - `3`: The host's audio uses the FR audio channel. If the host's upstream uses multiple audio channels, the Agora server mixes them into mono first.
+ * - `4`: The host's audio uses the BL audio channel. If the host's upstream uses multiple audio channels, the Agora server mixes them into mono first.
+ * - `5`: The host's audio uses the BR audio channel. If the host's upstream uses multiple audio channels, the Agora server mixes them into mono first.
+ * - `0xFF` or a value greater than 5: The host's audio is muted, and the Agora server removes the host's audio.
+ *
+ * @note If the value is not `0`, a special player is required.
+ */
+ int audioChannel;
+
+ TranscodingUser()
+ : uid(0),
+ x(0),
+ y(0),
+ width(0),
+ height(0),
+ zOrder(0),
+ alpha(1.0),
+ audioChannel(0) {}
+};
+
+/**
+ * Transcoding configurations for Media Push.
+ */
+struct LiveTranscoding {
+ /** The width of the video in pixels. The default value is 360.
+ * - When pushing video streams to the CDN, the value range of `width` is [64,1920].
+ * If the value is less than 64, Agora server automatically adjusts it to 64; if the
+ * value is greater than 1920, Agora server automatically adjusts it to 1920.
+ * - When pushing audio streams to the CDN, set `width` and `height` as 0.
+ */
+ int width;
+ /** The height of the video in pixels. The default value is 640.
+ * - When pushing video streams to the CDN, the value range of `height` is [64,1080].
+ * If the value is less than 64, Agora server automatically adjusts it to 64; if the
+ * value is greater than 1080, Agora server automatically adjusts it to 1080.
+ * - When pushing audio streams to the CDN, set `width` and `height` as 0.
+ */
+ int height;
+ /** Bitrate of the CDN live output video stream. The default value is 400 Kbps.
+
+ Set this parameter according to the Video Bitrate Table. If you set a bitrate beyond the proper range, the SDK automatically adapts it to a value within the range.
+ */
+ int videoBitrate;
+ /** Frame rate of the output video stream set for the CDN live streaming. The default value is 15 fps, and the value range is (0,30].
+
+ @note The Agora server adjusts any value over 30 to 30.
+ */
+ int videoFramerate;
+
+ /** **DEPRECATED** Latency mode:
+
+ - true: Low latency with unassured quality.
+ - false: (Default) High latency with assured quality.
+ */
+ bool lowLatency;
+
+ /** Video GOP in frames. The default value is 30 fps.
+ */
+ int videoGop;
+ /** Self-defined video codec profile: #VIDEO_CODEC_PROFILE_TYPE.
+
+ @note If you set this parameter to other values, Agora adjusts it to the default value of 100.
+ */
+ VIDEO_CODEC_PROFILE_TYPE videoCodecProfile;
+ /** The background color in RGB hex value. Value only. Do not include a preceeding #. For example, 0xFFB6C1 (light pink). The default value is 0x000000 (black).
+ */
+ unsigned int backgroundColor;
+ /** Video codec profile types for Media Push. See VIDEO_CODEC_TYPE_FOR_STREAM. */
+ VIDEO_CODEC_TYPE_FOR_STREAM videoCodecType;
+ /** The number of users in the live interactive streaming.
+ * The value range is [0, 17].
+ */
+ unsigned int userCount;
+ /** Manages the user layout configuration in the Media Push. Agora supports a maximum of 17 transcoding users in a Media Push channel. See `TranscodingUser`.
+ */
+ TranscodingUser* transcodingUsers;
+ /** Reserved property. Extra user-defined information to send SEI for the H.264/H.265 video stream to the CDN live client. Maximum length: 4096 Bytes.
+
+ For more information on SEI frame, see [SEI-related questions](https://docs.agora.io/en/faq/sei).
+ */
+ const char* transcodingExtraInfo;
+
+ /** **DEPRECATED** The metadata sent to the CDN live client.
+ */
+ const char* metadata;
+ /** The watermark on the live video. The image format needs to be PNG. See `RtcImage`.
+
+ You can add one watermark, or add multiple watermarks using an array. This parameter is used with `watermarkCount`.
+ */
+ RtcImage* watermark;
+ /**
+ * The number of watermarks on the live video. The total number of watermarks and background images can range from 0 to 10. This parameter is used with `watermark`.
+ */
+ unsigned int watermarkCount;
+
+ /** The number of background images on the live video. The image format needs to be PNG. See `RtcImage`.
+ *
+ * You can add a background image or use an array to add multiple background images. This parameter is used with `backgroundImageCount`.
+ */
+ RtcImage* backgroundImage;
+ /**
+ * The number of background images on the live video. The total number of watermarks and background images can range from 0 to 10. This parameter is used with `backgroundImage`.
+ */
+ unsigned int backgroundImageCount;
+
+ /** The audio sampling rate (Hz) of the output media stream. See #AUDIO_SAMPLE_RATE_TYPE.
+ */
+ AUDIO_SAMPLE_RATE_TYPE audioSampleRate;
+ /** Bitrate (Kbps) of the audio output stream for Media Push. The default value is 48, and the highest value is 128.
+ */
+ int audioBitrate;
+ /** The number of audio channels for Media Push. Agora recommends choosing 1 (mono), or 2 (stereo) audio channels. Special players are required if you choose 3, 4, or 5.
+ * - 1: (Default) Mono.
+ * - 2: Stereo.
+ * - 3: Three audio channels.
+ * - 4: Four audio channels.
+ * - 5: Five audio channels.
+ */
+ int audioChannels;
+ /** Audio codec profile type for Media Push. See #AUDIO_CODEC_PROFILE_TYPE.
+ */
+ AUDIO_CODEC_PROFILE_TYPE audioCodecProfile;
+ /** Advanced features of the RTMP or RTMPS streaming with transcoding. See LiveStreamAdvancedFeature.
+ */
+ LiveStreamAdvancedFeature* advancedFeatures;
+
+ /** The number of enabled advanced features. The default value is 0. */
+ unsigned int advancedFeatureCount;
+
+ LiveTranscoding()
+ : width(360),
+ height(640),
+ videoBitrate(400),
+ videoFramerate(15),
+ lowLatency(false),
+ videoGop(30),
+ videoCodecProfile(VIDEO_CODEC_PROFILE_HIGH),
+ backgroundColor(0x000000),
+ videoCodecType(VIDEO_CODEC_H264_FOR_STREAM),
+ userCount(0),
+ transcodingUsers(OPTIONAL_NULLPTR),
+ transcodingExtraInfo(OPTIONAL_NULLPTR),
+ metadata(OPTIONAL_NULLPTR),
+ watermark(OPTIONAL_NULLPTR),
+ watermarkCount(0),
+ backgroundImage(OPTIONAL_NULLPTR),
+ backgroundImageCount(0),
+ audioSampleRate(AUDIO_SAMPLE_RATE_48000),
+ audioBitrate(48),
+ audioChannels(1),
+ audioCodecProfile(AUDIO_CODEC_PROFILE_LC_AAC),
+ advancedFeatures(OPTIONAL_NULLPTR),
+ advancedFeatureCount(0) {}
+};
+
+/**
+ * The video streams for the video mixing on the local client.
+ */
+struct TranscodingVideoStream {
+ /**
+ * The source type of video for the video mixing on the local client. See #VIDEO_SOURCE_TYPE.
+ */
+ VIDEO_SOURCE_TYPE sourceType;
+ /**
+ * The ID of the remote user.
+ * @note Use this parameter only when the source type of the video for the video mixing on the local client is `VIDEO_SOURCE_REMOTE`.
+ */
+ uid_t remoteUserUid;
+ /**
+ * The URL of the image.
+ * @note Use this parameter only when the source type of the video for the video mixing on the local client is `RTC_IMAGE`.
+ */
+ const char* imageUrl;
+ /**
+ * MediaPlayer id if sourceType is MEDIA_PLAYER_SOURCE.
+ */
+ int mediaPlayerId;
+ /**
+ * The horizontal displacement of the top-left corner of the video for the video mixing on the client relative to the top-left corner (origin) of the canvas for this video mixing.
+ */
+ int x;
+ /**
+ * The vertical displacement of the top-left corner of the video for the video mixing on the client relative to the top-left corner (origin) of the canvas for this video mixing.
+ */
+ int y;
+ /**
+ * The width (px) of the video for the video mixing on the local client.
+ */
+ int width;
+ /**
+ * The height (px) of the video for the video mixing on the local client.
+ */
+ int height;
+ /**
+ * The number of the layer to which the video for the video mixing on the local client belongs. The value range is [0,100].
+ * - 0: (Default) The layer is at the bottom.
+ * - 100: The layer is at the top.
+ */
+ int zOrder;
+ /**
+ * The transparency of the video for the video mixing on the local client. The value range is [0.0,1.0]. 0.0 means the transparency is completely transparent. 1.0 means the transparency is opaque.
+ */
+ double alpha;
+ /**
+ * Whether to mirror the video for the video mixing on the local client.
+ * - true: Mirroring.
+ * - false: (Default) Do not mirror.
+ * @note The paramter only works for videos with the source type `CAMERA`.
+ */
+ bool mirror;
+
+ TranscodingVideoStream()
+ : sourceType(VIDEO_SOURCE_CAMERA_PRIMARY),
+ remoteUserUid(0),
+ imageUrl(OPTIONAL_NULLPTR),
+ x(0),
+ y(0),
+ width(0),
+ height(0),
+ zOrder(0),
+ alpha(1.0),
+ mirror(false) {}
+};
+
+/**
+ * The configuration of the video mixing on the local client.
+ */
+struct LocalTranscoderConfiguration {
+ /**
+ * The number of the video streams for the video mixing on the local client.
+ */
+ unsigned int streamCount;
+ /**
+ * The video streams for the video mixing on the local client. See TranscodingVideoStream.
+ */
+ TranscodingVideoStream* videoInputStreams;
+ /**
+ * The encoding configuration of the mixed video stream after the video mixing on the local client. See VideoEncoderConfiguration.
+ */
+ VideoEncoderConfiguration videoOutputConfiguration;
+ /**
+ * Whether to use the timestamp when the primary camera captures the video frame as the timestamp of the mixed video frame.
+ * - true: (Default) Use the timestamp of the captured video frame as the timestamp of the mixed video frame.
+ * - false: Do not use the timestamp of the captured video frame as the timestamp of the mixed video frame. Instead, use the timestamp when the mixed video frame is constructed.
+ */
+ bool syncWithPrimaryCamera;
+
+ LocalTranscoderConfiguration() : streamCount(0), videoInputStreams(OPTIONAL_NULLPTR), videoOutputConfiguration(), syncWithPrimaryCamera(true) {}
+};
+
+enum VIDEO_TRANSCODER_ERROR {
+ /**
+ * The video track of the video source is not started.
+ */
+ VT_ERR_VIDEO_SOURCE_NOT_READY = 1,
+ /**
+ * The video source type is not supported.
+ */
+ VT_ERR_INVALID_VIDEO_SOURCE_TYPE = 2,
+ /**
+ * The image url is not correctly of image source.
+ */
+ VT_ERR_INVALID_IMAGE_PATH = 3,
+ /**
+ * The image format not the type png/jpeg/gif of image source.
+ */
+ VT_ERR_UNSUPPORT_IMAGE_FORMAT = 4,
+ /**
+ * The layout is invalid such as width is zero.
+ */
+ VT_ERR_INVALID_LAYOUT = 5,
+ /**
+ * Internal error.
+ */
+ VT_ERR_INTERNAL = 20
+};
+
+/**
+ * Configurations of the last-mile network test.
+ */
+struct LastmileProbeConfig {
+ /**
+ * Determines whether to test the uplink network. Some users, for example,
+ * the audience in a live broadcast channel, do not need such a test:
+ * - true: Test.
+ * - false: Do not test.
+ */
+ bool probeUplink;
+ /**
+ * Determines whether to test the downlink network:
+ * - true: Test.
+ * - false: Do not test.
+ */
+ bool probeDownlink;
+ /**
+ * The expected maximum sending bitrate (bps) of the local user. The value range is [100000, 5000000]. We recommend setting this parameter
+ * according to the bitrate value set by `setVideoEncoderConfiguration`.
+ */
+ unsigned int expectedUplinkBitrate;
+ /**
+ * The expected maximum receiving bitrate (bps) of the local user. The value range is [100000,5000000].
+ */
+ unsigned int expectedDownlinkBitrate;
+};
+
+/**
+ * The status of the last-mile network tests.
+ */
+enum LASTMILE_PROBE_RESULT_STATE {
+ /**
+ * 1: The last-mile network probe test is complete.
+ */
+ LASTMILE_PROBE_RESULT_COMPLETE = 1,
+ /**
+ * 2: The last-mile network probe test is incomplete because the bandwidth estimation is not available due to limited test resources.
+ */
+ LASTMILE_PROBE_RESULT_INCOMPLETE_NO_BWE = 2,
+ /**
+ * 3: The last-mile network probe test is not carried out, probably due to poor network conditions.
+ */
+ LASTMILE_PROBE_RESULT_UNAVAILABLE = 3
+};
+
+/**
+ * Results of the uplink or downlink last-mile network test.
+ */
+struct LastmileProbeOneWayResult {
+ /**
+ * The packet loss rate (%).
+ */
+ unsigned int packetLossRate;
+ /**
+ * The network jitter (ms).
+ */
+ unsigned int jitter;
+ /**
+ * The estimated available bandwidth (bps).
+ */
+ unsigned int availableBandwidth;
+
+ LastmileProbeOneWayResult() : packetLossRate(0),
+ jitter(0),
+ availableBandwidth(0) {}
+};
+
+/**
+ * Results of the uplink and downlink last-mile network tests.
+ */
+struct LastmileProbeResult {
+ /**
+ * The status of the last-mile network tests. See #LASTMILE_PROBE_RESULT_STATE.
+ */
+ LASTMILE_PROBE_RESULT_STATE state;
+ /**
+ * Results of the uplink last-mile network test. For details, see LastmileProbeOneWayResult.
+ */
+ LastmileProbeOneWayResult uplinkReport;
+ /**
+ * Results of the downlink last-mile network test. For details, see LastmileProbeOneWayResult.
+ */
+ LastmileProbeOneWayResult downlinkReport;
+ /**
+ * The round-trip time (ms).
+ */
+ unsigned int rtt;
+
+ LastmileProbeResult()
+ : state(LASTMILE_PROBE_RESULT_UNAVAILABLE),
+ rtt(0) {}
+};
+
+/**
+ * Reasons causing the change of the connection state.
+ */
+enum CONNECTION_CHANGED_REASON_TYPE
+{
+ /**
+ * 0: The SDK is connecting to the server.
+ */
+ CONNECTION_CHANGED_CONNECTING = 0,
+ /**
+ * 1: The SDK has joined the channel successfully.
+ */
+ CONNECTION_CHANGED_JOIN_SUCCESS = 1,
+ /**
+ * 2: The connection between the SDK and the server is interrupted.
+ */
+ CONNECTION_CHANGED_INTERRUPTED = 2,
+ /**
+ * 3: The connection between the SDK and the server is banned by the server. This error occurs when the user is kicked out of the channel by the server.
+ */
+ CONNECTION_CHANGED_BANNED_BY_SERVER = 3,
+ /**
+ * 4: The SDK fails to join the channel. When the SDK fails to join the channel for more than 20 minutes, this error occurs and the SDK stops reconnecting to the channel.
+ */
+ CONNECTION_CHANGED_JOIN_FAILED = 4,
+ /**
+ * 5: The SDK has left the channel.
+ */
+ CONNECTION_CHANGED_LEAVE_CHANNEL = 5,
+ /**
+ * 6: The connection fails because the App ID is not valid.
+ */
+ CONNECTION_CHANGED_INVALID_APP_ID = 6,
+ /**
+ * 7: The connection fails because the channel name is not valid. Please rejoin the channel with a valid channel name.
+ */
+ CONNECTION_CHANGED_INVALID_CHANNEL_NAME = 7,
+ /**
+ * 8: The connection fails because the token is not valid. Typical reasons include:
+ * - The App Certificate for the project is enabled in Agora Console, but you do not use a token when joining the channel. If you enable the App Certificate, you must use a token to join the channel.
+ * - The `uid` specified when calling `joinChannel` to join the channel is inconsistent with the `uid` passed in when generating the token.
+ */
+ CONNECTION_CHANGED_INVALID_TOKEN = 8,
+ /**
+ * 9: The connection fails because the token has expired.
+ */
+ CONNECTION_CHANGED_TOKEN_EXPIRED = 9,
+ /**
+ * 10: The connection is rejected by the server. Typical reasons include:
+ * - The user is already in the channel and still calls a method, for example, `joinChannel`, to join the channel. Stop calling this method to clear this error.
+ * - The user tries to join the channel when conducting a pre-call test. The user needs to call the channel after the call test ends.
+ */
+ CONNECTION_CHANGED_REJECTED_BY_SERVER = 10,
+ /**
+ * 11: The connection changes to reconnecting because the SDK has set a proxy server.
+ */
+ CONNECTION_CHANGED_SETTING_PROXY_SERVER = 11,
+ /**
+ * 12: The connection state changed because the token is renewed.
+ */
+ CONNECTION_CHANGED_RENEW_TOKEN = 12,
+ /**
+ * 13: The IP address of the client has changed, possibly because the network type, IP address, or port has been changed.
+ */
+ CONNECTION_CHANGED_CLIENT_IP_ADDRESS_CHANGED = 13,
+ /**
+ * 14: Timeout for the keep-alive of the connection between the SDK and the Agora edge server. The connection state changes to CONNECTION_STATE_RECONNECTING.
+ */
+ CONNECTION_CHANGED_KEEP_ALIVE_TIMEOUT = 14,
+ /**
+ * 15: The SDK has rejoined the channel successfully.
+ */
+ CONNECTION_CHANGED_REJOIN_SUCCESS = 15,
+ /**
+ * 16: The connection between the SDK and the server is lost.
+ */
+ CONNECTION_CHANGED_LOST = 16,
+ /**
+ * 17: The change of connection state is caused by echo test.
+ */
+ CONNECTION_CHANGED_ECHO_TEST = 17,
+ /**
+ * 18: The local IP Address is changed by user.
+ */
+ CONNECTION_CHANGED_CLIENT_IP_ADDRESS_CHANGED_BY_USER = 18,
+ /**
+ * 19: The connection is failed due to join the same channel on another device with the same uid.
+ */
+ CONNECTION_CHANGED_SAME_UID_LOGIN = 19,
+ /**
+ * 20: The connection is failed due to too many broadcasters in the channel.
+ */
+ CONNECTION_CHANGED_TOO_MANY_BROADCASTERS = 20,
+
+ /**
+ * 21: The connection is failed due to license validation failure.
+ */
+ CONNECTION_CHANGED_LICENSE_VALIDATION_FAILURE = 21,
+ /*
+ * 22: The connection is failed due to certification verify failure.
+ */
+ CONNECTION_CHANGED_CERTIFICATION_VERYFY_FAILURE = 22,
+ /**
+ * 23: The connection is failed due to the lack of granting permission to the stream channel.
+ */
+ CONNECTION_CHANGED_STREAM_CHANNEL_NOT_AVAILABLE = 23,
+ /**
+ * 24: The connection is failed due to join channel with an inconsistent appid.
+ */
+ CONNECTION_CHANGED_INCONSISTENT_APPID = 24,
+};
+
+/**
+ * The reason of changing role's failure.
+ */
+enum CLIENT_ROLE_CHANGE_FAILED_REASON {
+ /**
+ * 1: Too many broadcasters in the channel.
+ */
+ CLIENT_ROLE_CHANGE_FAILED_TOO_MANY_BROADCASTERS = 1,
+ /**
+ * 2: The operation of changing role is not authorized.
+ */
+ CLIENT_ROLE_CHANGE_FAILED_NOT_AUTHORIZED = 2,
+ /**
+ * 3: The operation of changing role is timeout.
+ */
+ CLIENT_ROLE_CHANGE_FAILED_REQUEST_TIME_OUT = 3,
+ /**
+ * 4: The operation of changing role is interrupted since we lost connection with agora service.
+ */
+ CLIENT_ROLE_CHANGE_FAILED_CONNECTION_FAILED = 4,
+};
+
+/**
+ * The reason of notifying the user of a message.
+ */
+enum WLACC_MESSAGE_REASON {
+ /**
+ * WIFI signal is weak.
+ */
+ WLACC_MESSAGE_REASON_WEAK_SIGNAL = 0,
+ /**
+ * Channel congestion.
+ */
+ WLACC_MESSAGE_REASON_CHANNEL_CONGESTION = 1,
+};
+
+/**
+ * Suggest an action for the user.
+ */
+enum WLACC_SUGGEST_ACTION {
+ /**
+ * Please get close to AP.
+ */
+ WLACC_SUGGEST_ACTION_CLOSE_TO_WIFI = 0,
+ /**
+ * The user is advised to connect to the prompted SSID.
+ */
+ WLACC_SUGGEST_ACTION_CONNECT_SSID = 1,
+ /**
+ * The user is advised to check whether the AP supports 5G band and enable 5G band (the aciton link is attached), or purchases an AP that supports 5G. AP does not support 5G band.
+ */
+ WLACC_SUGGEST_ACTION_CHECK_5G = 2,
+ /**
+ * The user is advised to change the SSID of the 2.4G or 5G band (the aciton link is attached). The SSID of the 2.4G band AP is the same as that of the 5G band.
+ */
+ WLACC_SUGGEST_ACTION_MODIFY_SSID = 3,
+};
+
+/**
+ * Indicator optimization degree.
+ */
+struct WlAccStats {
+ /**
+ * End-to-end delay optimization percentage.
+ */
+ unsigned short e2eDelayPercent;
+ /**
+ * Frozen Ratio optimization percentage.
+ */
+ unsigned short frozenRatioPercent;
+ /**
+ * Loss Rate optimization percentage.
+ */
+ unsigned short lossRatePercent;
+};
+
+/**
+ * The network type.
+ */
+enum NETWORK_TYPE {
+ /**
+ * -1: The network type is unknown.
+ */
+ NETWORK_TYPE_UNKNOWN = -1,
+ /**
+ * 0: The SDK disconnects from the network.
+ */
+ NETWORK_TYPE_DISCONNECTED = 0,
+ /**
+ * 1: The network type is LAN.
+ */
+ NETWORK_TYPE_LAN = 1,
+ /**
+ * 2: The network type is Wi-Fi (including hotspots).
+ */
+ NETWORK_TYPE_WIFI = 2,
+ /**
+ * 3: The network type is mobile 2G.
+ */
+ NETWORK_TYPE_MOBILE_2G = 3,
+ /**
+ * 4: The network type is mobile 3G.
+ */
+ NETWORK_TYPE_MOBILE_3G = 4,
+ /**
+ * 5: The network type is mobile 4G.
+ */
+ NETWORK_TYPE_MOBILE_4G = 5,
+ /**
+ * 6: The network type is mobile 5G.
+ */
+ NETWORK_TYPE_MOBILE_5G = 6,
+};
+
+/**
+ * The mode of setting up video views.
+ */
+enum VIDEO_VIEW_SETUP_MODE {
+ /**
+ * 0: replace one view
+ */
+ VIDEO_VIEW_SETUP_REPLACE = 0,
+ /**
+ * 1: add one view
+ */
+ VIDEO_VIEW_SETUP_ADD = 1,
+ /**
+ * 2: remove one view
+ */
+ VIDEO_VIEW_SETUP_REMOVE = 2,
+};
+
+/**
+ * Attributes of video canvas object.
+ */
+struct VideoCanvas {
+ /**
+ * The user id of local video.
+ */
+ uid_t uid;
+
+ /**
+ * The uid of video stream composing the video stream from transcoder which will be drawn on this video canvas.
+ */
+ uid_t subviewUid;
+ /**
+ * Video display window.
+ */
+ view_t view;
+ /**
+ * A RGBA value indicates background color of the render view. Defaults to 0x00000000.
+ */
+ uint32_t backgroundColor;
+ /**
+ * The video render mode. See \ref agora::media::base::RENDER_MODE_TYPE "RENDER_MODE_TYPE".
+ * The default value is RENDER_MODE_HIDDEN.
+ */
+ media::base::RENDER_MODE_TYPE renderMode;
+ /**
+ * The video mirror mode. See \ref VIDEO_MIRROR_MODE_TYPE "VIDEO_MIRROR_MODE_TYPE".
+ * The default value is VIDEO_MIRROR_MODE_AUTO.
+ * @note
+ * - For the mirror mode of the local video view:
+ * If you use a front camera, the SDK enables the mirror mode by default;
+ * if you use a rear camera, the SDK disables the mirror mode by default.
+ * - For the remote user: The mirror mode is disabled by default.
+ */
+ VIDEO_MIRROR_MODE_TYPE mirrorMode;
+ /**
+ * The mode of setting up video view. See \ref VIDEO_VIEW_SETUP_MODE "VIDEO_VIEW_SETUP_MODE"
+ * The default value is VIDEO_VIEW_SETUP_REPLACE.
+ */
+ VIDEO_VIEW_SETUP_MODE setupMode;
+ /**
+ * The video source type. See \ref VIDEO_SOURCE_TYPE "VIDEO_SOURCE_TYPE".
+ * The default value is VIDEO_SOURCE_CAMERA_PRIMARY.
+ */
+ VIDEO_SOURCE_TYPE sourceType;
+ /**
+ * The media player id of AgoraMediaPlayer. It should set this parameter when the
+ * sourceType is VIDEO_SOURCE_MEDIA_PLAYER to show the video that AgoraMediaPlayer is playing.
+ * You can get this value by calling the method \ref getMediaPlayerId().
+ */
+ int mediaPlayerId;
+ /**
+ * If you want to display a certain part of a video frame, you can set
+ * this value to crop the video frame to show.
+ * The default value is empty(that is, if it has zero width or height), which means no cropping.
+ */
+ Rectangle cropArea;
+ /**
+ * Whether to apply alpha mask to the video frame if exsit:
+ * true: Apply alpha mask to video frame.
+ * false: (Default) Do not apply alpha mask to video frame.
+ */
+ bool enableAlphaMask;
+ /**
+ * The video frame position in pipeline. See \ref VIDEO_MODULE_POSITION "VIDEO_MODULE_POSITION".
+ * The default value is POSITION_POST_CAPTURER.
+ */
+ media::base::VIDEO_MODULE_POSITION position;
+
+ VideoCanvas()
+ : uid(0), subviewUid(0), view(NULL), backgroundColor(0x00000000), renderMode(media::base::RENDER_MODE_HIDDEN), mirrorMode(VIDEO_MIRROR_MODE_AUTO),
+ setupMode(VIDEO_VIEW_SETUP_REPLACE), sourceType(VIDEO_SOURCE_CAMERA_PRIMARY), mediaPlayerId(-ERR_NOT_READY),
+ cropArea(0, 0, 0, 0), enableAlphaMask(false), position(media::base::POSITION_POST_CAPTURER) {}
+
+ VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt)
+ : uid(0), subviewUid(0), view(v), backgroundColor(0x00000000), renderMode(m), mirrorMode(mt), setupMode(VIDEO_VIEW_SETUP_REPLACE),
+ sourceType(VIDEO_SOURCE_CAMERA_PRIMARY), mediaPlayerId(-ERR_NOT_READY),
+ cropArea(0, 0, 0, 0), enableAlphaMask(false), position(media::base::POSITION_POST_CAPTURER) {}
+
+ VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt, uid_t u)
+ : uid(u), subviewUid(0), view(v), backgroundColor(0x00000000), renderMode(m), mirrorMode(mt), setupMode(VIDEO_VIEW_SETUP_REPLACE),
+ sourceType(VIDEO_SOURCE_CAMERA_PRIMARY), mediaPlayerId(-ERR_NOT_READY),
+ cropArea(0, 0, 0, 0), enableAlphaMask(false), position(media::base::POSITION_POST_CAPTURER) {}
+
+ VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt, uid_t u, uid_t subu)
+ : uid(u), subviewUid(subu), view(v), backgroundColor(0x00000000), renderMode(m), mirrorMode(mt), setupMode(VIDEO_VIEW_SETUP_REPLACE),
+ sourceType(VIDEO_SOURCE_CAMERA_PRIMARY), mediaPlayerId(-ERR_NOT_READY),
+ cropArea(0, 0, 0, 0), enableAlphaMask(false), position(media::base::POSITION_POST_CAPTURER) {}
+};
+
+/** Image enhancement options.
+ */
+struct BeautyOptions {
+ /** The contrast level.
+ */
+ enum LIGHTENING_CONTRAST_LEVEL {
+ /** Low contrast level. */
+ LIGHTENING_CONTRAST_LOW = 0,
+ /** (Default) Normal contrast level. */
+ LIGHTENING_CONTRAST_NORMAL = 1,
+ /** High contrast level. */
+ LIGHTENING_CONTRAST_HIGH = 2,
+ };
+
+ /** The contrast level, used with the `lighteningLevel` parameter. The larger the value, the greater the contrast between light and dark. See #LIGHTENING_CONTRAST_LEVEL.
+ */
+ LIGHTENING_CONTRAST_LEVEL lighteningContrastLevel;
+
+ /** The brightness level. The value ranges from 0.0 (original) to 1.0. The default value is 0.0. The greater the value, the greater the degree of whitening. */
+ float lighteningLevel;
+
+ /** The value ranges from 0.0 (original) to 1.0. The default value is 0.0. The greater the value, the greater the degree of skin grinding.
+ */
+ float smoothnessLevel;
+
+ /** The redness level. The value ranges from 0.0 (original) to 1.0. The default value is 0.0. The larger the value, the greater the rosy degree.
+ */
+ float rednessLevel;
+
+ /** The sharpness level. The value ranges from 0.0 (original) to 1.0. The default value is 0.0. The larger the value, the greater the sharpening degree.
+ */
+ float sharpnessLevel;
+
+ BeautyOptions(LIGHTENING_CONTRAST_LEVEL contrastLevel, float lightening, float smoothness, float redness, float sharpness) : lighteningContrastLevel(contrastLevel), lighteningLevel(lightening), smoothnessLevel(smoothness), rednessLevel(redness), sharpnessLevel(sharpness) {}
+
+ BeautyOptions() : lighteningContrastLevel(LIGHTENING_CONTRAST_NORMAL), lighteningLevel(0), smoothnessLevel(0), rednessLevel(0), sharpnessLevel(0) {}
+};
+
+struct LowlightEnhanceOptions {
+ /**
+ * The low-light enhancement mode.
+ */
+ enum LOW_LIGHT_ENHANCE_MODE {
+ /** 0: (Default) Automatic mode. The SDK automatically enables or disables the low-light enhancement feature according to the ambient light to compensate for the lighting level or prevent overexposure, as necessary. */
+ LOW_LIGHT_ENHANCE_AUTO = 0,
+ /** Manual mode. Users need to enable or disable the low-light enhancement feature manually. */
+ LOW_LIGHT_ENHANCE_MANUAL = 1,
+ };
+ /**
+ * The low-light enhancement level.
+ */
+ enum LOW_LIGHT_ENHANCE_LEVEL {
+ /**
+ * 0: (Default) Promotes video quality during low-light enhancement. It processes the brightness, details, and noise of the video image. The performance consumption is moderate, the processing speed is moderate, and the overall video quality is optimal.
+ */
+ LOW_LIGHT_ENHANCE_LEVEL_HIGH_QUALITY = 0,
+ /**
+ * Promotes performance during low-light enhancement. It processes the brightness and details of the video image. The processing speed is faster.
+ */
+ LOW_LIGHT_ENHANCE_LEVEL_FAST = 1,
+ };
+
+ /** The low-light enhancement mode. See #LOW_LIGHT_ENHANCE_MODE.
+ */
+ LOW_LIGHT_ENHANCE_MODE mode;
+
+ /** The low-light enhancement level. See #LOW_LIGHT_ENHANCE_LEVEL.
+ */
+ LOW_LIGHT_ENHANCE_LEVEL level;
+
+ LowlightEnhanceOptions(LOW_LIGHT_ENHANCE_MODE lowlightMode, LOW_LIGHT_ENHANCE_LEVEL lowlightLevel) : mode(lowlightMode), level(lowlightLevel) {}
+
+ LowlightEnhanceOptions() : mode(LOW_LIGHT_ENHANCE_AUTO), level(LOW_LIGHT_ENHANCE_LEVEL_HIGH_QUALITY) {}
+};
+/**
+ * The video noise reduction options.
+ *
+ * @since v4.0.0
+ */
+struct VideoDenoiserOptions {
+ /** The video noise reduction mode.
+ */
+ enum VIDEO_DENOISER_MODE {
+ /** 0: (Default) Automatic mode. The SDK automatically enables or disables the video noise reduction feature according to the ambient light. */
+ VIDEO_DENOISER_AUTO = 0,
+ /** Manual mode. Users need to enable or disable the video noise reduction feature manually. */
+ VIDEO_DENOISER_MANUAL = 1,
+ };
+ /**
+ * The video noise reduction level.
+ */
+ enum VIDEO_DENOISER_LEVEL {
+ /**
+ * 0: (Default) Promotes video quality during video noise reduction. `HIGH_QUALITY` balances performance consumption and video noise reduction quality.
+ * The performance consumption is moderate, the video noise reduction speed is moderate, and the overall video quality is optimal.
+ */
+ VIDEO_DENOISER_LEVEL_HIGH_QUALITY = 0,
+ /**
+ * Promotes reducing performance consumption during video noise reduction. `FAST` prioritizes reducing performance consumption over video noise reduction quality.
+ * The performance consumption is lower, and the video noise reduction speed is faster. To avoid a noticeable shadowing effect (shadows trailing behind moving objects) in the processed video, Agora recommends that you use `FAST` when the camera is fixed.
+ */
+ VIDEO_DENOISER_LEVEL_FAST = 1,
+ /**
+ * Enhanced video noise reduction. `STRENGTH` prioritizes video noise reduction quality over reducing performance consumption.
+ * The performance consumption is higher, the video noise reduction speed is slower, and the video noise reduction quality is better.
+ * If `HIGH_QUALITY` is not enough for your video noise reduction needs, you can use `STRENGTH`.
+ */
+ VIDEO_DENOISER_LEVEL_STRENGTH = 2,
+ };
+ /** The video noise reduction mode. See #VIDEO_DENOISER_MODE.
+ */
+ VIDEO_DENOISER_MODE mode;
+
+ /** The video noise reduction level. See #VIDEO_DENOISER_LEVEL.
+ */
+ VIDEO_DENOISER_LEVEL level;
+
+ VideoDenoiserOptions(VIDEO_DENOISER_MODE denoiserMode, VIDEO_DENOISER_LEVEL denoiserLevel) : mode(denoiserMode), level(denoiserLevel) {}
+
+ VideoDenoiserOptions() : mode(VIDEO_DENOISER_AUTO), level(VIDEO_DENOISER_LEVEL_HIGH_QUALITY) {}
+};
+
+/** The color enhancement options.
+ *
+ * @since v4.0.0
+ */
+struct ColorEnhanceOptions {
+ /** The level of color enhancement. The value range is [0.0,1.0]. `0.0` is the default value, which means no color enhancement is applied to the video. The higher the value, the higher the level of color enhancement.
+ */
+ float strengthLevel;
+
+ /** The level of skin tone protection. The value range is [0.0,1.0]. `0.0` means no skin tone protection. The higher the value, the higher the level of skin tone protection.
+ * The default value is `1.0`. When the level of color enhancement is higher, the portrait skin tone can be significantly distorted, so you need to set the level of skin tone protection; when the level of skin tone protection is higher, the color enhancement effect can be slightly reduced.
+ * Therefore, to get the best color enhancement effect, Agora recommends that you adjust `strengthLevel` and `skinProtectLevel` to get the most appropriate values.
+ */
+ float skinProtectLevel;
+
+ ColorEnhanceOptions(float stength, float skinProtect) : strengthLevel(stength), skinProtectLevel(skinProtect) {}
+
+ ColorEnhanceOptions() : strengthLevel(0), skinProtectLevel(1) {}
+};
+
+/**
+ * The custom background image.
+ */
+struct VirtualBackgroundSource {
+ /** The type of the custom background source.
+ */
+ enum BACKGROUND_SOURCE_TYPE {
+ /**
+ * 0: Enable segementation with the captured video frame without replacing the background.
+ */
+ BACKGROUND_NONE = 0,
+ /**
+ * 1: (Default) The background source is a solid color.
+ */
+ BACKGROUND_COLOR = 1,
+ /**
+ * The background source is a file in PNG or JPG format.
+ */
+ BACKGROUND_IMG = 2,
+ /**
+ * The background source is the blurred original video frame.
+ * */
+ BACKGROUND_BLUR = 3,
+ /**
+ * The background source is a file in MP4, AVI, MKV, FLV format.
+ * */
+ BACKGROUND_VIDEO = 4,
+ };
+
+ /** The degree of blurring applied to the background source.
+ */
+ enum BACKGROUND_BLUR_DEGREE {
+ /** 1: The degree of blurring applied to the custom background image is low. The user can almost see the background clearly. */
+ BLUR_DEGREE_LOW = 1,
+ /** 2: The degree of blurring applied to the custom background image is medium. It is difficult for the user to recognize details in the background. */
+ BLUR_DEGREE_MEDIUM = 2,
+ /** 3: (Default) The degree of blurring applied to the custom background image is high. The user can barely see any distinguishing features in the background. */
+ BLUR_DEGREE_HIGH = 3,
+ };
+
+ /** The type of the custom background image. See #BACKGROUND_SOURCE_TYPE.
+ */
+ BACKGROUND_SOURCE_TYPE background_source_type;
+
+ /**
+ * The color of the custom background image. The format is a hexadecimal integer defined by RGB, without the # sign,
+ * such as 0xFFB6C1 for light pink. The default value is 0xFFFFFF, which signifies white. The value range
+ * is [0x000000,0xFFFFFF]. If the value is invalid, the SDK replaces the original background image with a white
+ * background image.
+ *
+ * @note This parameter takes effect only when the type of the custom background image is `BACKGROUND_COLOR`.
+ */
+ unsigned int color;
+
+ /**
+ * The local absolute path of the custom background image. PNG and JPG formats are supported. If the path is invalid,
+ * the SDK replaces the original background image with a white background image.
+ *
+ * @note This parameter takes effect only when the type of the custom background image is `BACKGROUND_IMG`.
+ */
+ const char* source;
+
+ /** The degree of blurring applied to the custom background image. See BACKGROUND_BLUR_DEGREE.
+ * @note This parameter takes effect only when the type of the custom background image is `BACKGROUND_BLUR`.
+ */
+ BACKGROUND_BLUR_DEGREE blur_degree;
+
+ VirtualBackgroundSource() : background_source_type(BACKGROUND_COLOR), color(0xffffff), source(OPTIONAL_NULLPTR), blur_degree(BLUR_DEGREE_HIGH) {}
+};
+
+struct SegmentationProperty {
+
+ enum SEG_MODEL_TYPE {
+
+ SEG_MODEL_AI = 1,
+ SEG_MODEL_GREEN = 2
+ };
+
+ SEG_MODEL_TYPE modelType;
+
+ float greenCapacity;
+
+
+ SegmentationProperty() : modelType(SEG_MODEL_AI), greenCapacity(0.5){}
+};
+
+/** The type of custom audio track
+*/
+enum AUDIO_TRACK_TYPE {
+ /**
+ * -1: Invalid audio track
+ */
+ AUDIO_TRACK_INVALID = -1,
+ /**
+ * 0: Mixable audio track
+ * You can push more than one mixable Audio tracks into one RTC connection(channel id + uid),
+ * and SDK will mix these tracks into one audio track automatically.
+ * However, compare to direct audio track, mixable track might cause extra 30ms+ delay.
+ */
+ AUDIO_TRACK_MIXABLE = 0,
+ /**
+ * 1: Direct audio track
+ * You can only push one direct (non-mixable) audio track into one RTC connection(channel id + uid).
+ * Compare to mixable stream, you can have lower lantency using direct audio track.
+ */
+ AUDIO_TRACK_DIRECT = 1,
+};
+
+/** The configuration of custom audio track
+*/
+struct AudioTrackConfig {
+ /**
+ * Enable local playback, enabled by default
+ * true: (Default) Enable local playback
+ * false: Do not enable local playback
+ */
+ bool enableLocalPlayback;
+
+ AudioTrackConfig()
+ : enableLocalPlayback(true) {}
+};
+
+/**
+ * Preset local voice reverberation options.
+ * bitmap allocation:
+ * | bit31 | bit30 - bit24 | bit23 - bit16 | bit15 - bit8 | bit7 - bit0 |
+ * |---------|--------------------|-----------------------------|--------------|----------------|
+ * |reserved | 0x1: voice beauty | 0x1: chat beautification | effect types | effect settings|
+ * | | | 0x2: singing beautification | | |
+ * | | | 0x3: timbre transform | | |
+ * | | | 0x4: ultra high_quality | | |
+ * | |--------------------|-----------------------------| | |
+ * | | 0x2: audio effect | 0x1: space construction | | |
+ * | | | 0x2: voice changer effect | | |
+ * | | | 0x3: style transform | | |
+ * | | | 0x4: electronic sound | | |
+ * | | | 0x5: magic tone | | |
+ * | |--------------------|-----------------------------| | |
+ * | | 0x3: voice changer | 0x1: voice transform | | |
+ */
+/** The options for SDK preset voice beautifier effects.
+ */
+enum VOICE_BEAUTIFIER_PRESET {
+ /** Turn off voice beautifier effects and use the original voice.
+ */
+ VOICE_BEAUTIFIER_OFF = 0x00000000,
+ /** A more magnetic voice.
+ *
+ * @note Agora recommends using this enumerator to process a male-sounding voice; otherwise, you
+ * may experience vocal distortion.
+ */
+ CHAT_BEAUTIFIER_MAGNETIC = 0x01010100,
+ /** A fresher voice.
+ *
+ * @note Agora recommends using this enumerator to process a female-sounding voice; otherwise, you
+ * may experience vocal distortion.
+ */
+ CHAT_BEAUTIFIER_FRESH = 0x01010200,
+ /** A more vital voice.
+ *
+ * @note Agora recommends using this enumerator to process a female-sounding voice; otherwise, you
+ * may experience vocal distortion.
+ */
+ CHAT_BEAUTIFIER_VITALITY = 0x01010300,
+ /**
+ * Singing beautifier effect.
+ * - If you call `setVoiceBeautifierPreset`(SINGING_BEAUTIFIER), you can beautify a male-sounding voice and add a reverberation effect
+ * that sounds like singing in a small room. Agora recommends not using `setVoiceBeautifierPreset`(SINGING_BEAUTIFIER) to process
+ * a female-sounding voice; otherwise, you may experience vocal distortion.
+ * - If you call `setVoiceBeautifierParameters`(SINGING_BEAUTIFIER, param1, param2), you can beautify a male- or
+ * female-sounding voice and add a reverberation effect.
+ */
+ SINGING_BEAUTIFIER = 0x01020100,
+ /** A more vigorous voice.
+ */
+ TIMBRE_TRANSFORMATION_VIGOROUS = 0x01030100,
+ /** A deeper voice.
+ */
+ TIMBRE_TRANSFORMATION_DEEP = 0x01030200,
+ /** A mellower voice.
+ */
+ TIMBRE_TRANSFORMATION_MELLOW = 0x01030300,
+ /** A falsetto voice.
+ */
+ TIMBRE_TRANSFORMATION_FALSETTO = 0x01030400,
+ /** A fuller voice.
+ */
+ TIMBRE_TRANSFORMATION_FULL = 0x01030500,
+ /** A clearer voice.
+ */
+ TIMBRE_TRANSFORMATION_CLEAR = 0x01030600,
+ /** A more resounding voice.
+ */
+ TIMBRE_TRANSFORMATION_RESOUNDING = 0x01030700,
+ /** A more ringing voice.
+ */
+ TIMBRE_TRANSFORMATION_RINGING = 0x01030800,
+ /**
+ * A ultra-high quality voice, which makes the audio clearer and restores more details.
+ * - To achieve better audio effect quality, Agora recommends that you call `setAudioProfile`
+ * and set the `profile` to `AUDIO_PROFILE_MUSIC_HIGH_QUALITY(4)` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO(5)`
+ * and `scenario` to `AUDIO_SCENARIO_HIGH_DEFINITION(6)` before calling `setVoiceBeautifierPreset`.
+ * - If you have an audio capturing device that can already restore audio details to a high
+ * degree, Agora recommends that you do not enable ultra-high quality; otherwise, the SDK may
+ * over-restore audio details, and you may not hear the anticipated voice effect.
+ */
+ ULTRA_HIGH_QUALITY_VOICE = 0x01040100
+};
+
+/** Preset voice effects.
+ *
+ * For better voice effects, Agora recommends setting the `profile` parameter of `setAudioProfile` to `AUDIO_PROFILE_MUSIC_HIGH_QUALITY` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO` before using the following presets:
+ *
+ * - `ROOM_ACOUSTICS_KTV`
+ * - `ROOM_ACOUSTICS_VOCAL_CONCERT`
+ * - `ROOM_ACOUSTICS_STUDIO`
+ * - `ROOM_ACOUSTICS_PHONOGRAPH`
+ * - `ROOM_ACOUSTICS_SPACIAL`
+ * - `ROOM_ACOUSTICS_ETHEREAL`
+ * - `VOICE_CHANGER_EFFECT_UNCLE`
+ * - `VOICE_CHANGER_EFFECT_OLDMAN`
+ * - `VOICE_CHANGER_EFFECT_BOY`
+ * - `VOICE_CHANGER_EFFECT_SISTER`
+ * - `VOICE_CHANGER_EFFECT_GIRL`
+ * - `VOICE_CHANGER_EFFECT_PIGKING`
+ * - `VOICE_CHANGER_EFFECT_HULK`
+ * - `PITCH_CORRECTION`
+ */
+enum AUDIO_EFFECT_PRESET {
+ /** Turn off voice effects, that is, use the original voice.
+ */
+ AUDIO_EFFECT_OFF = 0x00000000,
+ /** The voice effect typical of a KTV venue.
+ */
+ ROOM_ACOUSTICS_KTV = 0x02010100,
+ /** The voice effect typical of a concert hall.
+ */
+ ROOM_ACOUSTICS_VOCAL_CONCERT = 0x02010200,
+ /** The voice effect typical of a recording studio.
+ */
+ ROOM_ACOUSTICS_STUDIO = 0x02010300,
+ /** The voice effect typical of a vintage phonograph.
+ */
+ ROOM_ACOUSTICS_PHONOGRAPH = 0x02010400,
+ /** The virtual stereo effect, which renders monophonic audio as stereo audio.
+ *
+ * @note Before using this preset, set the `profile` parameter of `setAudioProfile`
+ * to `AUDIO_PROFILE_MUSIC_STANDARD_STEREO(3)` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO(5)`;
+ * otherwise, the preset setting is invalid.
+ */
+ ROOM_ACOUSTICS_VIRTUAL_STEREO = 0x02010500,
+ /** A more spatial voice effect.
+ */
+ ROOM_ACOUSTICS_SPACIAL = 0x02010600,
+ /** A more ethereal voice effect.
+ */
+ ROOM_ACOUSTICS_ETHEREAL = 0x02010700,
+ /** A 3D voice effect that makes the voice appear to be moving around the user. The default cycle
+ * period of the 3D voice effect is 10 seconds. To change the cycle period, call `setAudioEffectParameters`
+ * after this method.
+ *
+ * @note
+ * - Before using this preset, set the `profile` parameter of `setAudioProfile` to
+ * `AUDIO_PROFILE_MUSIC_STANDARD_STEREO` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO`; otherwise,
+ * the preset setting is invalid.
+ * - If the 3D voice effect is enabled, users need to use stereo audio playback devices to hear
+ * the anticipated voice effect.
+ */
+ ROOM_ACOUSTICS_3D_VOICE = 0x02010800,
+ /** virtual suround sound.
+ *
+ * @note
+ * - Agora recommends using this enumerator to process virtual suround sound; otherwise, you may
+ * not hear the anticipated voice effect.
+ * - To achieve better audio effect quality, Agora recommends calling \ref
+ * IRtcEngine::setAudioProfile "setAudioProfile" and setting the `profile` parameter to
+ * `AUDIO_PROFILE_MUSIC_HIGH_QUALITY(4)` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO(5)` before
+ * setting this enumerator.
+ */
+ ROOM_ACOUSTICS_VIRTUAL_SURROUND_SOUND = 0x02010900,
+ /** A middle-aged man's voice.
+ *
+ * @note
+ * Agora recommends using this enumerator to process a male-sounding voice; otherwise, you may
+ * not hear the anticipated voice effect.
+ */
+ VOICE_CHANGER_EFFECT_UNCLE = 0x02020100,
+ /** A senior man's voice.
+ *
+ * @note Agora recommends using this enumerator to process a male-sounding voice; otherwise, you may
+ * not hear the anticipated voice effect.
+ */
+ VOICE_CHANGER_EFFECT_OLDMAN = 0x02020200,
+ /** A boy's voice.
+ *
+ * @note Agora recommends using this enumerator to process a male-sounding voice; otherwise, you may
+ * not hear the anticipated voice effect.
+ */
+ VOICE_CHANGER_EFFECT_BOY = 0x02020300,
+ /** A young woman's voice.
+ *
+ * @note
+ * - Agora recommends using this enumerator to process a female-sounding voice; otherwise, you may
+ * not hear the anticipated voice effect.
+ */
+ VOICE_CHANGER_EFFECT_SISTER = 0x02020400,
+ /** A girl's voice.
+ *
+ * @note Agora recommends using this enumerator to process a female-sounding voice; otherwise, you may
+ * not hear the anticipated voice effect.
+ */
+ VOICE_CHANGER_EFFECT_GIRL = 0x02020500,
+ /** The voice of Pig King, a character in Journey to the West who has a voice like a growling
+ * bear.
+ */
+ VOICE_CHANGER_EFFECT_PIGKING = 0x02020600,
+ /** The Hulk's voice.
+ */
+ VOICE_CHANGER_EFFECT_HULK = 0x02020700,
+ /** An audio effect typical of R&B music.
+ *
+ * @note Before using this preset, set the `profile` parameter of `setAudioProfile` to
+ - `AUDIO_PROFILE_MUSIC_HIGH_QUALITY` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO`; otherwise,
+ * the preset setting is invalid.
+ */
+ STYLE_TRANSFORMATION_RNB = 0x02030100,
+ /** The voice effect typical of popular music.
+ *
+ * @note Before using this preset, set the `profile` parameter of `setAudioProfile` to
+ - `AUDIO_PROFILE_MUSIC_HIGH_QUALITY` or `AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO`; otherwise,
+ * the preset setting is invalid.
+ */
+ STYLE_TRANSFORMATION_POPULAR = 0x02030200,
+ /** A pitch correction effect that corrects the user's pitch based on the pitch of the natural C
+ * major scale. After setting this voice effect, you can call `setAudioEffectParameters` to adjust
+ * the basic mode of tuning and the pitch of the main tone.
+ */
+ PITCH_CORRECTION = 0x02040100,
+
+ /** Todo: Electronic sound, Magic tone haven't been implemented.
+ *
+ */
+};
+
+/** The options for SDK preset voice conversion.
+ */
+enum VOICE_CONVERSION_PRESET {
+ /** Turn off voice conversion and use the original voice.
+ */
+ VOICE_CONVERSION_OFF = 0x00000000,
+ /** A gender-neutral voice. To avoid audio distortion, ensure that you use this enumerator to process a female-sounding voice.
+ */
+ VOICE_CHANGER_NEUTRAL = 0x03010100,
+ /** A sweet voice. To avoid audio distortion, ensure that you use this enumerator to process a female-sounding voice.
+ */
+ VOICE_CHANGER_SWEET = 0x03010200,
+ /** A steady voice. To avoid audio distortion, ensure that you use this enumerator to process a male-sounding voice.
+ */
+ VOICE_CHANGER_SOLID = 0x03010300,
+ /** A deep voice. To avoid audio distortion, ensure that you use this enumerator to process a male-sounding voice.
+ */
+ VOICE_CHANGER_BASS = 0x03010400,
+ /** A voice like a cartoon character.
+ */
+ VOICE_CHANGER_CARTOON = 0x03010500,
+ /** A voice like a child.
+ */
+ VOICE_CHANGER_CHILDLIKE = 0x03010600,
+ /** A voice like a phone operator.
+ */
+ VOICE_CHANGER_PHONE_OPERATOR = 0x03010700,
+ /** A monster voice.
+ */
+ VOICE_CHANGER_MONSTER = 0x03010800,
+ /** A voice like Transformers.
+ */
+ VOICE_CHANGER_TRANSFORMERS = 0x03010900,
+ /** A voice like Groot.
+ */
+ VOICE_CHANGER_GROOT = 0x03010A00,
+ /** A voice like Darth Vader.
+ */
+ VOICE_CHANGER_DARTH_VADER = 0x03010B00,
+ /** A rough female voice.
+ */
+ VOICE_CHANGER_IRON_LADY = 0x03010C00,
+ /** A voice like Crayon Shin-chan.
+ */
+ VOICE_CHANGER_SHIN_CHAN = 0x03010D00,
+ /** A voice like a castrato.
+ */
+ VOICE_CHANGER_GIRLISH_MAN = 0x03010E00,
+ /** A voice like chipmunk.
+ */
+ VOICE_CHANGER_CHIPMUNK = 0x03010F00,
+
+};
+
+/** The options for SDK preset headphone equalizer.
+ */
+enum HEADPHONE_EQUALIZER_PRESET {
+ /** Turn off headphone EQ and use the original voice.
+ */
+ HEADPHONE_EQUALIZER_OFF = 0x00000000,
+ /** For over-ear headphones.
+ */
+ HEADPHONE_EQUALIZER_OVEREAR = 0x04000001,
+ /** For in-ear headphones.
+ */
+ HEADPHONE_EQUALIZER_INEAR = 0x04000002
+};
+
+/**
+ * Screen sharing configurations.
+ */
+struct ScreenCaptureParameters {
+ /**
+ * On Windows and macOS, it represents the video encoding resolution of the shared screen stream.
+ * See `VideoDimensions`. The default value is 1920 x 1080, that is, 2,073,600 pixels. Agora uses
+ * the value of this parameter to calculate the charges.
+ *
+ * If the aspect ratio is different between the encoding dimensions and screen dimensions, Agora
+ * applies the following algorithms for encoding. Suppose dimensions are 1920 x 1080:
+ * - If the value of the screen dimensions is lower than that of dimensions, for example,
+ * 1000 x 1000 pixels, the SDK uses 1000 x 1000 pixels for encoding.
+ * - If the value of the screen dimensions is higher than that of dimensions, for example,
+ * 2000 x 1500, the SDK uses the maximum value under dimensions with the aspect ratio of
+ * the screen dimension (4:3) for encoding, that is, 1440 x 1080.
+ */
+ VideoDimensions dimensions;
+ /**
+ * On Windows and macOS, it represents the video encoding frame rate (fps) of the shared screen stream.
+ * The frame rate (fps) of the shared region. The default value is 5. We do not recommend setting
+ * this to a value greater than 15.
+ */
+ int frameRate;
+ /**
+ * On Windows and macOS, it represents the video encoding bitrate of the shared screen stream.
+ * The bitrate (Kbps) of the shared region. The default value is 0 (the SDK works out a bitrate
+ * according to the dimensions of the current screen).
+ */
+ int bitrate;
+ /** Whether to capture the mouse in screen sharing:
+ * - `true`: (Default) Capture the mouse.
+ * - `false`: Do not capture the mouse.
+ */
+ bool captureMouseCursor;
+ /**
+ * Whether to bring the window to the front when calling the `startScreenCaptureByWindowId` method to share it:
+ * - `true`: Bring the window to the front.
+ * - `false`: (Default) Do not bring the window to the front.
+ */
+ bool windowFocus;
+ /**
+ * A list of IDs of windows to be blocked. When calling `startScreenCaptureByDisplayId` to start screen sharing,
+ * you can use this parameter to block a specified window. When calling `updateScreenCaptureParameters` to update
+ * screen sharing configurations, you can use this parameter to dynamically block the specified windows during
+ * screen sharing.
+ */
+ view_t *excludeWindowList;
+ /**
+ * The number of windows to be blocked.
+ */
+ int excludeWindowCount;
+
+ /** The width (px) of the border. Defaults to 0, and the value range is [0,50].
+ *
+ */
+ int highLightWidth;
+ /** The color of the border in RGBA format. The default value is 0xFF8CBF26.
+ *
+ */
+ unsigned int highLightColor;
+ /** Whether to place a border around the shared window or screen:
+ * - true: Place a border.
+ * - false: (Default) Do not place a border.
+ *
+ * @note When you share a part of a window or screen, the SDK places a border around the entire window or screen if you set `enableHighLight` as true.
+ *
+ */
+ bool enableHighLight;
+
+ ScreenCaptureParameters()
+ : dimensions(1920, 1080), frameRate(5), bitrate(STANDARD_BITRATE), captureMouseCursor(true), windowFocus(false), excludeWindowList(OPTIONAL_NULLPTR), excludeWindowCount(0), highLightWidth(0), highLightColor(0), enableHighLight(false) {}
+ ScreenCaptureParameters(const VideoDimensions& d, int f, int b)
+ : dimensions(d), frameRate(f), bitrate(b), captureMouseCursor(true), windowFocus(false), excludeWindowList(OPTIONAL_NULLPTR), excludeWindowCount(0), highLightWidth(0), highLightColor(0), enableHighLight(false) {}
+ ScreenCaptureParameters(int width, int height, int f, int b)
+ : dimensions(width, height), frameRate(f), bitrate(b), captureMouseCursor(true), windowFocus(false), excludeWindowList(OPTIONAL_NULLPTR), excludeWindowCount(0), highLightWidth(0), highLightColor(0), enableHighLight(false){}
+ ScreenCaptureParameters(int width, int height, int f, int b, bool cur, bool fcs)
+ : dimensions(width, height), frameRate(f), bitrate(b), captureMouseCursor(cur), windowFocus(fcs), excludeWindowList(OPTIONAL_NULLPTR), excludeWindowCount(0), highLightWidth(0), highLightColor(0), enableHighLight(false) {}
+ ScreenCaptureParameters(int width, int height, int f, int b, view_t *ex, int cnt)
+ : dimensions(width, height), frameRate(f), bitrate(b), captureMouseCursor(true), windowFocus(false), excludeWindowList(ex), excludeWindowCount(cnt), highLightWidth(0), highLightColor(0), enableHighLight(false) {}
+ ScreenCaptureParameters(int width, int height, int f, int b, bool cur, bool fcs, view_t *ex, int cnt)
+ : dimensions(width, height), frameRate(f), bitrate(b), captureMouseCursor(cur), windowFocus(fcs), excludeWindowList(ex), excludeWindowCount(cnt), highLightWidth(0), highLightColor(0), enableHighLight(false) {}
+};
+
+/**
+ * Audio recording quality.
+ */
+enum AUDIO_RECORDING_QUALITY_TYPE {
+ /**
+ * 0: Low quality. The sample rate is 32 kHz, and the file size is around 1.2 MB after 10 minutes of recording.
+ */
+ AUDIO_RECORDING_QUALITY_LOW = 0,
+ /**
+ * 1: Medium quality. The sample rate is 32 kHz, and the file size is around 2 MB after 10 minutes of recording.
+ */
+ AUDIO_RECORDING_QUALITY_MEDIUM = 1,
+ /**
+ * 2: High quality. The sample rate is 32 kHz, and the file size is around 3.75 MB after 10 minutes of recording.
+ */
+ AUDIO_RECORDING_QUALITY_HIGH = 2,
+ /**
+ * 3: Ultra high audio recording quality.
+ */
+ AUDIO_RECORDING_QUALITY_ULTRA_HIGH = 3,
+};
+
+/**
+ * Recording content. Set in `startAudioRecording`.
+ */
+enum AUDIO_FILE_RECORDING_TYPE {
+ /**
+ * 1: Only records the audio of the local user.
+ */
+ AUDIO_FILE_RECORDING_MIC = 1,
+ /**
+ * 2: Only records the audio of all remote users.
+ */
+ AUDIO_FILE_RECORDING_PLAYBACK = 2,
+ /**
+ * 3: Records the mixed audio of the local and all remote users.
+ */
+ AUDIO_FILE_RECORDING_MIXED = 3,
+};
+
+/**
+ * Audio encoded frame observer position.
+ */
+enum AUDIO_ENCODED_FRAME_OBSERVER_POSITION {
+ /**
+ * 1: Only records the audio of the local user.
+ */
+ AUDIO_ENCODED_FRAME_OBSERVER_POSITION_RECORD = 1,
+ /**
+ * 2: Only records the audio of all remote users.
+ */
+ AUDIO_ENCODED_FRAME_OBSERVER_POSITION_PLAYBACK = 2,
+ /**
+ * 3: Records the mixed audio of the local and all remote users.
+ */
+ AUDIO_ENCODED_FRAME_OBSERVER_POSITION_MIXED = 3,
+};
+
+/**
+ * Recording configuration.
+ */
+struct AudioRecordingConfiguration {
+ /**
+ * The absolute path (including the filename extensions) of the recording file. For example: `C:\music\audio.mp4`.
+ * @note Ensure that the directory for the log files exists and is writable.
+ */
+ const char* filePath;
+ /**
+ * Whether to encode the audio data:
+ * - `true`: Encode audio data in AAC.
+ * - `false`: (Default) Do not encode audio data, but save the recorded audio data directly.
+ */
+ bool encode;
+ /**
+ * Recording sample rate (Hz).
+ * - 16000
+ * - (Default) 32000
+ * - 44100
+ * - 48000
+ * @note If you set this parameter to 44100 or 48000, Agora recommends recording WAV files, or AAC files with quality
+ * to be `AUDIO_RECORDING_QUALITY_MEDIUM` or `AUDIO_RECORDING_QUALITY_HIGH` for better recording quality.
+ */
+ int sampleRate;
+ /**
+ * The recording content. See `AUDIO_FILE_RECORDING_TYPE`.
+ */
+ AUDIO_FILE_RECORDING_TYPE fileRecordingType;
+ /**
+ * Recording quality. See `AUDIO_RECORDING_QUALITY_TYPE`.
+ * @note This parameter applies to AAC files only.
+ */
+ AUDIO_RECORDING_QUALITY_TYPE quality;
+
+ /**
+ * Recording channel. The following values are supported:
+ * - (Default) 1
+ * - 2
+ */
+ int recordingChannel;
+
+ AudioRecordingConfiguration()
+ : filePath(OPTIONAL_NULLPTR),
+ encode(false),
+ sampleRate(32000),
+ fileRecordingType(AUDIO_FILE_RECORDING_MIXED),
+ quality(AUDIO_RECORDING_QUALITY_LOW),
+ recordingChannel(1) {}
+
+ AudioRecordingConfiguration(const char* file_path, int sample_rate, AUDIO_RECORDING_QUALITY_TYPE quality_type, int channel)
+ : filePath(file_path),
+ encode(false),
+ sampleRate(sample_rate),
+ fileRecordingType(AUDIO_FILE_RECORDING_MIXED),
+ quality(quality_type),
+ recordingChannel(channel) {}
+
+ AudioRecordingConfiguration(const char* file_path, bool enc, int sample_rate, AUDIO_FILE_RECORDING_TYPE type, AUDIO_RECORDING_QUALITY_TYPE quality_type, int channel)
+ : filePath(file_path),
+ encode(enc),
+ sampleRate(sample_rate),
+ fileRecordingType(type),
+ quality(quality_type),
+ recordingChannel(channel) {}
+
+ AudioRecordingConfiguration(const AudioRecordingConfiguration &rhs)
+ : filePath(rhs.filePath),
+ encode(rhs.encode),
+ sampleRate(rhs.sampleRate),
+ fileRecordingType(rhs.fileRecordingType),
+ quality(rhs.quality),
+ recordingChannel(rhs.recordingChannel) {}
+};
+
+/**
+ * Observer settings for the encoded audio.
+ */
+struct AudioEncodedFrameObserverConfig {
+ /**
+ * Audio profile. For details, see `AUDIO_ENCODED_FRAME_OBSERVER_POSITION`.
+ */
+ AUDIO_ENCODED_FRAME_OBSERVER_POSITION postionType;
+ /**
+ * Audio encoding type. For details, see `AUDIO_ENCODING_TYPE`.
+ */
+ AUDIO_ENCODING_TYPE encodingType;
+
+ AudioEncodedFrameObserverConfig()
+ : postionType(AUDIO_ENCODED_FRAME_OBSERVER_POSITION_PLAYBACK),
+ encodingType(AUDIO_ENCODING_TYPE_OPUS_48000_MEDIUM){}
+
+};
+/**
+ * The encoded audio observer.
+ */
+class IAudioEncodedFrameObserver {
+public:
+/**
+* Gets the encoded audio data of the local user.
+*
+* After calling `registerAudioEncodedFrameObserver` and setting the encoded audio as `AUDIO_ENCODED_FRAME_OBSERVER_POSITION_RECORD`,
+* you can get the encoded audio data of the local user from this callback.
+*
+* @param frameBuffer The pointer to the audio frame buffer.
+* @param length The data length (byte) of the audio frame.
+* @param audioEncodedFrameInfo Audio information after encoding. For details, see `EncodedAudioFrameInfo`.
+*/
+virtual void onRecordAudioEncodedFrame(const uint8_t* frameBuffer, int length, const EncodedAudioFrameInfo& audioEncodedFrameInfo) = 0;
+
+/**
+* Gets the encoded audio data of all remote users.
+*
+* After calling `registerAudioEncodedFrameObserver` and setting the encoded audio as `AUDIO_ENCODED_FRAME_OBSERVER_POSITION_PLAYBACK`,
+* you can get encoded audio data of all remote users through this callback.
+*
+* @param frameBuffer The pointer to the audio frame buffer.
+* @param length The data length (byte) of the audio frame.
+* @param audioEncodedFrameInfo Audio information after encoding. For details, see `EncodedAudioFrameInfo`.
+*/
+virtual void onPlaybackAudioEncodedFrame(const uint8_t* frameBuffer, int length, const EncodedAudioFrameInfo& audioEncodedFrameInfo) = 0;
+
+/**
+* Gets the mixed and encoded audio data of the local and all remote users.
+*
+* After calling `registerAudioEncodedFrameObserver` and setting the audio profile as `AUDIO_ENCODED_FRAME_OBSERVER_POSITION_MIXED`,
+* you can get the mixed and encoded audio data of the local and all remote users through this callback.
+*
+* @param frameBuffer The pointer to the audio frame buffer.
+* @param length The data length (byte) of the audio frame.
+* @param audioEncodedFrameInfo Audio information after encoding. For details, see `EncodedAudioFrameInfo`.
+*/
+virtual void onMixedAudioEncodedFrame(const uint8_t* frameBuffer, int length, const EncodedAudioFrameInfo& audioEncodedFrameInfo) = 0;
+
+virtual ~IAudioEncodedFrameObserver () {}
+};
+
+/** The region for connection, which is the region where the server the SDK connects to is located.
+ */
+enum AREA_CODE {
+ /**
+ * Mainland China.
+ */
+ AREA_CODE_CN = 0x00000001,
+ /**
+ * North America.
+ */
+ AREA_CODE_NA = 0x00000002,
+ /**
+ * Europe.
+ */
+ AREA_CODE_EU = 0x00000004,
+ /**
+ * Asia, excluding Mainland China.
+ */
+ AREA_CODE_AS = 0x00000008,
+ /**
+ * Japan.
+ */
+ AREA_CODE_JP = 0x00000010,
+ /**
+ * India.
+ */
+ AREA_CODE_IN = 0x00000020,
+ /**
+ * (Default) Global.
+ */
+ AREA_CODE_GLOB = (0xFFFFFFFF)
+};
+
+enum AREA_CODE_EX {
+ /**
+ * Oceania
+ */
+ AREA_CODE_OC = 0x00000040,
+ /**
+ * South-American
+ */
+ AREA_CODE_SA = 0x00000080,
+ /**
+ * Africa
+ */
+ AREA_CODE_AF = 0x00000100,
+ /**
+ * South Korea
+ */
+ AREA_CODE_KR = 0x00000200,
+ /**
+ * Hong Kong and Macou
+ */
+ AREA_CODE_HKMC = 0x00000400,
+ /**
+ * United States
+ */
+ AREA_CODE_US = 0x00000800,
+ /**
+ * The global area (except China)
+ */
+ AREA_CODE_OVS = 0xFFFFFFFE
+};
+
+/**
+ * The error code of the channel media replay.
+ */
+enum CHANNEL_MEDIA_RELAY_ERROR {
+ /** 0: No error.
+ */
+ RELAY_OK = 0,
+ /** 1: An error occurs in the server response.
+ */
+ RELAY_ERROR_SERVER_ERROR_RESPONSE = 1,
+ /** 2: No server response. You can call the `leaveChannel` method to leave the channel.
+ *
+ * This error can also occur if your project has not enabled co-host token authentication. You can contact technical
+ * support to enable the service for cohosting across channels before starting a channel media relay.
+ */
+ RELAY_ERROR_SERVER_NO_RESPONSE = 2,
+ /** 3: The SDK fails to access the service, probably due to limited resources of the server.
+ */
+ RELAY_ERROR_NO_RESOURCE_AVAILABLE = 3,
+ /** 4: Fails to send the relay request.
+ */
+ RELAY_ERROR_FAILED_JOIN_SRC = 4,
+ /** 5: Fails to accept the relay request.
+ */
+ RELAY_ERROR_FAILED_JOIN_DEST = 5,
+ /** 6: The server fails to receive the media stream.
+ */
+ RELAY_ERROR_FAILED_PACKET_RECEIVED_FROM_SRC = 6,
+ /** 7: The server fails to send the media stream.
+ */
+ RELAY_ERROR_FAILED_PACKET_SENT_TO_DEST = 7,
+ /** 8: The SDK disconnects from the server due to poor network connections. You can call the `leaveChannel` method to
+ * leave the channel.
+ */
+ RELAY_ERROR_SERVER_CONNECTION_LOST = 8,
+ /** 9: An internal error occurs in the server.
+ */
+ RELAY_ERROR_INTERNAL_ERROR = 9,
+ /** 10: The token of the source channel has expired.
+ */
+ RELAY_ERROR_SRC_TOKEN_EXPIRED = 10,
+ /** 11: The token of the destination channel has expired.
+ */
+ RELAY_ERROR_DEST_TOKEN_EXPIRED = 11,
+};
+
+/**
+ * The state code of the channel media relay.
+ */
+enum CHANNEL_MEDIA_RELAY_STATE {
+ /** 0: The initial state. After you successfully stop the channel media relay by calling `stopChannelMediaRelay`,
+ * the `onChannelMediaRelayStateChanged` callback returns this state.
+ */
+ RELAY_STATE_IDLE = 0,
+ /** 1: The SDK tries to relay the media stream to the destination channel.
+ */
+ RELAY_STATE_CONNECTING = 1,
+ /** 2: The SDK successfully relays the media stream to the destination channel.
+ */
+ RELAY_STATE_RUNNING = 2,
+ /** 3: An error occurs. See `code` in `onChannelMediaRelayStateChanged` for the error code.
+ */
+ RELAY_STATE_FAILURE = 3,
+};
+
+/** The definition of ChannelMediaInfo.
+ */
+struct ChannelMediaInfo {
+ /** The user ID.
+ */
+ uid_t uid;
+ /** The channel name. The default value is NULL, which means that the SDK
+ * applies the current channel name.
+ */
+ const char* channelName;
+ /** The token that enables the user to join the channel. The default value
+ * is NULL, which means that the SDK applies the current token.
+ */
+ const char* token;
+
+ ChannelMediaInfo() : uid(0), channelName(NULL), token(NULL) {}
+ ChannelMediaInfo(const char* c, const char* t, uid_t u) : uid(u), channelName(c), token(t) {}
+};
+
+/** The definition of ChannelMediaRelayConfiguration.
+ */
+struct ChannelMediaRelayConfiguration {
+ /** The information of the source channel `ChannelMediaInfo`. It contains the following members:
+ * - `channelName`: The name of the source channel. The default value is `NULL`, which means the SDK applies the name
+ * of the current channel.
+ * - `uid`: The unique ID to identify the relay stream in the source channel. The default value is 0, which means the
+ * SDK generates a random UID. You must set it as 0.
+ * - `token`: The token for joining the source channel. It is generated with the `channelName` and `uid` you set in
+ * `srcInfo`.
+ * - If you have not enabled the App Certificate, set this parameter as the default value `NULL`, which means the
+ * SDK applies the App ID.
+ * - If you have enabled the App Certificate, you must use the token generated with the `channelName` and `uid`, and
+ * the `uid` must be set as 0.
+ */
+ ChannelMediaInfo* srcInfo;
+ /** The information of the destination channel `ChannelMediaInfo`. It contains the following members:
+ * - `channelName`: The name of the destination channel.
+ * - `uid`: The unique ID to identify the relay stream in the destination channel. The value
+ * ranges from 0 to (2^32-1). To avoid UID conflicts, this `UID` must be different from any
+ * other `UID` in the destination channel. The default value is 0, which means the SDK generates
+ * a random `UID`. Do not set this parameter as the `UID` of the host in the destination channel,
+ * and ensure that this `UID` is different from any other `UID` in the channel.
+ * - `token`: The token for joining the destination channel. It is generated with the `channelName`
+ * and `uid` you set in `destInfos`.
+ * - If you have not enabled the App Certificate, set this parameter as the default value NULL,
+ * which means the SDK applies the App ID.
+ * If you have enabled the App Certificate, you must use the token generated with the `channelName`
+ * and `uid`.
+ */
+ ChannelMediaInfo* destInfos;
+ /** The number of destination channels. The default value is 0, and the value range is from 0 to
+ * 6. Ensure that the value of this parameter corresponds to the number of `ChannelMediaInfo`
+ * structs you define in `destInfo`.
+ */
+ int destCount;
+
+ ChannelMediaRelayConfiguration() : srcInfo(OPTIONAL_NULLPTR), destInfos(OPTIONAL_NULLPTR), destCount(0) {}
+};
+
+/**
+ * The uplink network information.
+ */
+struct UplinkNetworkInfo {
+ /**
+ * The target video encoder bitrate (bps).
+ */
+ int video_encoder_target_bitrate_bps;
+
+ UplinkNetworkInfo() : video_encoder_target_bitrate_bps(0) {}
+
+ bool operator==(const UplinkNetworkInfo& rhs) const {
+ return (video_encoder_target_bitrate_bps == rhs.video_encoder_target_bitrate_bps);
+ }
+};
+
+struct DownlinkNetworkInfo {
+ struct PeerDownlinkInfo {
+ /**
+ * The ID of the user who owns the remote video stream.
+ */
+ const char* userId;
+ /**
+ * The remote video stream type: #VIDEO_STREAM_TYPE.
+ */
+ VIDEO_STREAM_TYPE stream_type;
+ /**
+ * The remote video downscale type: #REMOTE_VIDEO_DOWNSCALE_LEVEL.
+ */
+ REMOTE_VIDEO_DOWNSCALE_LEVEL current_downscale_level;
+ /**
+ * The expected bitrate in bps.
+ */
+ int expected_bitrate_bps;
+
+ PeerDownlinkInfo()
+ : userId(OPTIONAL_NULLPTR),
+ stream_type(VIDEO_STREAM_HIGH),
+ current_downscale_level(REMOTE_VIDEO_DOWNSCALE_LEVEL_NONE),
+ expected_bitrate_bps(-1) {}
+
+ PeerDownlinkInfo(const PeerDownlinkInfo& rhs)
+ : stream_type(rhs.stream_type),
+ current_downscale_level(rhs.current_downscale_level),
+ expected_bitrate_bps(rhs.expected_bitrate_bps) {
+ if (rhs.userId != OPTIONAL_NULLPTR) {
+ const int len = std::strlen(rhs.userId);
+ char* buf = new char[len + 1];
+ std::memcpy(buf, rhs.userId, len);
+ buf[len] = '\0';
+ userId = buf;
+ }
+ }
+
+ PeerDownlinkInfo& operator=(const PeerDownlinkInfo& rhs) {
+ if (this == &rhs) return *this;
+ userId = OPTIONAL_NULLPTR;
+ stream_type = rhs.stream_type;
+ current_downscale_level = rhs.current_downscale_level;
+ expected_bitrate_bps = rhs.expected_bitrate_bps;
+ if (rhs.userId != OPTIONAL_NULLPTR) {
+ const int len = std::strlen(rhs.userId);
+ char* buf = new char[len + 1];
+ std::memcpy(buf, rhs.userId, len);
+ buf[len] = '\0';
+ userId = buf;
+ }
+ return *this;
+ }
+
+ ~PeerDownlinkInfo() { delete[] userId; }
+ };
+
+ /**
+ * The lastmile buffer delay queue time in ms.
+ */
+ int lastmile_buffer_delay_time_ms;
+ /**
+ * The current downlink bandwidth estimation(bps) after downscale.
+ */
+ int bandwidth_estimation_bps;
+ /**
+ * The total video downscale level count.
+ */
+ int total_downscale_level_count;
+ /**
+ * The peer video downlink info array.
+ */
+ PeerDownlinkInfo* peer_downlink_info;
+ /**
+ * The total video received count.
+ */
+ int total_received_video_count;
+
+ DownlinkNetworkInfo()
+ : lastmile_buffer_delay_time_ms(-1),
+ bandwidth_estimation_bps(-1),
+ total_downscale_level_count(-1),
+ peer_downlink_info(OPTIONAL_NULLPTR),
+ total_received_video_count(-1) {}
+
+ DownlinkNetworkInfo(const DownlinkNetworkInfo& info)
+ : lastmile_buffer_delay_time_ms(info.lastmile_buffer_delay_time_ms),
+ bandwidth_estimation_bps(info.bandwidth_estimation_bps),
+ total_downscale_level_count(info.total_downscale_level_count),
+ peer_downlink_info(OPTIONAL_NULLPTR),
+ total_received_video_count(info.total_received_video_count) {
+ if (total_received_video_count <= 0) return;
+ peer_downlink_info = new PeerDownlinkInfo[total_received_video_count];
+ for (int i = 0; i < total_received_video_count; ++i)
+ peer_downlink_info[i] = info.peer_downlink_info[i];
+ }
+
+ DownlinkNetworkInfo& operator=(const DownlinkNetworkInfo& rhs) {
+ if (this == &rhs) return *this;
+ lastmile_buffer_delay_time_ms = rhs.lastmile_buffer_delay_time_ms;
+ bandwidth_estimation_bps = rhs.bandwidth_estimation_bps;
+ total_downscale_level_count = rhs.total_downscale_level_count;
+ peer_downlink_info = OPTIONAL_NULLPTR;
+ total_received_video_count = rhs.total_received_video_count;
+ if (total_received_video_count > 0) {
+ peer_downlink_info = new PeerDownlinkInfo[total_received_video_count];
+ for (int i = 0; i < total_received_video_count; ++i)
+ peer_downlink_info[i] = rhs.peer_downlink_info[i];
+ }
+ return *this;
+ }
+
+ ~DownlinkNetworkInfo() { delete[] peer_downlink_info; }
+};
+
+/**
+ * The built-in encryption mode.
+ *
+ * Agora recommends using AES_128_GCM2 or AES_256_GCM2 encrypted mode. These two modes support the
+ * use of salt for higher security.
+ */
+enum ENCRYPTION_MODE {
+ /** 1: 128-bit AES encryption, XTS mode.
+ */
+ AES_128_XTS = 1,
+ /** 2: 128-bit AES encryption, ECB mode.
+ */
+ AES_128_ECB = 2,
+ /** 3: 256-bit AES encryption, XTS mode.
+ */
+ AES_256_XTS = 3,
+ /** 4: 128-bit SM4 encryption, ECB mode.
+ */
+ SM4_128_ECB = 4,
+ /** 5: 128-bit AES encryption, GCM mode.
+ */
+ AES_128_GCM = 5,
+ /** 6: 256-bit AES encryption, GCM mode.
+ */
+ AES_256_GCM = 6,
+ /** 7: (Default) 128-bit AES encryption, GCM mode. This encryption mode requires the setting of
+ * salt (`encryptionKdfSalt`).
+ */
+ AES_128_GCM2 = 7,
+ /** 8: 256-bit AES encryption, GCM mode. This encryption mode requires the setting of salt (`encryptionKdfSalt`).
+ */
+ AES_256_GCM2 = 8,
+ /** Enumerator boundary.
+ */
+ MODE_END,
+};
+
+/** Built-in encryption configurations. */
+struct EncryptionConfig {
+ /**
+ * The built-in encryption mode. See #ENCRYPTION_MODE. Agora recommends using `AES_128_GCM2`
+ * or `AES_256_GCM2` encrypted mode. These two modes support the use of salt for higher security.
+ */
+ ENCRYPTION_MODE encryptionMode;
+ /**
+ * Encryption key in string type with unlimited length. Agora recommends using a 32-byte key.
+ *
+ * @note If you do not set an encryption key or set it as NULL, you cannot use the built-in encryption, and the SDK returns #ERR_INVALID_ARGUMENT (-2).
+ */
+ const char* encryptionKey;
+ /**
+ * Salt, 32 bytes in length. Agora recommends that you use OpenSSL to generate salt on the server side.
+ *
+ * @note This parameter takes effect only in `AES_128_GCM2` or `AES_256_GCM2` encrypted mode.
+ * In this case, ensure that this parameter is not 0.
+ */
+ uint8_t encryptionKdfSalt[32];
+
+ EncryptionConfig()
+ : encryptionMode(AES_128_GCM2),
+ encryptionKey(OPTIONAL_NULLPTR)
+ {
+ memset(encryptionKdfSalt, 0, sizeof(encryptionKdfSalt));
+ }
+
+ /// @cond
+ const char* getEncryptionString() const {
+ switch(encryptionMode) {
+ case AES_128_XTS:
+ return "aes-128-xts";
+ case AES_128_ECB:
+ return "aes-128-ecb";
+ case AES_256_XTS:
+ return "aes-256-xts";
+ case SM4_128_ECB:
+ return "sm4-128-ecb";
+ case AES_128_GCM:
+ return "aes-128-gcm";
+ case AES_256_GCM:
+ return "aes-256-gcm";
+ case AES_128_GCM2:
+ return "aes-128-gcm-2";
+ case AES_256_GCM2:
+ return "aes-256-gcm-2";
+ default:
+ return "aes-128-gcm-2";
+ }
+ return "aes-128-gcm-2";
+ }
+ /// @endcond
+};
+
+/** Encryption error type.
+ */
+enum ENCRYPTION_ERROR_TYPE {
+ /**
+ * 0: Internal reason.
+ */
+ ENCRYPTION_ERROR_INTERNAL_FAILURE = 0,
+ /**
+ * 1: Decryption errors. Ensure that the receiver and the sender use the same encryption mode and key.
+ */
+ ENCRYPTION_ERROR_DECRYPTION_FAILURE = 1,
+ /**
+ * 2: Encryption errors.
+ */
+ ENCRYPTION_ERROR_ENCRYPTION_FAILURE = 2,
+};
+
+enum UPLOAD_ERROR_REASON
+{
+ UPLOAD_SUCCESS = 0,
+ UPLOAD_NET_ERROR = 1,
+ UPLOAD_SERVER_ERROR = 2,
+};
+
+/** The type of the device permission.
+ */
+enum PERMISSION_TYPE {
+ /**
+ * 0: Permission for the audio capture device.
+ */
+ RECORD_AUDIO = 0,
+ /**
+ * 1: Permission for the camera.
+ */
+ CAMERA = 1,
+
+ SCREEN_CAPTURE = 2,
+};
+
+/**
+ * The subscribing state.
+ */
+enum STREAM_SUBSCRIBE_STATE {
+ /**
+ * 0: The initial subscribing state after joining the channel.
+ */
+ SUB_STATE_IDLE = 0,
+ /**
+ * 1: Fails to subscribe to the remote stream. Possible reasons:
+ * - The remote user:
+ * - Calls `muteLocalAudioStream(true)` or `muteLocalVideoStream(true)` to stop sending local
+ * media stream.
+ * - Calls `disableAudio` or `disableVideo `to disable the local audio or video module.
+ * - Calls `enableLocalAudio(false)` or `enableLocalVideo(false)` to disable the local audio or video capture.
+ * - The role of the remote user is audience.
+ * - The local user calls the following methods to stop receiving remote streams:
+ * - Calls `muteRemoteAudioStream(true)`, `muteAllRemoteAudioStreams(true)` or `setDefaultMuteAllRemoteAudioStreams(true)` to stop receiving the remote audio streams.
+ * - Calls `muteRemoteVideoStream(true)`, `muteAllRemoteVideoStreams(true)` or `setDefaultMuteAllRemoteVideoStreams(true)` to stop receiving the remote video streams.
+ */
+ SUB_STATE_NO_SUBSCRIBED = 1,
+ /**
+ * 2: Subscribing.
+ */
+ SUB_STATE_SUBSCRIBING = 2,
+ /**
+ * 3: Subscribes to and receives the remote stream successfully.
+ */
+ SUB_STATE_SUBSCRIBED = 3
+};
+
+/**
+ * The publishing state.
+ */
+enum STREAM_PUBLISH_STATE {
+ /**
+ * 0: The initial publishing state after joining the channel.
+ */
+ PUB_STATE_IDLE = 0,
+ /**
+ * 1: Fails to publish the local stream. Possible reasons:
+ * - The local user calls `muteLocalAudioStream(true)` or `muteLocalVideoStream(true)` to stop sending the local media stream.
+ * - The local user calls `disableAudio` or `disableVideo` to disable the local audio or video module.
+ * - The local user calls `enableLocalAudio(false)` or `enableLocalVideo(false)` to disable the local audio or video capture.
+ * - The role of the local user is audience.
+ */
+ PUB_STATE_NO_PUBLISHED = 1,
+ /**
+ * 2: Publishing.
+ */
+ PUB_STATE_PUBLISHING = 2,
+ /**
+ * 3: Publishes successfully.
+ */
+ PUB_STATE_PUBLISHED = 3
+};
+
+/**
+ * The EchoTestConfiguration struct.
+ */
+struct EchoTestConfiguration {
+ view_t view;
+ bool enableAudio;
+ bool enableVideo;
+ const char* token;
+ const char* channelId;
+ int intervalInSeconds;
+
+ EchoTestConfiguration(view_t v, bool ea, bool ev, const char* t, const char* c, const int is)
+ : view(v), enableAudio(ea), enableVideo(ev), token(t), channelId(c), intervalInSeconds(is) {}
+
+ EchoTestConfiguration()
+ : view(OPTIONAL_NULLPTR), enableAudio(true), enableVideo(true), token(OPTIONAL_NULLPTR), channelId(OPTIONAL_NULLPTR), intervalInSeconds(2) {}
+};
+
+/**
+ * The information of the user.
+ */
+struct UserInfo {
+ /**
+ * The user ID.
+ */
+ uid_t uid;
+ /**
+ * The user account. The maximum data length is `MAX_USER_ACCOUNT_LENGTH_TYPE`.
+ */
+ char userAccount[MAX_USER_ACCOUNT_LENGTH];
+
+ UserInfo() : uid(0) {
+ userAccount[0] = '\0';
+ }
+};
+
+/**
+ * The audio filter of in-ear monitoring.
+ */
+enum EAR_MONITORING_FILTER_TYPE {
+ /**
+ * 1: Do not add an audio filter to the in-ear monitor.
+ */
+ EAR_MONITORING_FILTER_NONE = (1<<0),
+ /**
+ * 2: Enable audio filters to the in-ear monitor. If you implement functions such as voice
+ * beautifier and audio effect, users can hear the voice after adding these effects.
+ */
+ EAR_MONITORING_FILTER_BUILT_IN_AUDIO_FILTERS = (1<<1),
+ /**
+ * 4: Enable noise suppression to the in-ear monitor.
+ */
+ EAR_MONITORING_FILTER_NOISE_SUPPRESSION = (1<<2)
+};
+
+/**
+ * Thread priority type.
+ */
+enum THREAD_PRIORITY_TYPE {
+ /**
+ * 0: Lowest priority.
+ */
+ LOWEST = 0,
+ /**
+ * 1: Low priority.
+ */
+ LOW = 1,
+ /**
+ * 2: Normal priority.
+ */
+ NORMAL = 2,
+ /**
+ * 3: High priority.
+ */
+ HIGH = 3,
+ /**
+ * 4. Highest priority.
+ */
+ HIGHEST = 4,
+ /**
+ * 5. Critical priority.
+ */
+ CRITICAL = 5,
+};
+
+#if defined(__ANDROID__) || (defined(__APPLE__) && TARGET_OS_IOS)
+
+/**
+ * The video configuration for the shared screen stream.
+ */
+struct ScreenVideoParameters {
+ /**
+ * The dimensions of the video encoding resolution. The default value is `1280` x `720`.
+ * For recommended values, see [Recommended video
+ * profiles](https://docs.agora.io/en/Interactive%20Broadcast/game_streaming_video_profile?platform=Android#recommended-video-profiles).
+ * If the aspect ratio is different between width and height and the screen, the SDK adjusts the
+ * video encoding resolution according to the following rules (using an example where `width` ×
+ * `height` is 1280 × 720):
+ * - When the width and height of the screen are both lower than `width` and `height`, the SDK
+ * uses the resolution of the screen for video encoding. For example, if the screen is 640 ×
+ * 360, The SDK uses 640 × 360 for video encoding.
+ * - When either the width or height of the screen is higher than `width` or `height`, the SDK
+ * uses the maximum values that do not exceed those of `width` and `height` while maintaining
+ * the aspect ratio of the screen for video encoding. For example, if the screen is 2000 × 1500,
+ * the SDK uses 960 × 720 for video encoding.
+ *
+ * @note
+ * - The billing of the screen sharing stream is based on the values of width and height.
+ * When you do not pass in these values, Agora bills you at 1280 × 720;
+ * when you pass in these values, Agora bills you at those values.
+ * For details, see [Pricing for Real-time
+ * Communication](https://docs.agora.io/en/Interactive%20Broadcast/billing_rtc).
+ * - This value does not indicate the orientation mode of the output ratio.
+ * For how to set the video orientation, see `ORIENTATION_MODE`.
+ * - Whether the SDK can support a resolution at 720P depends on the performance of the device.
+ * If you set 720P but the device cannot support it, the video frame rate can be lower.
+ */
+ VideoDimensions dimensions;
+ /**
+ * The video encoding frame rate (fps). The default value is `15`.
+ * For recommended values, see [Recommended video
+ * profiles](https://docs.agora.io/en/Interactive%20Broadcast/game_streaming_video_profile?platform=Android#recommended-video-profiles).
+ */
+ int frameRate = 15;
+ /**
+ * The video encoding bitrate (Kbps). For recommended values, see [Recommended video
+ * profiles](https://docs.agora.io/en/Interactive%20Broadcast/game_streaming_video_profile?platform=Android#recommended-video-profiles).
+ */
+ int bitrate;
+ /*
+ * The content hint of the screen sharing:
+ */
+ VIDEO_CONTENT_HINT contentHint = VIDEO_CONTENT_HINT::CONTENT_HINT_MOTION;
+
+ ScreenVideoParameters() : dimensions(1280, 720) {}
+};
+
+/**
+ * The audio configuration for the shared screen stream.
+ */
+struct ScreenAudioParameters {
+ /**
+ * The audio sample rate (Hz). The default value is `16000`.
+ */
+ int sampleRate = 16000;
+ /**
+ * The number of audio channels. The default value is `2`, indicating dual channels.
+ */
+ int channels = 2;
+ /**
+ * The volume of the captured system audio. The value range is [0,100]. The default value is
+ * `100`.
+ */
+ int captureSignalVolume = 100;
+};
+
+/**
+ * The configuration of the screen sharing
+ */
+struct ScreenCaptureParameters2 {
+ /**
+ * Determines whether to capture system audio during screen sharing:
+ * - `true`: Capture.
+ * - `false`: (Default) Do not capture.
+ *
+ * **Note**
+ * Due to system limitations, capturing system audio is only available for Android API level 29
+ * and later (that is, Android 10 and later).
+ */
+ bool captureAudio = false;
+ /**
+ * The audio configuration for the shared screen stream.
+ */
+ ScreenAudioParameters audioParams;
+ /**
+ * Determines whether to capture the screen during screen sharing:
+ * - `true`: (Default) Capture.
+ * - `false`: Do not capture.
+ *
+ * **Note**
+ * Due to system limitations, screen capture is only available for Android API level 21 and later
+ * (that is, Android 5 and later).
+ */
+ bool captureVideo = true;
+ /**
+ * The video configuration for the shared screen stream.
+ */
+ ScreenVideoParameters videoParams;
+};
+#endif
+
+/**
+ * The tracing event of media rendering.
+ */
+enum MEDIA_TRACE_EVENT {
+ /**
+ * 0: The media frame has been rendered.
+ */
+ MEDIA_TRACE_EVENT_VIDEO_RENDERED = 0,
+ /**
+ * 1: The media frame has been decoded.
+ */
+ MEDIA_TRACE_EVENT_VIDEO_DECODED,
+};
+
+/**
+ * The video rendering tracing result
+ */
+struct VideoRenderingTracingInfo {
+ /**
+ * Elapsed time from the start tracing time to the time when the tracing event occurred.
+ */
+ int elapsedTime;
+ /**
+ * Elapsed time from the start tracing time to the time when join channel.
+ *
+ * **Note**
+ * If the start tracing time is behind the time when join channel, this value will be negative.
+ */
+ int start2JoinChannel;
+ /**
+ * Elapsed time from joining channel to finishing joining channel.
+ */
+ int join2JoinSuccess;
+ /**
+ * Elapsed time from finishing joining channel to remote user joined.
+ *
+ * **Note**
+ * If the start tracing time is after the time finishing join channel, this value will be
+ * the elapsed time from the start tracing time to remote user joined. The minimum value is 0.
+ */
+ int joinSuccess2RemoteJoined;
+ /**
+ * Elapsed time from remote user joined to set the view.
+ *
+ * **Note**
+ * If the start tracing time is after the time when remote user joined, this value will be
+ * the elapsed time from the start tracing time to set the view. The minimum value is 0.
+ */
+ int remoteJoined2SetView;
+ /**
+ * Elapsed time from remote user joined to the time subscribing remote video stream.
+ *
+ * **Note**
+ * If the start tracing time is after the time when remote user joined, this value will be
+ * the elapsed time from the start tracing time to the time subscribing remote video stream.
+ * The minimum value is 0.
+ */
+ int remoteJoined2UnmuteVideo;
+ /**
+ * Elapsed time from remote user joined to the remote video packet received.
+ *
+ * **Note**
+ * If the start tracing time is after the time when remote user joined, this value will be
+ * the elapsed time from the start tracing time to the time subscribing remote video stream.
+ * The minimum value is 0.
+ */
+ int remoteJoined2PacketReceived;
+};
+
+enum CONFIG_FETCH_TYPE {
+ /**
+ * 1: Fetch config when initializing RtcEngine, without channel info.
+ */
+ CONFIG_FETCH_TYPE_INITIALIZE = 1,
+ /**
+ * 2: Fetch config when joining channel with channel info, such as channel name and uid.
+ */
+ CONFIG_FETCH_TYPE_JOIN_CHANNEL = 2,
+};
+
+
+/** The local proxy mode type. */
+enum LOCAL_PROXY_MODE {
+ /** 0: Connect local proxy with high priority, if not connected to local proxy, fallback to sdrtn.
+ */
+ ConnectivityFirst = 0,
+ /** 1: Only connect local proxy
+ */
+ LocalOnly = 1,
+};
+
+struct LogUploadServerInfo {
+ /** Log upload server domain
+ */
+ const char* serverDomain;
+ /** Log upload server path
+ */
+ const char* serverPath;
+ /** Log upload server port
+ */
+ int serverPort;
+ /** Whether to use HTTPS request:
+ - true: Use HTTPS request
+ - fasle: Use HTTP request
+ */
+ bool serverHttps;
+
+ LogUploadServerInfo() : serverDomain(NULL), serverPath(NULL), serverPort(0), serverHttps(true) {}
+
+ LogUploadServerInfo(const char* domain, const char* path, int port, bool https) : serverDomain(domain), serverPath(path), serverPort(port), serverHttps(https) {}
+};
+
+struct AdvancedConfigInfo {
+ /** Log upload server
+ */
+ LogUploadServerInfo logUploadServer;
+};
+
+struct LocalAccessPointConfiguration {
+ /** Local access point IP address list.
+ */
+ const char** ipList;
+ /** The number of local access point IP address.
+ */
+ int ipListSize;
+ /** Local access point domain list.
+ */
+ const char** domainList;
+ /** The number of local access point domain.
+ */
+ int domainListSize;
+ /** Certificate domain name installed on specific local access point. pass "" means using sni domain on specific local access point
+ * SNI(Server Name Indication) is an extension to the TLS protocol.
+ */
+ const char* verifyDomainName;
+ /** Local proxy connection mode, connectivity first or local only.
+ */
+ LOCAL_PROXY_MODE mode;
+ /** Local proxy connection, advanced Config info.
+ */
+ AdvancedConfigInfo advancedConfig;
+ LocalAccessPointConfiguration() : ipList(NULL), ipListSize(0), domainList(NULL), domainListSize(0), verifyDomainName(NULL), mode(ConnectivityFirst) {}
+};
+
+/**
+ * The information about recorded media streams.
+ */
+struct RecorderStreamInfo {
+ const char* channelId;
+ /**
+ * The user ID.
+ */
+ uid_t uid;
+ /**
+ * The channel ID of the audio/video stream needs to be recorded.
+ */
+ RecorderStreamInfo() : channelId(NULL), uid(0) {}
+ RecorderStreamInfo(const char* channelId, uid_t uid) : channelId(channelId), uid(uid) {}
+};
+} // namespace rtc
+
+namespace base {
+
+class IEngineBase {
+ public:
+ virtual int queryInterface(rtc::INTERFACE_ID_TYPE iid, void** inter) = 0;
+ virtual ~IEngineBase() {}
+};
+
+class AParameter : public agora::util::AutoPtr {
+ public:
+ AParameter(IEngineBase& engine) { initialize(&engine); }
+ AParameter(IEngineBase* engine) { initialize(engine); }
+ AParameter(IAgoraParameter* p) : agora::util::AutoPtr(p) {}
+
+ private:
+ bool initialize(IEngineBase* engine) {
+ IAgoraParameter* p = OPTIONAL_NULLPTR;
+ if (engine && !engine->queryInterface(rtc::AGORA_IID_PARAMETER_ENGINE, (void**)&p)) reset(p);
+ return p != OPTIONAL_NULLPTR;
+ }
+};
+
+class LicenseCallback {
+ public:
+ virtual ~LicenseCallback() {}
+ virtual void onCertificateRequired() = 0;
+ virtual void onLicenseRequest() = 0;
+ virtual void onLicenseValidated() = 0;
+ virtual void onLicenseError(int result) = 0;
+};
+
+} // namespace base
+
+/**
+ * Spatial audio parameters
+ */
+struct SpatialAudioParams {
+ /**
+ * Speaker azimuth in a spherical coordinate system centered on the listener.
+ */
+ Optional speaker_azimuth;
+ /**
+ * Speaker elevation in a spherical coordinate system centered on the listener.
+ */
+ Optional speaker_elevation;
+ /**
+ * Distance between speaker and listener.
+ */
+ Optional speaker_distance;
+ /**
+ * Speaker orientation [0-180], 0 degree is the same with listener orientation.
+ */
+ Optional speaker_orientation;
+ /**
+ * Enable blur or not for the speaker.
+ */
+ Optional enable_blur;
+ /**
+ * Enable air absorb or not for the speaker.
+ */
+ Optional enable_air_absorb;
+ /**
+ * Speaker attenuation factor.
+ */
+ Optional speaker_attenuation;
+ /**
+ * Enable doppler factor.
+ */
+ Optional enable_doppler;
+};
+/**
+ * Layout info of video stream which compose a transcoder video stream.
+*/
+struct VideoLayout
+{
+ /**
+ * Channel Id from which this video stream come from.
+ */
+ const char* channelId;
+ /**
+ * User id of video stream.
+ */
+ rtc::uid_t uid;
+ /**
+ * User account of video stream.
+ */
+ user_id_t strUid;
+ /**
+ * x coordinate of video stream on a transcoded video stream canvas.
+ */
+ uint32_t x;
+ /**
+ * y coordinate of video stream on a transcoded video stream canvas.
+ */
+ uint32_t y;
+ /**
+ * width of video stream on a transcoded video stream canvas.
+ */
+ uint32_t width;
+ /**
+ * height of video stream on a transcoded video stream canvas.
+ */
+ uint32_t height;
+ /**
+ * video state of video stream on a transcoded video stream canvas.
+ * 0 for normal video , 1 for placeholder image showed , 2 for black image.
+ */
+ uint32_t videoState;
+
+ VideoLayout() : channelId(OPTIONAL_NULLPTR), uid(0), strUid(OPTIONAL_NULLPTR), x(0), y(0), width(0), height(0), videoState(0) {}
+};
+} // namespace agora
+
+/**
+ * Gets the version of the SDK.
+ * @param [out] build The build number of Agora SDK.
+ * @return The string of the version of the SDK.
+ */
+AGORA_API const char* AGORA_CALL getAgoraSdkVersion(int* build);
+
+/**
+ * Gets error description of an error code.
+ * @param [in] err The error code.
+ * @return The description of the error code.
+ */
+AGORA_API const char* AGORA_CALL getAgoraSdkErrorDescription(int err);
+
+AGORA_API int AGORA_CALL setAgoraSdkExternalSymbolLoader(void* (*func)(const char* symname));
+
+/**
+ * Generate credential
+ * @param [in, out] credential The content of the credential.
+ * @return The description of the error code.
+ * @note For license only, everytime will generate a different credential.
+ * So, just need to call once for a device, and then save the credential
+ */
+AGORA_API int AGORA_CALL createAgoraCredential(agora::util::AString &credential);
+
+/**
+ * Verify given certificate and return the result
+ * When you receive onCertificateRequired event, you must validate the certificate by calling
+ * this function. This is sync call, and if validation is success, it will return ERR_OK. And
+ * if failed to pass validation, you won't be able to joinChannel and ERR_CERT_FAIL will be
+ * returned.
+ * @param [in] credential_buf pointer to the credential's content.
+ * @param [in] credential_len the length of the credential's content.
+ * @param [in] certificate_buf pointer to the certificate's content.
+ * @param [in] certificate_len the length of the certificate's content.
+ * @return The description of the error code.
+ * @note For license only.
+ */
+AGORA_API int AGORA_CALL getAgoraCertificateVerifyResult(const char *credential_buf, int credential_len,
+ const char *certificate_buf, int certificate_len);
+
+/**
+ * @brief Implement the agora::base::LicenseCallback,
+ * create a LicenseCallback object to receive callbacks of license.
+ *
+ * @param [in] callback The object of agora::LiceseCallback,
+ * set the callback to null before delete it.
+ */
+AGORA_API void setAgoraLicenseCallback(agora::base::LicenseCallback *callback);
+
+/**
+ * @brief Get the LicenseCallback pointer if already setup,
+ * otherwise, return null.
+ *
+ * @return a pointer of agora::base::LicenseCallback
+ */
+
+AGORA_API agora::base::LicenseCallback* getAgoraLicenseCallback();
+
+/*
+ * Get monotonic time in ms which can be used by capture time,
+ * typical scenario is as follows:
+ *
+ * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ * | // custom audio/video base capture time, e.g. the first audio/video capture time. |
+ * | int64_t custom_capture_time_base; |
+ * | |
+ * | int64_t agora_monotonic_time = getAgoraCurrentMonotonicTimeInMs(); |
+ * | |
+ * | // offset is fixed once calculated in the begining. |
+ * | const int64_t offset = agora_monotonic_time - custom_capture_time_base; |
+ * | |
+ * | // realtime_custom_audio/video_capture_time is the origin capture time that customer provided.|
+ * | // actual_audio/video_capture_time is the actual capture time transfered to sdk. |
+ * | int64_t actual_audio_capture_time = realtime_custom_audio_capture_time + offset; |
+ * | int64_t actual_video_capture_time = realtime_custom_video_capture_time + offset; |
+ * ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ *
+ * @return
+ * - >= 0: Success.
+ * - < 0: Failure.
+ */
+AGORA_API int64_t AGORA_CALL getAgoraCurrentMonotonicTimeInMs();
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraExtensionProviderEntry.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraExtensionProviderEntry.h
new file mode 100644
index 000000000..acae66ea4
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraExtensionProviderEntry.h
@@ -0,0 +1,77 @@
+//
+// Copyright (c) 2020 Agora.io. All rights reserved
+
+// This program is confidential and proprietary to Agora.io.
+// And may not be copied, reproduced, modified, disclosed to others, published
+// or used, in whole or in part, without the express prior written permission
+// of Agora.io.
+
+#pragma once // NOLINT(build/header_guard)
+
+#include "NGIAgoraExtensionControl.h"
+AGORA_API agora::rtc::IExtensionControl* AGORA_CALL getAgoraExtensionControl();
+AGORA_API void AGORA_CALL declareProviderVersion(
+ const char*, const agora::rtc::ExtensionVersion&);
+typedef void(*agora_ext_entry_func_t)(void);
+AGORA_API void AGORA_CALL registerProviderEntry(const char*, agora_ext_entry_func_t);
+
+#define DECLARE_CREATE_AND_REGISTER_PROVIDER(PROVIDER_NAME, PROVIDER_CLASS, PROVIDER_INTERFACE_USED, ...) \
+static void register_##PROVIDER_NAME##_to_agora() { \
+ auto control = getAgoraExtensionControl(); \
+ agora::rtc::ExtensionVersion version = \
+ agora::rtc::ExtensionInterfaceVersion::Version(); \
+ declareProviderVersion(#PROVIDER_NAME, version); \
+ if (#PROVIDER_NAME && control) { \
+ control->registerProvider(#PROVIDER_NAME, \
+ new agora::RefCountedObject(__VA_ARGS__)); \
+ } \
+} \
+
+#define DECLARE_CREATE_AND_REGISTER_PROVIDER_PTR(PROVIDER_NAME, PROVIDER_INTERFACE_USED, PROVIDER_REF_PTR) \
+static void register_##PROVIDER_NAME##_to_agora() { \
+ auto control = getAgoraExtensionControl(); \
+ agora::rtc::ExtensionVersion version = \
+ agora::rtc::ExtensionInterfaceVersion::Version(); \
+ declareProviderVersion(#PROVIDER_NAME, version); \
+ if (#PROVIDER_NAME && control) { \
+ control->registerProvider(#PROVIDER_NAME, PROVIDER_REF_PTR); \
+ } \
+} \
+
+
+#if defined (__GNUC__)
+#define REGISTER_AGORA_EXTENSION_PROVIDER(PROVIDER_NAME, PROVIDER_CLASS, PROVIDER_INTERFACE_USED, ...) \
+DECLARE_CREATE_AND_REGISTER_PROVIDER(PROVIDER_NAME, PROVIDER_CLASS, PROVIDER_INTERFACE_USED, __VA_ARGS__); \
+__attribute__((constructor, used)) \
+static void _##PROVIDER_NAME##_provider_entry() { \
+ registerProviderEntry(#PROVIDER_NAME, register_##PROVIDER_NAME##_to_agora); \
+} \
+
+#define REGISTER_AGORA_EXTENSION_PROVIDER_PTR(PROVIDER_NAME, PROVIDER_INTERFACE_USED, PROVIDER_REF_PTR) \
+DECLARE_CREATE_AND_REGISTER_PROVIDER_PTR(PROVIDER_NAME, PROVIDER_INTERFACE_USED, PROVIDER_REF_PTR); \
+__attribute__((constructor, used)) \
+static void _##PROVIDER_NAME##_provider_entry() { \
+ registerProviderEntry(#PROVIDER_NAME, register_##PROVIDER_NAME##_to_agora); \
+} \
+
+
+#elif defined (_MSC_VER)
+#define REGISTER_AGORA_EXTENSION_PROVIDER(PROVIDER_NAME, PROVIDER_CLASS, PROVIDER_INTERFACE_USED, ...) \
+DECLARE_CREATE_AND_REGISTER_PROVIDER(PROVIDER_NAME, PROVIDER_CLASS, PROVIDER_INTERFACE_USED, __VA_ARGS__); \
+static int _##PROVIDER_NAME##_provider_entry() { \
+ registerProviderEntry(#PROVIDER_NAME, register_##PROVIDER_NAME##_to_agora); \
+ return 0; \
+} \
+const int DUMMY_AGORA_REGEXT_##PROVIDE_NAME##_VAR = _##PROVIDER_NAME##_provider_entry(); \
+
+#define REGISTER_AGORA_EXTENSION_PROVIDER_PTR(PROVIDER_NAME, PROVIDER_INTERFACE_USED, PROVIDER_REF_PTR) \
+DECLARE_CREATE_AND_REGISTER_PROVIDER_PTR(PROVIDER_NAME, PROVIDER_INTERFACE_USED, PROVIDER_REF_PTR); \
+static int _##PROVIDER_NAME##_provider_entry() { \
+ registerProviderEntry(#PROVIDER_NAME, register_##PROVIDER_NAME##_to_agora); \
+ return 0; \
+} \
+const int DUMMY_AGORA_REGEXT_##PROVIDE_NAME##_VAR = _##PROVIDER_NAME##_provider_entry(); \
+
+#else
+#error Unsupported Compilation Toolchain!
+#endif
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraExtensionVersion.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraExtensionVersion.h
new file mode 100644
index 000000000..988e5ecc8
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraExtensionVersion.h
@@ -0,0 +1,103 @@
+//
+// Copyright (c) 2021 Agora.io. All rights reserved
+
+// This program is confidential and proprietary to Agora.io.
+// And may not be copied, reproduced, modified, disclosed to others, published
+// or used, in whole or in part, without the express prior written permission
+// of Agora.io.
+
+#pragma once
+
+namespace agora {
+namespace rtc {
+
+struct ExtensionVersion {
+ // Extension Framework Version : major.minor.micro
+ int major_v;
+ int minor_v;
+ int micro_v;
+
+ ExtensionVersion()
+ : major_v(0), minor_v(0), micro_v(0) {}
+ ExtensionVersion(int majorV, int minorV = 0, int microV = 0)
+ : major_v(majorV), minor_v(minorV), micro_v(microV) {}
+
+ bool operator==(const ExtensionVersion& other) const {
+ return major_v == other.major_v && minor_v == other.minor_v && micro_v == other.micro_v;
+ }
+
+ bool operator>(const ExtensionVersion& other) const {
+ return major_v > other.major_v || (major_v == other.major_v && minor_v > other.minor_v)
+ || (major_v == other.major_v && minor_v == other.minor_v && micro_v > other.micro_v);
+ }
+
+ bool operator<(const ExtensionVersion& other) const {
+ return major_v < other.major_v || (major_v == other.major_v && minor_v < other.minor_v)
+ || (major_v == other.major_v && minor_v == other.minor_v && micro_v < other.micro_v);
+ }
+
+ bool operator<=(const ExtensionVersion& other) const {
+ return !operator>(other);
+ }
+
+ bool operator>=(const ExtensionVersion& other) const {
+ return !operator<(other);
+ }
+};
+
+#define BUMP_MAJOR_VERSION(VERSION) \
+ ExtensionVersion(VERSION.major_v + 1, 0, 0); \
+
+#define BUMP_MINOR_VERSION(VERSION) \
+ ExtensionVersion(VERSION.major_v, VERSION.minor_v + 1, 0); \
+
+#define BUMP_MICRO_VERSION(VERSION) \
+ ExtensionVersion(VERSION.major_v, VERSION.minor_v, VERSION.micro_v + 1); \
+
+class IExtensionProvider;
+class IExtensionProviderV2;
+class IExtensionProviderV3;
+class IAudioFilter;
+class IExtensionVideoFilter;
+class IScreenCaptureSource;
+
+template
+struct ExtensionInterfaceVersion;
+
+template <>
+struct ExtensionInterfaceVersion {
+ static ExtensionVersion Version() {
+ return ExtensionVersion(1, 0, 0);
+ }
+};
+
+template <>
+struct ExtensionInterfaceVersion {
+ static ExtensionVersion Version() {
+ return BUMP_MAJOR_VERSION(ExtensionInterfaceVersion::Version());
+ }
+};
+
+template <>
+struct ExtensionInterfaceVersion {
+ static ExtensionVersion Version() {
+ return ExtensionVersion(1, 0, 0);
+ }
+};
+
+template <>
+struct ExtensionInterfaceVersion {
+ static ExtensionVersion Version() {
+ return ExtensionVersion(1, 0, 0);
+ }
+};
+
+template <>
+struct ExtensionInterfaceVersion {
+ static ExtensionVersion Version() {
+ return ExtensionVersion(1, 0, 0);
+ }
+};
+
+} // namespace rtc
+} // namespace agora
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h
new file mode 100644
index 000000000..15dfd4b38
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h
@@ -0,0 +1,1671 @@
+// Agora Engine SDK
+//
+// Created by Sting Feng in 2017-11.
+// Copyright (c) 2017 Agora.io. All rights reserved.
+
+#pragma once // NOLINT(build/header_guard)
+
+#include
+#include
+#include
+#include
+
+#ifndef OPTIONAL_ENUM_SIZE_T
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+#define OPTIONAL_ENUM_SIZE_T enum : size_t
+#else
+#define OPTIONAL_ENUM_SIZE_T enum
+#endif
+#endif
+
+#if !defined(__APPLE__)
+#define __deprecated
+#endif
+
+namespace agora {
+namespace rtc {
+
+typedef unsigned int uid_t;
+typedef unsigned int track_id_t;
+typedef unsigned int conn_id_t;
+typedef unsigned int video_track_id_t;
+
+static const unsigned int INVALID_TRACK_ID = 0xffffffff;
+static const unsigned int DEFAULT_CONNECTION_ID = 0;
+static const unsigned int DUMMY_CONNECTION_ID = (std::numeric_limits::max)();
+
+struct EncodedVideoFrameInfo;
+
+/**
+* Video source types definition.
+**/
+enum VIDEO_SOURCE_TYPE {
+ /** Video captured by the camera.
+ */
+ VIDEO_SOURCE_CAMERA_PRIMARY = 0,
+ VIDEO_SOURCE_CAMERA = VIDEO_SOURCE_CAMERA_PRIMARY,
+ /** Video captured by the secondary camera.
+ */
+ VIDEO_SOURCE_CAMERA_SECONDARY = 1,
+ /** Video for screen sharing.
+ */
+ VIDEO_SOURCE_SCREEN_PRIMARY = 2,
+ VIDEO_SOURCE_SCREEN = VIDEO_SOURCE_SCREEN_PRIMARY,
+ /** Video for secondary screen sharing.
+ */
+ VIDEO_SOURCE_SCREEN_SECONDARY = 3,
+ /** Not define.
+ */
+ VIDEO_SOURCE_CUSTOM = 4,
+ /** Video for media player sharing.
+ */
+ VIDEO_SOURCE_MEDIA_PLAYER = 5,
+ /** Video for png image.
+ */
+ VIDEO_SOURCE_RTC_IMAGE_PNG = 6,
+ /** Video for png image.
+ */
+ VIDEO_SOURCE_RTC_IMAGE_JPEG = 7,
+ /** Video for png image.
+ */
+ VIDEO_SOURCE_RTC_IMAGE_GIF = 8,
+ /** Remote video received from network.
+ */
+ VIDEO_SOURCE_REMOTE = 9,
+ /** Video for transcoded.
+ */
+ VIDEO_SOURCE_TRANSCODED = 10,
+
+ /** Video captured by the third camera.
+ */
+ VIDEO_SOURCE_CAMERA_THIRD = 11,
+ /** Video captured by the fourth camera.
+ */
+ VIDEO_SOURCE_CAMERA_FOURTH = 12,
+ /** Video for third screen sharing.
+ */
+ VIDEO_SOURCE_SCREEN_THIRD = 13,
+ /** Video for fourth screen sharing.
+ */
+ VIDEO_SOURCE_SCREEN_FOURTH = 14,
+
+ VIDEO_SOURCE_UNKNOWN = 100
+};
+
+/**
+ * Audio routes.
+ */
+enum AudioRoute
+{
+ /**
+ * -1: The default audio route.
+ */
+ ROUTE_DEFAULT = -1,
+ /**
+ * The Headset.
+ */
+ ROUTE_HEADSET = 0,
+ /**
+ * The Earpiece.
+ */
+ ROUTE_EARPIECE = 1,
+ /**
+ * The Headset with no microphone.
+ */
+ ROUTE_HEADSETNOMIC = 2,
+ /**
+ * The Speakerphone.
+ */
+ ROUTE_SPEAKERPHONE = 3,
+ /**
+ * The Loudspeaker.
+ */
+ ROUTE_LOUDSPEAKER = 4,
+ /**
+ * The Bluetooth Headset via HFP.
+ */
+ ROUTE_HEADSETBLUETOOTH = 5,
+ /**
+ * The USB.
+ */
+ ROUTE_USB = 6,
+ /**
+ * The HDMI.
+ */
+ ROUTE_HDMI = 7,
+ /**
+ * The DisplayPort.
+ */
+ ROUTE_DISPLAYPORT = 8,
+ /**
+ * The AirPlay.
+ */
+ ROUTE_AIRPLAY = 9,
+ /**
+ * The Bluetooth Speaker via A2DP.
+ */
+ ROUTE_BLUETOOTH_SPEAKER = 10,
+};
+
+/**
+ * Bytes per sample
+ */
+enum BYTES_PER_SAMPLE {
+ /**
+ * two bytes per sample
+ */
+ TWO_BYTES_PER_SAMPLE = 2,
+};
+
+struct AudioParameters {
+ int sample_rate;
+ size_t channels;
+ size_t frames_per_buffer;
+
+ AudioParameters()
+ : sample_rate(0),
+ channels(0),
+ frames_per_buffer(0) {}
+};
+
+/**
+ * The use mode of the audio data.
+ */
+enum RAW_AUDIO_FRAME_OP_MODE_TYPE {
+ /** 0: Read-only mode: Users only read the data from `AudioFrame` without modifying anything.
+ * For example, when users acquire the data with the Agora SDK, then start the media push.
+ */
+ RAW_AUDIO_FRAME_OP_MODE_READ_ONLY = 0,
+
+ /** 2: Read and write mode: Users read the data from `AudioFrame`, modify it, and then play it.
+ * For example, when users have their own audio-effect processing module and perform some voice pre-processing, such as a voice change.
+ */
+ RAW_AUDIO_FRAME_OP_MODE_READ_WRITE = 2,
+};
+
+} // namespace rtc
+
+namespace media {
+ /**
+ * The type of media device.
+ */
+enum MEDIA_SOURCE_TYPE {
+ /**
+ * 0: The audio playback device.
+ */
+ AUDIO_PLAYOUT_SOURCE = 0,
+ /**
+ * 1: Microphone.
+ */
+ AUDIO_RECORDING_SOURCE = 1,
+ /**
+ * 2: Video captured by primary camera.
+ */
+ PRIMARY_CAMERA_SOURCE = 2,
+ /**
+ * 3: Video captured by secondary camera.
+ */
+ SECONDARY_CAMERA_SOURCE = 3,
+ /**
+ * 4: Video captured by primary screen capturer.
+ */
+ PRIMARY_SCREEN_SOURCE = 4,
+ /**
+ * 5: Video captured by secondary screen capturer.
+ */
+ SECONDARY_SCREEN_SOURCE = 5,
+ /**
+ * 6: Video captured by custom video source.
+ */
+ CUSTOM_VIDEO_SOURCE = 6,
+ /**
+ * 7: Video for media player sharing.
+ */
+ MEDIA_PLAYER_SOURCE = 7,
+ /**
+ * 8: Video for png image.
+ */
+ RTC_IMAGE_PNG_SOURCE = 8,
+ /**
+ * 9: Video for jpeg image.
+ */
+ RTC_IMAGE_JPEG_SOURCE = 9,
+ /**
+ * 10: Video for gif image.
+ */
+ RTC_IMAGE_GIF_SOURCE = 10,
+ /**
+ * 11: Remote video received from network.
+ */
+ REMOTE_VIDEO_SOURCE = 11,
+ /**
+ * 12: Video for transcoded.
+ */
+ TRANSCODED_VIDEO_SOURCE = 12,
+ /**
+ * 100: Internal Usage only.
+ */
+ UNKNOWN_MEDIA_SOURCE = 100
+};
+/** Definition of contentinspect
+ */
+#define MAX_CONTENT_INSPECT_MODULE_COUNT 32
+enum CONTENT_INSPECT_RESULT {
+ CONTENT_INSPECT_NEUTRAL = 1,
+ CONTENT_INSPECT_SEXY = 2,
+ CONTENT_INSPECT_PORN = 3,
+};
+
+enum CONTENT_INSPECT_TYPE {
+/**
+ * (Default) content inspect type invalid
+ */
+CONTENT_INSPECT_INVALID = 0,
+/**
+ * @deprecated
+ * Content inspect type moderation
+ */
+CONTENT_INSPECT_MODERATION __deprecated = 1,
+/**
+ * Content inspect type supervise
+ */
+CONTENT_INSPECT_SUPERVISION = 2,
+/**
+ * Content inspect type image moderation
+ */
+CONTENT_INSPECT_IMAGE_MODERATION = 3
+};
+
+struct ContentInspectModule {
+ /**
+ * The content inspect module type.
+ */
+ CONTENT_INSPECT_TYPE type;
+ /**The content inspect frequency, default is 0 second.
+ * the frequency <= 0 is invalid.
+ */
+ unsigned int interval;
+ ContentInspectModule() {
+ type = CONTENT_INSPECT_INVALID;
+ interval = 0;
+ }
+};
+/** Definition of ContentInspectConfig.
+ */
+struct ContentInspectConfig {
+ const char* extraInfo;
+ /**
+ * The specific server configuration for image moderation. Please contact technical support.
+ */
+ const char* serverConfig;
+ /**The content inspect modules, max length of modules is 32.
+ * the content(snapshot of send video stream, image) can be used to max of 32 types functions.
+ */
+ ContentInspectModule modules[MAX_CONTENT_INSPECT_MODULE_COUNT];
+ /**The content inspect module count.
+ */
+ int moduleCount;
+ ContentInspectConfig& operator=(const ContentInspectConfig& rth)
+ {
+ extraInfo = rth.extraInfo;
+ serverConfig = rth.serverConfig;
+ moduleCount = rth.moduleCount;
+ memcpy(&modules, &rth.modules, MAX_CONTENT_INSPECT_MODULE_COUNT * sizeof(ContentInspectModule));
+ return *this;
+ }
+ ContentInspectConfig() :extraInfo(NULL), serverConfig(NULL), moduleCount(0){}
+};
+
+namespace base {
+
+typedef void* view_t;
+
+typedef const char* user_id_t;
+
+static const uint8_t kMaxCodecNameLength = 50;
+
+/**
+ * The definition of the PacketOptions struct, which contains infomation of the packet
+ * in the RTP (Real-time Transport Protocal) header.
+ */
+struct PacketOptions {
+ /**
+ * The timestamp of the packet.
+ */
+ uint32_t timestamp;
+ // Audio level indication.
+ uint8_t audioLevelIndication;
+ PacketOptions()
+ : timestamp(0),
+ audioLevelIndication(127) {}
+};
+
+/**
+ * The detailed information of the incoming audio encoded frame.
+ */
+
+struct AudioEncodedFrameInfo {
+ /**
+ * The send time of the packet.
+ */
+ uint64_t sendTs;
+ /**
+ * The codec of the packet.
+ */
+ uint8_t codec;
+ AudioEncodedFrameInfo()
+ : sendTs(0),
+ codec(0) {}
+};
+
+/**
+ * The detailed information of the incoming audio frame in the PCM format.
+ */
+struct AudioPcmFrame {
+ /**
+ * The buffer size of the PCM audio frame.
+ */
+ OPTIONAL_ENUM_SIZE_T {
+ // Stereo, 32 kHz, 60 ms (2 * 32 * 60)
+ /**
+ * The max number of the samples of the data.
+ *
+ * When the number of audio channel is two, the sample rate is 32 kHZ,
+ * the buffer length of the data is 60 ms, the number of the samples of the data is 3840 (2 x 32 x 60).
+ */
+ kMaxDataSizeSamples = 3840,
+ /** The max number of the bytes of the data. */
+ kMaxDataSizeBytes = kMaxDataSizeSamples * sizeof(int16_t),
+ };
+
+ /** The timestamp (ms) of the audio frame.
+ */
+ int64_t capture_timestamp;
+ /** The number of samples per channel.
+ */
+ size_t samples_per_channel_;
+ /** The sample rate (Hz) of the audio data.
+ */
+ int sample_rate_hz_;
+ /** The channel number.
+ */
+ size_t num_channels_;
+ /** The number of bytes per sample.
+ */
+ rtc::BYTES_PER_SAMPLE bytes_per_sample;
+ /** The audio frame data. */
+ int16_t data_[kMaxDataSizeSamples];
+
+ AudioPcmFrame& operator=(const AudioPcmFrame& src) {
+ if(this == &src) {
+ return *this;
+ }
+
+ this->capture_timestamp = src.capture_timestamp;
+ this->samples_per_channel_ = src.samples_per_channel_;
+ this->sample_rate_hz_ = src.sample_rate_hz_;
+ this->bytes_per_sample = src.bytes_per_sample;
+ this->num_channels_ = src.num_channels_;
+
+ size_t length = src.samples_per_channel_ * src.num_channels_;
+ if (length > kMaxDataSizeSamples) {
+ length = kMaxDataSizeSamples;
+ }
+
+ memcpy(this->data_, src.data_, length * sizeof(int16_t));
+
+ return *this;
+ }
+
+ AudioPcmFrame()
+ : capture_timestamp(0),
+ samples_per_channel_(0),
+ sample_rate_hz_(0),
+ num_channels_(0),
+ bytes_per_sample(rtc::TWO_BYTES_PER_SAMPLE) {
+ memset(data_, 0, sizeof(data_));
+ }
+
+ AudioPcmFrame(const AudioPcmFrame& src)
+ : capture_timestamp(src.capture_timestamp),
+ samples_per_channel_(src.samples_per_channel_),
+ sample_rate_hz_(src.sample_rate_hz_),
+ num_channels_(src.num_channels_),
+ bytes_per_sample(src.bytes_per_sample) {
+ size_t length = src.samples_per_channel_ * src.num_channels_;
+ if (length > kMaxDataSizeSamples) {
+ length = kMaxDataSizeSamples;
+ }
+
+ memcpy(this->data_, src.data_, length * sizeof(int16_t));
+ }
+};
+
+/** Audio dual-mono output mode
+ */
+enum AUDIO_DUAL_MONO_MODE {
+ /**< ChanLOut=ChanLin, ChanRout=ChanRin */
+ AUDIO_DUAL_MONO_STEREO = 0,
+ /**< ChanLOut=ChanRout=ChanLin */
+ AUDIO_DUAL_MONO_L = 1,
+ /**< ChanLOut=ChanRout=ChanRin */
+ AUDIO_DUAL_MONO_R = 2,
+ /**< ChanLout=ChanRout=(ChanLin+ChanRin)/2 */
+ AUDIO_DUAL_MONO_MIX = 3
+};
+
+/**
+ * Video pixel formats.
+ */
+enum VIDEO_PIXEL_FORMAT {
+ /**
+ * 0: Default format.
+ */
+ VIDEO_PIXEL_DEFAULT = 0,
+ /**
+ * 1: I420.
+ */
+ VIDEO_PIXEL_I420 = 1,
+ /**
+ * 2: BGRA.
+ */
+ VIDEO_PIXEL_BGRA = 2,
+ /**
+ * 3: NV21.
+ */
+ VIDEO_PIXEL_NV21 = 3,
+ /**
+ * 4: RGBA.
+ */
+ VIDEO_PIXEL_RGBA = 4,
+ /**
+ * 8: NV12.
+ */
+ VIDEO_PIXEL_NV12 = 8,
+ /**
+ * 10: GL_TEXTURE_2D
+ */
+ VIDEO_TEXTURE_2D = 10,
+ /**
+ * 11: GL_TEXTURE_OES
+ */
+ VIDEO_TEXTURE_OES = 11,
+ /*
+ 12: pixel format for iOS CVPixelBuffer NV12
+ */
+ VIDEO_CVPIXEL_NV12 = 12,
+ /*
+ 13: pixel format for iOS CVPixelBuffer I420
+ */
+ VIDEO_CVPIXEL_I420 = 13,
+ /*
+ 14: pixel format for iOS CVPixelBuffer BGRA
+ */
+ VIDEO_CVPIXEL_BGRA = 14,
+ /**
+ * 16: I422.
+ */
+ VIDEO_PIXEL_I422 = 16,
+ /**
+ * 17: ID3D11Texture2D, only support DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_TYPELESS, DXGI_FORMAT_NV12 texture format
+ */
+ VIDEO_TEXTURE_ID3D11TEXTURE2D = 17,
+};
+
+/**
+ * The video display mode.
+ */
+enum RENDER_MODE_TYPE {
+ /**
+ * 1: Uniformly scale the video until it fills the visible boundaries
+ * (cropped). One dimension of the video may have clipped contents.
+ */
+ RENDER_MODE_HIDDEN = 1,
+ /**
+ * 2: Uniformly scale the video until one of its dimension fits the boundary
+ * (zoomed to fit). Areas that are not filled due to the disparity in the
+ * aspect ratio will be filled with black.
+ */
+ RENDER_MODE_FIT = 2,
+ /**
+ * @deprecated
+ * 3: This mode is deprecated.
+ */
+ RENDER_MODE_ADAPTIVE __deprecated = 3,
+};
+
+/**
+ * The camera video source type
+ */
+enum CAMERA_VIDEO_SOURCE_TYPE {
+ /**
+ * 0: the video frame comes from the front camera
+ */
+ CAMERA_SOURCE_FRONT = 0,
+ /**
+ * 1: the video frame comes from the back camera
+ */
+ CAMERA_SOURCE_BACK = 1,
+ /**
+ * 1: the video frame source is unsepcified
+ */
+ VIDEO_SOURCE_UNSPECIFIED = 2,
+};
+
+/**
+ * The IVideoFrameMetaInfo class.
+ * This interface provides access to metadata information.
+ */
+class IVideoFrameMetaInfo {
+ public:
+ enum META_INFO_KEY {
+ KEY_FACE_CAPTURE = 0,
+ };
+ virtual ~IVideoFrameMetaInfo() {};
+ virtual const char* getMetaInfoStr(META_INFO_KEY key) const = 0;
+};
+
+/**
+ * The definition of the ExternalVideoFrame struct.
+ */
+struct ExternalVideoFrame {
+ ExternalVideoFrame()
+ : type(VIDEO_BUFFER_RAW_DATA),
+ format(VIDEO_PIXEL_DEFAULT),
+ buffer(NULL),
+ stride(0),
+ height(0),
+ cropLeft(0),
+ cropTop(0),
+ cropRight(0),
+ cropBottom(0),
+ rotation(0),
+ timestamp(0),
+ eglContext(NULL),
+ eglType(EGL_CONTEXT10),
+ textureId(0),
+ metadata_buffer(NULL),
+ metadata_size(0),
+ alphaBuffer(NULL),
+ d3d11_texture_2d(NULL),
+ texture_slice_index(0){}
+
+ /**
+ * The EGL context type.
+ */
+ enum EGL_CONTEXT_TYPE {
+ /**
+ * 0: When using the OpenGL interface (javax.microedition.khronos.egl.*) defined by Khronos
+ */
+ EGL_CONTEXT10 = 0,
+ /**
+ * 0: When using the OpenGL interface (android.opengl.*) defined by Android
+ */
+ EGL_CONTEXT14 = 1,
+ };
+
+ /**
+ * Video buffer types.
+ */
+ enum VIDEO_BUFFER_TYPE {
+ /**
+ * 1: Raw data.
+ */
+ VIDEO_BUFFER_RAW_DATA = 1,
+ /**
+ * 2: The same as VIDEO_BUFFER_RAW_DATA.
+ */
+ VIDEO_BUFFER_ARRAY = 2,
+ /**
+ * 3: The video buffer in the format of texture.
+ */
+ VIDEO_BUFFER_TEXTURE = 3,
+ };
+
+ /**
+ * The buffer type: #VIDEO_BUFFER_TYPE.
+ */
+ VIDEO_BUFFER_TYPE type;
+ /**
+ * The pixel format: #VIDEO_PIXEL_FORMAT
+ */
+ VIDEO_PIXEL_FORMAT format;
+ /**
+ * The video buffer.
+ */
+ void* buffer;
+ /**
+ * The line spacing of the incoming video frame (px). For
+ * texture, it is the width of the texture.
+ */
+ int stride;
+ /**
+ * The height of the incoming video frame.
+ */
+ int height;
+ /**
+ * [Raw data related parameter] The number of pixels trimmed from the left. The default value is
+ * 0.
+ */
+ int cropLeft;
+ /**
+ * [Raw data related parameter] The number of pixels trimmed from the top. The default value is
+ * 0.
+ */
+ int cropTop;
+ /**
+ * [Raw data related parameter] The number of pixels trimmed from the right. The default value is
+ * 0.
+ */
+ int cropRight;
+ /**
+ * [Raw data related parameter] The number of pixels trimmed from the bottom. The default value
+ * is 0.
+ */
+ int cropBottom;
+ /**
+ * [Raw data related parameter] The clockwise rotation information of the video frame. You can set the
+ * rotation angle as 0, 90, 180, or 270. The default value is 0.
+ */
+ int rotation;
+ /**
+ * The timestamp (ms) of the incoming video frame. An incorrect timestamp results in a frame loss or
+ * unsynchronized audio and video.
+ *
+ * Please refer to getAgoraCurrentMonotonicTimeInMs or getCurrentMonotonicTimeInMs
+ * to determine how to fill this filed.
+ */
+ long long timestamp;
+ /**
+ * [Texture-related parameter]
+ * When using the OpenGL interface (javax.microedition.khronos.egl.*) defined by Khronos, set EGLContext to this field.
+ * When using the OpenGL interface (android.opengl.*) defined by Android, set EGLContext to this field.
+ */
+ void *eglContext;
+ /**
+ * [Texture related parameter] Texture ID used by the video frame.
+ */
+ EGL_CONTEXT_TYPE eglType;
+ /**
+ * [Texture related parameter] Incoming 4 × 4 transformational matrix. The typical value is a unit matrix.
+ */
+ int textureId;
+ /**
+ * [Texture related parameter] Incoming 4 × 4 transformational matrix. The typical value is a unit matrix.
+ */
+ float matrix[16];
+ /**
+ * [Texture related parameter] The MetaData buffer.
+ * The default value is NULL
+ */
+ uint8_t* metadata_buffer;
+ /**
+ * [Texture related parameter] The MetaData size.
+ * The default value is 0
+ */
+ int metadata_size;
+ /**
+ * Indicates the output data of the portrait segmentation algorithm, which is consistent with the size of the video frame.
+ * The value range of each pixel is [0,255], where 0 represents the background; 255 represents the foreground (portrait).
+ * The default value is NULL
+ */
+ uint8_t* alphaBuffer;
+
+ /**
+ * [For Windows only] The pointer of ID3D11Texture2D used by the video frame.
+ */
+ void *d3d11_texture_2d;
+
+ /**
+ * [For Windows only] The index of ID3D11Texture2D array used by the video frame.
+ */
+ int texture_slice_index;
+};
+
+/**
+ * The definition of the VideoFrame struct.
+ */
+struct VideoFrame {
+ VideoFrame():
+ type(VIDEO_PIXEL_DEFAULT),
+ width(0),
+ height(0),
+ yStride(0),
+ uStride(0),
+ vStride(0),
+ yBuffer(NULL),
+ uBuffer(NULL),
+ vBuffer(NULL),
+ rotation(0),
+ renderTimeMs(0),
+ avsync_type(0),
+ metadata_buffer(NULL),
+ metadata_size(0),
+ sharedContext(0),
+ textureId(0),
+ d3d11Texture2d(NULL),
+ alphaBuffer(NULL),
+ pixelBuffer(NULL),
+ metaInfo(NULL){
+ memset(matrix, 0, sizeof(matrix));
+ }
+ /**
+ * The video pixel format: #VIDEO_PIXEL_FORMAT.
+ */
+ VIDEO_PIXEL_FORMAT type;
+ /**
+ * The width of the video frame.
+ */
+ int width;
+ /**
+ * The height of the video frame.
+ */
+ int height;
+ /**
+ * The line span of Y buffer in the YUV data.
+ */
+ int yStride;
+ /**
+ * The line span of U buffer in the YUV data.
+ */
+ int uStride;
+ /**
+ * The line span of V buffer in the YUV data.
+ */
+ int vStride;
+ /**
+ * The pointer to the Y buffer in the YUV data.
+ */
+ uint8_t* yBuffer;
+ /**
+ * The pointer to the U buffer in the YUV data.
+ */
+ uint8_t* uBuffer;
+ /**
+ * The pointer to the V buffer in the YUV data.
+ */
+ uint8_t* vBuffer;
+ /**
+ * The clockwise rotation information of this frame. You can set it as 0, 90, 180 or 270.
+ */
+ int rotation;
+ /**
+ * The timestamp to render the video stream. Use this parameter for audio-video synchronization when
+ * rendering the video.
+ *
+ * @note This parameter is for rendering the video, not capturing the video.
+ */
+ int64_t renderTimeMs;
+ /**
+ * The type of audio-video synchronization.
+ */
+ int avsync_type;
+ /**
+ * [Texture related parameter] The MetaData buffer.
+ * The default value is NULL
+ */
+ uint8_t* metadata_buffer;
+ /**
+ * [Texture related parameter] The MetaData size.
+ * The default value is 0
+ */
+ int metadata_size;
+ /**
+ * [Texture related parameter], egl context.
+ */
+ void* sharedContext;
+ /**
+ * [Texture related parameter], Texture ID used by the video frame.
+ */
+ int textureId;
+ /**
+ * [Texture related parameter] The pointer of ID3D11Texture2D used by the video frame,for Windows only.
+ */
+ void* d3d11Texture2d;
+ /**
+ * [Texture related parameter], Incoming 4 × 4 transformational matrix.
+ */
+ float matrix[16];
+ /**
+ * Indicates the output data of the portrait segmentation algorithm, which is consistent with the size of the video frame.
+ * The value range of each pixel is [0,255], where 0 represents the background; 255 represents the foreground (portrait).
+ * The default value is NULL
+ */
+ uint8_t* alphaBuffer;
+ /**
+ *The type of CVPixelBufferRef, for iOS and macOS only.
+ */
+ void* pixelBuffer;
+ /**
+ * The pointer to IVideoFrameMetaInfo, which is the interface to get metainfo contents from VideoFrame.
+ */
+ IVideoFrameMetaInfo* metaInfo;
+};
+
+/**
+ * The IVideoFrameObserver class.
+ */
+class IVideoFrameObserver {
+ public:
+ /**
+ * Occurs each time the player receives a video frame.
+ *
+ * After registering the video frame observer,
+ * the callback occurs each time the player receives a video frame to report the detailed information of the video frame.
+ * @param frame The detailed information of the video frame. See {@link VideoFrame}.
+ */
+ virtual void onFrame(const VideoFrame* frame) = 0;
+ virtual ~IVideoFrameObserver() {}
+ virtual bool isExternal() { return true; }
+ virtual VIDEO_PIXEL_FORMAT getVideoFormatPreference() { return VIDEO_PIXEL_DEFAULT; }
+};
+
+enum MEDIA_PLAYER_SOURCE_TYPE {
+ /**
+ * The real type of media player when use MEDIA_PLAYER_SOURCE_DEFAULT is decided by the
+ * type of SDK package. It is full feature media player in full-featured SDK, or simple
+ * media player in others.
+ */
+ MEDIA_PLAYER_SOURCE_DEFAULT,
+ /**
+ * Full featured media player is designed to support more codecs and media format, which
+ * requires more package size than simple player. If you need this player enabled, you
+ * might need to download a full-featured SDK.
+ */
+ MEDIA_PLAYER_SOURCE_FULL_FEATURED,
+ /**
+ * Simple media player with limit codec supported, which requires minimal package size
+ * requirement and is enabled by default
+ */
+ MEDIA_PLAYER_SOURCE_SIMPLE,
+};
+
+enum VIDEO_MODULE_POSITION {
+ POSITION_POST_CAPTURER = 1 << 0,
+ POSITION_PRE_RENDERER = 1 << 1,
+ POSITION_PRE_ENCODER = 1 << 2,
+ POSITION_POST_CAPTURER_ORIGIN = 1 << 3,
+};
+
+} // namespace base
+
+/**
+ * The audio frame observer.
+ */
+class IAudioPcmFrameSink {
+ public:
+ /**
+ * Occurs when each time the player receives an audio frame.
+ *
+ * After registering the audio frame observer,
+ * the callback occurs when each time the player receives an audio frame,
+ * reporting the detailed information of the audio frame.
+ * @param frame The detailed information of the audio frame. See {@link AudioPcmFrame}.
+ */
+ virtual void onFrame(agora::media::base::AudioPcmFrame* frame) = 0;
+ virtual ~IAudioPcmFrameSink() {}
+};
+
+/**
+ * The IAudioFrameObserverBase class.
+ */
+class IAudioFrameObserverBase {
+ public:
+ /**
+ * Audio frame types.
+ */
+ enum AUDIO_FRAME_TYPE {
+ /**
+ * 0: 16-bit PCM.
+ */
+ FRAME_TYPE_PCM16 = 0,
+ };
+ enum { MAX_HANDLE_TIME_CNT = 10 };
+ /**
+ * The definition of the AudioFrame struct.
+ */
+ struct AudioFrame {
+ /**
+ * The audio frame type: #AUDIO_FRAME_TYPE.
+ */
+ AUDIO_FRAME_TYPE type;
+ /**
+ * The number of samples per channel in this frame.
+ */
+ int samplesPerChannel;
+ /**
+ * The number of bytes per sample: #BYTES_PER_SAMPLE
+ */
+ agora::rtc::BYTES_PER_SAMPLE bytesPerSample;
+ /**
+ * The number of audio channels (data is interleaved, if stereo).
+ * - 1: Mono.
+ * - 2: Stereo.
+ */
+ int channels;
+ /**
+ * The sample rate
+ */
+ int samplesPerSec;
+ /**
+ * The data buffer of the audio frame. When the audio frame uses a stereo channel, the data
+ * buffer is interleaved.
+ *
+ * Buffer data size: buffer = samplesPerChannel × channels × bytesPerSample.
+ */
+ void* buffer;
+ /**
+ * The timestamp to render the audio data.
+ *
+ * You can use this timestamp to restore the order of the captured audio frame, and synchronize
+ * audio and video frames in video scenarios, including scenarios where external video sources
+ * are used.
+ */
+ int64_t renderTimeMs;
+ /**
+ * A reserved parameter.
+ *
+ * You can use this presentationMs parameter to indicate the presenation milisecond timestamp,
+ * this will then filled into audio4 extension part, the remote side could use this pts in av
+ * sync process with video frame.
+ */
+ int avsync_type;
+ /**
+ * The pts timestamp of this audio frame.
+ *
+ * This timestamp is used to indicate the origin pts time of the frame, and sync with video frame by
+ * the pts time stamp
+ */
+ int64_t presentationMs;
+ /**
+ * The number of the audio track.
+ */
+ int audioTrackNumber;
+
+ AudioFrame() : type(FRAME_TYPE_PCM16),
+ samplesPerChannel(0),
+ bytesPerSample(rtc::TWO_BYTES_PER_SAMPLE),
+ channels(0),
+ samplesPerSec(0),
+ buffer(NULL),
+ renderTimeMs(0),
+ avsync_type(0),
+ presentationMs(0),
+ audioTrackNumber(0) {}
+ };
+
+ enum AUDIO_FRAME_POSITION {
+ AUDIO_FRAME_POSITION_NONE = 0x0000,
+ /** The position for observing the playback audio of all remote users after mixing
+ */
+ AUDIO_FRAME_POSITION_PLAYBACK = 0x0001,
+ /** The position for observing the recorded audio of the local user
+ */
+ AUDIO_FRAME_POSITION_RECORD = 0x0002,
+ /** The position for observing the mixed audio of the local user and all remote users
+ */
+ AUDIO_FRAME_POSITION_MIXED = 0x0004,
+ /** The position for observing the audio of a single remote user before mixing
+ */
+ AUDIO_FRAME_POSITION_BEFORE_MIXING = 0x0008,
+ /** The position for observing the ear monitoring audio of the local user
+ */
+ AUDIO_FRAME_POSITION_EAR_MONITORING = 0x0010,
+ };
+
+ struct AudioParams {
+ /** The audio sample rate (Hz), which can be set as one of the following values:
+
+ - `8000`
+ - `16000` (Default)
+ - `32000`
+ - `44100 `
+ - `48000`
+ */
+ int sample_rate;
+
+ /* The number of audio channels, which can be set as either of the following values:
+
+ - `1`: Mono (Default)
+ - `2`: Stereo
+ */
+ int channels;
+
+ /* The use mode of the audio data. See AgoraAudioRawFrameOperationMode.
+ */
+ rtc::RAW_AUDIO_FRAME_OP_MODE_TYPE mode;
+
+ /** The number of samples. For example, set it as 1024 for RTMP or RTMPS
+ streaming.
+ */
+ int samples_per_call;
+
+ AudioParams() : sample_rate(0), channels(0), mode(rtc::RAW_AUDIO_FRAME_OP_MODE_READ_ONLY), samples_per_call(0) {}
+ AudioParams(int samplerate, int channel, rtc::RAW_AUDIO_FRAME_OP_MODE_TYPE type, int samplesPerCall) : sample_rate(samplerate), channels(channel), mode(type), samples_per_call(samplesPerCall) {}
+ };
+
+ public:
+ virtual ~IAudioFrameObserverBase() {}
+
+ /**
+ * Occurs when the recorded audio frame is received.
+ * @param channelId The channel name
+ * @param audioFrame The reference to the audio frame: AudioFrame.
+ * @return
+ * - true: The recorded audio frame is valid and is encoded and sent.
+ * - false: The recorded audio frame is invalid and is not encoded or sent.
+ */
+ virtual bool onRecordAudioFrame(const char* channelId, AudioFrame& audioFrame) = 0;
+ /**
+ * Occurs when the playback audio frame is received.
+ * @param channelId The channel name
+ * @param audioFrame The reference to the audio frame: AudioFrame.
+ * @return
+ * - true: The playback audio frame is valid and is encoded and sent.
+ * - false: The playback audio frame is invalid and is not encoded or sent.
+ */
+ virtual bool onPlaybackAudioFrame(const char* channelId, AudioFrame& audioFrame) = 0;
+ /**
+ * Occurs when the mixed audio data is received.
+ * @param channelId The channel name
+ * @param audioFrame The reference to the audio frame: AudioFrame.
+ * @return
+ * - true: The mixed audio data is valid and is encoded and sent.
+ * - false: The mixed audio data is invalid and is not encoded or sent.
+ */
+ virtual bool onMixedAudioFrame(const char* channelId, AudioFrame& audioFrame) = 0;
+ /**
+ * Occurs when the ear monitoring audio frame is received.
+ * @param audioFrame The reference to the audio frame: AudioFrame.
+ * @return
+ * - true: The ear monitoring audio data is valid and is encoded and sent.
+ * - false: The ear monitoring audio data is invalid and is not encoded or sent.
+ */
+ virtual bool onEarMonitoringAudioFrame(AudioFrame& audioFrame) = 0;
+ /**
+ * Occurs when the before-mixing playback audio frame is received.
+ * @param channelId The channel name
+ * @param userId ID of the remote user.
+ * @param audioFrame The reference to the audio frame: AudioFrame.
+ * @return
+ * - true: The before-mixing playback audio frame is valid and is encoded and sent.
+ * - false: The before-mixing playback audio frame is invalid and is not encoded or sent.
+ */
+ virtual bool onPlaybackAudioFrameBeforeMixing(const char* channelId, base::user_id_t userId, AudioFrame& audioFrame) {
+ (void) channelId;
+ (void) userId;
+ (void) audioFrame;
+ return true;
+ }
+
+ /**
+ * Sets the frame position for the audio observer.
+ * @return A bit mask that controls the frame position of the audio observer.
+ * @note - Use '|' (the OR operator) to observe multiple frame positions.
+ *
+ * After you successfully register the audio observer, the SDK triggers this callback each time it receives a audio frame. You can determine which position to observe by setting the return value.
+ * The SDK provides 4 positions for observer. Each position corresponds to a callback function:
+ * - `AUDIO_FRAME_POSITION_PLAYBACK (1 << 0)`: The position for playback audio frame is received, which corresponds to the \ref onPlaybackFrame "onPlaybackFrame" callback.
+ * - `AUDIO_FRAME_POSITION_RECORD (1 << 1)`: The position for record audio frame is received, which corresponds to the \ref onRecordFrame "onRecordFrame" callback.
+ * - `AUDIO_FRAME_POSITION_MIXED (1 << 2)`: The position for mixed audio frame is received, which corresponds to the \ref onMixedFrame "onMixedFrame" callback.
+ * - `AUDIO_FRAME_POSITION_BEFORE_MIXING (1 << 3)`: The position for playback audio frame before mixing is received, which corresponds to the \ref onPlaybackFrameBeforeMixing "onPlaybackFrameBeforeMixing" callback.
+ * @return The bit mask that controls the audio observation positions.
+ * See AUDIO_FRAME_POSITION.
+ */
+
+ virtual int getObservedAudioFramePosition() = 0;
+
+ /** Sets the audio playback format
+ **Note**:
+
+ - The SDK calculates the sample interval according to the `AudioParams`
+ you set in the return value of this callback and triggers the
+ `onPlaybackAudioFrame` callback at the calculated sample interval.
+ Sample interval (seconds) = `samplesPerCall`/(`sampleRate` × `channel`).
+ Ensure that the value of sample interval is equal to or greater than 0.01.
+
+ @return Sets the audio format. See AgoraAudioParams.
+ */
+ virtual AudioParams getPlaybackAudioParams() = 0;
+
+ /** Sets the audio recording format
+ **Note**:
+ - The SDK calculates the sample interval according to the `AudioParams`
+ you set in the return value of this callback and triggers the
+ `onRecordAudioFrame` callback at the calculated sample interval.
+ Sample interval (seconds) = `samplesPerCall`/(`sampleRate` × `channel`).
+ Ensure that the value of sample interval is equal to or greater than 0.01.
+
+ @return Sets the audio format. See AgoraAudioParams.
+ */
+ virtual AudioParams getRecordAudioParams() = 0;
+
+ /** Sets the audio mixing format
+ **Note**:
+ - The SDK calculates the sample interval according to the `AudioParams`
+ you set in the return value of this callback and triggers the
+ `onMixedAudioFrame` callback at the calculated sample interval.
+ Sample interval (seconds) = `samplesPerCall`/(`sampleRate` × `channel`).
+ Ensure that the value of sample interval is equal to or greater than 0.01.
+
+ @return Sets the audio format. See AgoraAudioParams.
+ */
+ virtual AudioParams getMixedAudioParams() = 0;
+
+ /** Sets the ear monitoring audio format
+ **Note**:
+ - The SDK calculates the sample interval according to the `AudioParams`
+ you set in the return value of this callback and triggers the
+ `onEarMonitoringAudioFrame` callback at the calculated sample interval.
+ Sample interval (seconds) = `samplesPerCall`/(`sampleRate` × `channel`).
+ Ensure that the value of sample interval is equal to or greater than 0.01.
+
+ @return Sets the audio format. See AgoraAudioParams.
+ */
+ virtual AudioParams getEarMonitoringAudioParams() = 0;
+};
+
+/**
+ * The IAudioFrameObserver class.
+ */
+class IAudioFrameObserver : public IAudioFrameObserverBase {
+ public:
+ using IAudioFrameObserverBase::onPlaybackAudioFrameBeforeMixing;
+ /**
+ * Occurs when the before-mixing playback audio frame is received.
+ * @param channelId The channel name
+ * @param uid ID of the remote user.
+ * @param audioFrame The reference to the audio frame: AudioFrame.
+ * @return
+ * - true: The before-mixing playback audio frame is valid and is encoded and sent.
+ * - false: The before-mixing playback audio frame is invalid and is not encoded or sent.
+ */
+ virtual bool onPlaybackAudioFrameBeforeMixing(const char* channelId, rtc::uid_t uid, AudioFrame& audioFrame) = 0;
+};
+
+struct AudioSpectrumData {
+ /**
+ * The audio spectrum data of audio.
+ */
+ const float *audioSpectrumData;
+ /**
+ * The data length of audio spectrum data.
+ */
+ int dataLength;
+
+ AudioSpectrumData() : audioSpectrumData(NULL), dataLength(0) {}
+ AudioSpectrumData(const float *data, int length) :
+ audioSpectrumData(data), dataLength(length) {}
+};
+
+struct UserAudioSpectrumInfo {
+ /**
+ * User ID of the speaker.
+ */
+ agora::rtc::uid_t uid;
+ /**
+ * The audio spectrum data of audio.
+ */
+ struct AudioSpectrumData spectrumData;
+
+ UserAudioSpectrumInfo() : uid(0) {}
+
+ UserAudioSpectrumInfo(agora::rtc::uid_t uid, const float* data, int length) : uid(uid), spectrumData(data, length) {}
+};
+
+/**
+ * The IAudioSpectrumObserver class.
+ */
+class IAudioSpectrumObserver {
+public:
+ virtual ~IAudioSpectrumObserver() {}
+
+ /**
+ * Reports the audio spectrum of local audio.
+ *
+ * This callback reports the audio spectrum data of the local audio at the moment
+ * in the channel.
+ *
+ * You can set the time interval of this callback using \ref ILocalUser::enableAudioSpectrumMonitor "enableAudioSpectrumMonitor".
+ *
+ * @param data The audio spectrum data of local audio.
+ * - true: Processed.
+ * - false: Not processed.
+ */
+ virtual bool onLocalAudioSpectrum(const AudioSpectrumData& data) = 0;
+ /**
+ * Reports the audio spectrum of remote user.
+ *
+ * This callback reports the IDs and audio spectrum data of the loudest speakers at the moment
+ * in the channel.
+ *
+ * You can set the time interval of this callback using \ref ILocalUser::enableAudioSpectrumMonitor "enableAudioSpectrumMonitor".
+ *
+ * @param spectrums The pointer to \ref agora::media::UserAudioSpectrumInfo "UserAudioSpectrumInfo", which is an array containing
+ * the user ID and audio spectrum data for each speaker.
+ * - This array contains the following members:
+ * - `uid`, which is the UID of each remote speaker
+ * - `spectrumData`, which reports the audio spectrum of each remote speaker.
+ * @param spectrumNumber The array length of the spectrums.
+ * - true: Processed.
+ * - false: Not processed.
+ */
+ virtual bool onRemoteAudioSpectrum(const UserAudioSpectrumInfo* spectrums, unsigned int spectrumNumber) = 0;
+};
+
+/**
+ * The IVideoEncodedFrameObserver class.
+ */
+class IVideoEncodedFrameObserver {
+ public:
+ /**
+ * Occurs each time the SDK receives an encoded video image.
+ * @param uid The user id of remote user.
+ * @param imageBuffer The pointer to the video image buffer.
+ * @param length The data length of the video image.
+ * @param videoEncodedFrameInfo The information of the encoded video frame: EncodedVideoFrameInfo.
+ * @return Determines whether to accept encoded video image.
+ * - true: Accept.
+ * - false: Do not accept.
+ */
+ virtual bool onEncodedVideoFrameReceived(rtc::uid_t uid, const uint8_t* imageBuffer, size_t length,
+ const rtc::EncodedVideoFrameInfo& videoEncodedFrameInfo) = 0;
+
+ virtual ~IVideoEncodedFrameObserver() {}
+};
+
+/**
+ * The IVideoFrameObserver class.
+ */
+class IVideoFrameObserver {
+ public:
+ typedef media::base::VideoFrame VideoFrame;
+ /**
+ * The process mode of the video frame:
+ */
+ enum VIDEO_FRAME_PROCESS_MODE {
+ /**
+ * Read-only mode.
+ *
+ * In this mode, you do not modify the video frame. The video frame observer is a renderer.
+ */
+ PROCESS_MODE_READ_ONLY, // Observer works as a pure renderer and will not modify the original frame.
+ /**
+ * Read and write mode.
+ *
+ * In this mode, you modify the video frame. The video frame observer is a video filter.
+ */
+ PROCESS_MODE_READ_WRITE, // Observer works as a filter that will process the video frame and affect the following frame processing in SDK.
+ };
+
+ public:
+ virtual ~IVideoFrameObserver() {}
+
+ /**
+ * Occurs each time the SDK receives a video frame captured by the local camera.
+ *
+ * After you successfully register the video frame observer, the SDK triggers this callback each time
+ * a video frame is received. In this callback, you can get the video data captured by the local
+ * camera. You can then pre-process the data according to your scenarios.
+ *
+ * After pre-processing, you can send the processed video data back to the SDK by setting the
+ * `videoFrame` parameter in this callback.
+ *
+ * @note
+ * - If you get the video data in RGBA color encoding format, Agora does not support using this callback to send the processed data in RGBA color encoding format back to the SDK.
+ * - The video data that this callback gets has not been pre-processed, such as watermarking, cropping content, rotating, or image enhancement.
+ *
+ * @param videoFrame A pointer to the video frame: VideoFrame
+ * @param sourceType source type of video frame. See #VIDEO_SOURCE_TYPE.
+ * @return Determines whether to ignore the current video frame if the pre-processing fails:
+ * - true: Do not ignore.
+ * - false: Ignore, in which case this method does not sent the current video frame to the SDK.
+ */
+ virtual bool onCaptureVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE sourceType, VideoFrame& videoFrame) = 0;
+
+ /**
+ * Occurs each time the SDK receives a video frame before encoding.
+ *
+ * After you successfully register the video frame observer, the SDK triggers this callback each time
+ * when it receives a video frame. In this callback, you can get the video data before encoding. You can then
+ * process the data according to your particular scenarios.
+ *
+ * After processing, you can send the processed video data back to the SDK by setting the
+ * `videoFrame` parameter in this callback.
+ *
+ * @note
+ * - To get the video data captured from the second screen before encoding, you need to set (1 << 2) as a frame position through `getObservedFramePosition`.
+ * - The video data that this callback gets has been pre-processed, such as watermarking, cropping content, rotating, or image enhancement.
+ * - This callback does not support sending processed RGBA video data back to the SDK.
+ *
+ * @param videoFrame A pointer to the video frame: VideoFrame
+ * @param sourceType source type of video frame. See #VIDEO_SOURCE_TYPE.
+ * @return Determines whether to ignore the current video frame if the pre-processing fails:
+ * - true: Do not ignore.
+ * - false: Ignore, in which case this method does not sent the current video frame to the SDK.
+ */
+ virtual bool onPreEncodeVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE sourceType, VideoFrame& videoFrame) = 0;
+
+ /**
+ * Occurs each time the SDK receives a video frame decoded by the MediaPlayer.
+ *
+ * After you successfully register the video frame observer, the SDK triggers this callback each
+ * time a video frame is decoded. In this callback, you can get the video data decoded by the
+ * MediaPlayer. You can then pre-process the data according to your scenarios.
+ *
+ * After pre-processing, you can send the processed video data back to the SDK by setting the
+ * `videoFrame` parameter in this callback.
+ *
+ * @note
+ * - This callback will not be affected by the return values of \ref getVideoFrameProcessMode "getVideoFrameProcessMode", \ref getRotationApplied "getRotationApplied", \ref getMirrorApplied "getMirrorApplied", \ref getObservedFramePosition "getObservedFramePosition".
+ * - On Android, this callback is not affected by the return value of \ref getVideoFormatPreference "getVideoFormatPreference"
+ *
+ * @param videoFrame A pointer to the video frame: VideoFrame
+ * @param mediaPlayerId ID of the mediaPlayer.
+ * @return Determines whether to ignore the current video frame if the pre-processing fails:
+ * - true: Do not ignore.
+ * - false: Ignore, in which case this method does not sent the current video frame to the SDK.
+ */
+ virtual bool onMediaPlayerVideoFrame(VideoFrame& videoFrame, int mediaPlayerId) = 0;
+
+ /**
+ * Occurs each time the SDK receives a video frame sent by the remote user.
+ *
+ * After you successfully register the video frame observer, the SDK triggers this callback each time a
+ * video frame is received. In this callback, you can get the video data sent by the remote user. You
+ * can then post-process the data according to your scenarios.
+ *
+ * After post-processing, you can send the processed data back to the SDK by setting the `videoFrame`
+ * parameter in this callback.
+ *
+ * @note This callback does not support sending processed RGBA video data back to the SDK.
+ *
+ * @param channelId The channel name
+ * @param remoteUid ID of the remote user who sends the current video frame.
+ * @param videoFrame A pointer to the video frame: VideoFrame
+ * @return Determines whether to ignore the current video frame if the post-processing fails:
+ * - true: Do not ignore.
+ * - false: Ignore, in which case this method does not sent the current video frame to the SDK.
+ */
+ virtual bool onRenderVideoFrame(const char* channelId, rtc::uid_t remoteUid, VideoFrame& videoFrame) = 0;
+
+ virtual bool onTranscodedVideoFrame(VideoFrame& videoFrame) = 0;
+
+ /**
+ * Occurs each time the SDK receives a video frame and prompts you to set the process mode of the video frame.
+ *
+ * After you successfully register the video frame observer, the SDK triggers this callback each time it receives
+ * a video frame. You need to set your preferred process mode in the return value of this callback.
+ * @return VIDEO_FRAME_PROCESS_MODE.
+ */
+ virtual VIDEO_FRAME_PROCESS_MODE getVideoFrameProcessMode() {
+ return PROCESS_MODE_READ_ONLY;
+ }
+
+ /**
+ * Sets the format of the raw video data output by the SDK.
+ *
+ * If you want to get raw video data in a color encoding format other than YUV 420, register this callback when
+ * calling `registerVideoFrameObserver`. After you successfully register the video frame observer, the SDK triggers
+ * this callback each time it receives a video frame. You need to set your preferred video data in the return value
+ * of this callback.
+ *
+ * @note If you want the video captured by the sender to be the original format, set the original video data format
+ * to VIDEO_PIXEL_DEFAULT in the return value. On different platforms, the original video pixel format is also
+ * different, for the actual video pixel format, see `VideoFrame`.
+ *
+ * @return Sets the video format. See VIDEO_PIXEL_FORMAT.
+ */
+ virtual base::VIDEO_PIXEL_FORMAT getVideoFormatPreference() { return base::VIDEO_PIXEL_DEFAULT; }
+
+ /**
+ * Occurs each time the SDK receives a video frame, and prompts you whether to rotate the captured video.
+ *
+ * If you want to rotate the captured video according to the rotation member in the `VideoFrame` class, register this
+ * callback by calling `registerVideoFrameObserver`. After you successfully register the video frame observer, the
+ * SDK triggers this callback each time it receives a video frame. You need to set whether to rotate the video frame
+ * in the return value of this callback.
+ *
+ * @note This function only supports video data in RGBA or YUV420.
+ *
+ * @return Determines whether to rotate.
+ * - `true`: Rotate the captured video.
+ * - `false`: (Default) Do not rotate the captured video.
+ */
+ virtual bool getRotationApplied() { return false; }
+
+ /**
+ * Occurs each time the SDK receives a video frame and prompts you whether or not to mirror the captured video.
+ *
+ * If the video data you want to obtain is a mirror image of the original video, you need to register this callback
+ * when calling `registerVideoFrameObserver`. After you successfully register the video frame observer, the SDK
+ * triggers this callback each time it receives a video frame. You need to set whether or not to mirror the video
+ * frame in the return value of this callback.
+ *
+ * @note This function only supports video data in RGBA and YUV420 formats.
+ *
+ * @return Determines whether to mirror.
+ * - `true`: Mirror the captured video.
+ * - `false`: (Default) Do not mirror the captured video.
+ */
+ virtual bool getMirrorApplied() { return false; }
+
+ /**
+ * Sets the frame position for the video observer.
+ *
+ * After you successfully register the video observer, the SDK triggers this callback each time it receives
+ * a video frame. You can determine which position to observe by setting the return value. The SDK provides
+ * 3 positions for observer. Each position corresponds to a callback function:
+ *
+ * POSITION_POST_CAPTURER(1 << 0): The position after capturing the video data, which corresponds to the onCaptureVideoFrame callback.
+ * POSITION_PRE_RENDERER(1 << 1): The position before receiving the remote video data, which corresponds to the onRenderVideoFrame callback.
+ * POSITION_PRE_ENCODER(1 << 2): The position before encoding the video data, which corresponds to the onPreEncodeVideoFrame callback.
+ *
+ * To observe multiple frame positions, use '|' (the OR operator).
+ * This callback observes POSITION_POST_CAPTURER(1 << 0) and POSITION_PRE_RENDERER(1 << 1) by default.
+ * To conserve the system consumption, you can reduce the number of frame positions that you want to observe.
+ *
+ * @return A bit mask that controls the frame position of the video observer: VIDEO_OBSERVER_POSITION.
+ */
+ virtual uint32_t getObservedFramePosition() {
+ return base::POSITION_POST_CAPTURER | base::POSITION_PRE_RENDERER;
+ }
+
+ /**
+ * Indicate if the observer is for internal use.
+ * Note: Never override this function
+ * @return
+ * - true: the observer is for external use
+ * - false: the observer is for internal use
+ */
+ virtual bool isExternal() { return true; }
+};
+
+/**
+ * The external video source type.
+ */
+enum EXTERNAL_VIDEO_SOURCE_TYPE {
+ /**
+ * 0: non-encoded video frame.
+ */
+ VIDEO_FRAME = 0,
+ /**
+ * 1: encoded video frame.
+ */
+ ENCODED_VIDEO_FRAME,
+};
+
+/**
+ * The format of the recording file.
+ *
+ * @since v3.5.2
+ */
+enum MediaRecorderContainerFormat {
+ /**
+ * 1: (Default) MP4.
+ */
+ FORMAT_MP4 = 1,
+};
+/**
+ * The recording content.
+ *
+ * @since v3.5.2
+ */
+enum MediaRecorderStreamType {
+ /**
+ * Only audio.
+ */
+ STREAM_TYPE_AUDIO = 0x01,
+ /**
+ * Only video.
+ */
+ STREAM_TYPE_VIDEO = 0x02,
+ /**
+ * (Default) Audio and video.
+ */
+ STREAM_TYPE_BOTH = STREAM_TYPE_AUDIO | STREAM_TYPE_VIDEO,
+};
+/**
+ * The current recording state.
+ *
+ * @since v3.5.2
+ */
+enum RecorderState {
+ /**
+ * -1: An error occurs during the recording. See RecorderReasonCode for the reason.
+ */
+ RECORDER_STATE_ERROR = -1,
+ /**
+ * 2: The audio and video recording is started.
+ */
+ RECORDER_STATE_START = 2,
+ /**
+ * 3: The audio and video recording is stopped.
+ */
+ RECORDER_STATE_STOP = 3,
+};
+/**
+ * The reason for the state change
+ *
+ * @since v3.5.2
+ */
+enum RecorderReasonCode {
+ /**
+ * 0: No error occurs.
+ */
+ RECORDER_REASON_NONE = 0,
+ /**
+ * 1: The SDK fails to write the recorded data to a file.
+ */
+ RECORDER_REASON_WRITE_FAILED = 1,
+ /**
+ * 2: The SDK does not detect audio and video streams to be recorded, or audio and video streams are interrupted for more than five seconds during recording.
+ */
+ RECORDER_REASON_NO_STREAM = 2,
+ /**
+ * 3: The recording duration exceeds the upper limit.
+ */
+ RECORDER_REASON_OVER_MAX_DURATION = 3,
+ /**
+ * 4: The recording configuration changes.
+ */
+ RECORDER_REASON_CONFIG_CHANGED = 4,
+};
+/**
+ * Configurations for the local audio and video recording.
+ *
+ * @since v3.5.2
+ */
+struct MediaRecorderConfiguration {
+ /**
+ * The absolute path (including the filename extensions) of the recording file.
+ * For example, `C:\Users\\AppData\Local\Agora\\example.mp4` on Windows,
+ * `/App Sandbox/Library/Caches/example.mp4` on iOS, `/Library/Logs/example.mp4` on macOS, and
+ * `/storage/emulated/0/Android/data//files/example.mp4` on Android.
+ *
+ * @note Ensure that the specified path exists and is writable.
+ */
+ const char* storagePath;
+ /**
+ * The format of the recording file. See \ref agora::rtc::MediaRecorderContainerFormat "MediaRecorderContainerFormat".
+ */
+ MediaRecorderContainerFormat containerFormat;
+ /**
+ * The recording content. See \ref agora::rtc::MediaRecorderStreamType "MediaRecorderStreamType".
+ */
+ MediaRecorderStreamType streamType;
+ /**
+ * The maximum recording duration, in milliseconds. The default value is 120000.
+ */
+ int maxDurationMs;
+ /**
+ * The interval (ms) of updating the recording information. The value range is
+ * [1000,10000]. Based on the set value of `recorderInfoUpdateInterval`, the
+ * SDK triggers the \ref IMediaRecorderObserver::onRecorderInfoUpdated "onRecorderInfoUpdated"
+ * callback to report the updated recording information.
+ */
+ int recorderInfoUpdateInterval;
+
+ MediaRecorderConfiguration() : storagePath(NULL), containerFormat(FORMAT_MP4), streamType(STREAM_TYPE_BOTH), maxDurationMs(120000), recorderInfoUpdateInterval(0) {}
+ MediaRecorderConfiguration(const char* path, MediaRecorderContainerFormat format, MediaRecorderStreamType type, int duration, int interval) : storagePath(path), containerFormat(format), streamType(type), maxDurationMs(duration), recorderInfoUpdateInterval(interval) {}
+};
+/**
+ * Information for the recording file.
+ *
+ * @since v3.5.2
+ */
+struct RecorderInfo {
+ /**
+ * The absolute path of the recording file.
+ */
+ const char* fileName;
+ /**
+ * The recording duration, in milliseconds.
+ */
+ unsigned int durationMs;
+ /**
+ * The size in bytes of the recording file.
+ */
+ unsigned int fileSize;
+
+ RecorderInfo() : fileName(NULL), durationMs(0), fileSize(0) {}
+ RecorderInfo(const char* name, unsigned int dur, unsigned int size) : fileName(name), durationMs(dur), fileSize(size) {}
+};
+
+class IMediaRecorderObserver {
+ public:
+ /**
+ * Occurs when the recording state changes.
+ *
+ * @since v4.0.0
+ *
+ * When the local audio and video recording state changes, the SDK triggers this callback to report the current
+ * recording state and the reason for the change.
+ *
+ * @param channelId The channel name.
+ * @param uid ID of the user.
+ * @param state The current recording state. See \ref agora::media::RecorderState "RecorderState".
+ * @param reason The reason for the state change. See \ref agora::media::RecorderReasonCode "RecorderReasonCode".
+ */
+ virtual void onRecorderStateChanged(const char* channelId, rtc::uid_t uid, RecorderState state, RecorderReasonCode reason) = 0;
+ /**
+ * Occurs when the recording information is updated.
+ *
+ * @since v4.0.0
+ *
+ * After you successfully register this callback and enable the local audio and video recording, the SDK periodically triggers
+ * the `onRecorderInfoUpdated` callback based on the set value of `recorderInfoUpdateInterval`. This callback reports the
+ * filename, duration, and size of the current recording file.
+ *
+ * @param channelId The channel name.
+ * @param uid ID of the user.
+ * @param info Information about the recording file. See \ref agora::media::RecorderInfo "RecorderInfo".
+ *
+ */
+ virtual void onRecorderInfoUpdated(const char* channelId, rtc::uid_t uid, const RecorderInfo& info) = 0;
+
+ virtual ~IMediaRecorderObserver() {}
+};
+
+} // namespace media
+} // namespace agora
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h
new file mode 100644
index 000000000..3beaba788
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h
@@ -0,0 +1,516 @@
+//
+// Agora Engine SDK
+//
+// Created by Sting Feng in 2020-05.
+// Copyright (c) 2017 Agora.io. All rights reserved.
+
+#pragma once // NOLINT(build/header_guard)
+
+#include
+#include
+
+#include "AgoraOptional.h"
+
+/**
+ * set analyze duration for real time stream
+ * @example "setPlayerOption(KEY_PLAYER_REAL_TIME_STREAM_ANALYZE_DURATION,1000000)"
+ */
+#define KEY_PLAYER_REAL_TIME_STREAM_ANALYZE_DURATION "analyze_duration"
+
+/**
+ * make the player to enable audio or not
+ * @example "setPlayerOption(KEY_PLAYER_ENABLE_AUDIO,0)"
+ */
+#define KEY_PLAYER_ENABLE_AUDIO "enable_audio"
+
+/**
+ * make the player to enable video or not
+ * @example "setPlayerOption(KEY_PLAYER_ENABLE_VIDEO,0)"
+ */
+#define KEY_PLAYER_ENABLE_VIDEO "enable_video"
+
+/**
+ * set the player enable to search metadata
+ * @example "setPlayerOption(KEY_PLAYER_DISABLE_SEARCH_METADATA,0)"
+ */
+#define KEY_PLAYER_ENABLE_SEARCH_METADATA "enable_search_metadata"
+
+/**
+ * set the player sei filter type
+ * @example "setPlayerOption(KEY_PLAYER_SEI_FILTER_TYPE,"5")"
+ */
+#define KEY_PLAYER_SEI_FILTER_TYPE "set_sei_filter_type"
+
+namespace agora {
+
+namespace media {
+
+namespace base {
+static const uint8_t kMaxCharBufferLength = 50;
+/**
+ * @brief The playback state.
+ *
+ */
+enum MEDIA_PLAYER_STATE {
+ /** Default state.
+ */
+ PLAYER_STATE_IDLE = 0,
+ /** Opening the media file.
+ */
+ PLAYER_STATE_OPENING,
+ /** The media file is opened successfully.
+ */
+ PLAYER_STATE_OPEN_COMPLETED,
+ /** Playing the media file.
+ */
+ PLAYER_STATE_PLAYING,
+ /** The playback is paused.
+ */
+ PLAYER_STATE_PAUSED,
+ /** The playback is completed.
+ */
+ PLAYER_STATE_PLAYBACK_COMPLETED,
+ /** All loops are completed.
+ */
+ PLAYER_STATE_PLAYBACK_ALL_LOOPS_COMPLETED,
+ /** The playback is stopped.
+ */
+ PLAYER_STATE_STOPPED,
+ /** Player pausing (internal)
+ */
+ PLAYER_STATE_PAUSING_INTERNAL = 50,
+ /** Player stopping (internal)
+ */
+ PLAYER_STATE_STOPPING_INTERNAL,
+ /** Player seeking state (internal)
+ */
+ PLAYER_STATE_SEEKING_INTERNAL,
+ /** Player getting state (internal)
+ */
+ PLAYER_STATE_GETTING_INTERNAL,
+ /** None state for state machine (internal)
+ */
+ PLAYER_STATE_NONE_INTERNAL,
+ /** Do nothing state for state machine (internal)
+ */
+ PLAYER_STATE_DO_NOTHING_INTERNAL,
+ /** Player set track state (internal)
+ */
+ PLAYER_STATE_SET_TRACK_INTERNAL,
+ /** The playback fails.
+ */
+ PLAYER_STATE_FAILED = 100,
+};
+/**
+ * @brief Player error code
+ *
+ */
+enum MEDIA_PLAYER_REASON {
+ /** No error.
+ */
+ PLAYER_REASON_NONE = 0,
+ /** The parameter is invalid.
+ */
+ PLAYER_REASON_INVALID_ARGUMENTS = -1,
+ /** Internel error.
+ */
+ PLAYER_REASON_INTERNAL = -2,
+ /** No resource.
+ */
+ PLAYER_REASON_NO_RESOURCE = -3,
+ /** Invalid media source.
+ */
+ PLAYER_REASON_INVALID_MEDIA_SOURCE = -4,
+ /** The type of the media stream is unknown.
+ */
+ PLAYER_REASON_UNKNOWN_STREAM_TYPE = -5,
+ /** The object is not initialized.
+ */
+ PLAYER_REASON_OBJ_NOT_INITIALIZED = -6,
+ /** The codec is not supported.
+ */
+ PLAYER_REASON_CODEC_NOT_SUPPORTED = -7,
+ /** Invalid renderer.
+ */
+ PLAYER_REASON_VIDEO_RENDER_FAILED = -8,
+ /** An error occurs in the internal state of the player.
+ */
+ PLAYER_REASON_INVALID_STATE = -9,
+ /** The URL of the media file cannot be found.
+ */
+ PLAYER_REASON_URL_NOT_FOUND = -10,
+ /** Invalid connection between the player and the Agora server.
+ */
+ PLAYER_REASON_INVALID_CONNECTION_STATE = -11,
+ /** The playback buffer is insufficient.
+ */
+ PLAYER_REASON_SRC_BUFFER_UNDERFLOW = -12,
+ /** The audio mixing file playback is interrupted.
+ */
+ PLAYER_REASON_INTERRUPTED = -13,
+ /** The SDK does not support this function.
+ */
+ PLAYER_REASON_NOT_SUPPORTED = -14,
+ /** The token has expired.
+ */
+ PLAYER_REASON_TOKEN_EXPIRED = -15,
+ /** The ip has expired.
+ */
+ PLAYER_REASON_IP_EXPIRED = -16,
+ /** An unknown error occurs.
+ */
+ PLAYER_REASON_UNKNOWN = -17,
+};
+
+/**
+ * @brief The type of the media stream.
+ *
+ */
+enum MEDIA_STREAM_TYPE {
+ /** The type is unknown.
+ */
+ STREAM_TYPE_UNKNOWN = 0,
+ /** The video stream.
+ */
+ STREAM_TYPE_VIDEO = 1,
+ /** The audio stream.
+ */
+ STREAM_TYPE_AUDIO = 2,
+ /** The subtitle stream.
+ */
+ STREAM_TYPE_SUBTITLE = 3,
+};
+
+/**
+ * @brief The playback event.
+ *
+ */
+enum MEDIA_PLAYER_EVENT {
+ /** The player begins to seek to the new playback position.
+ */
+ PLAYER_EVENT_SEEK_BEGIN = 0,
+ /** The seek operation completes.
+ */
+ PLAYER_EVENT_SEEK_COMPLETE = 1,
+ /** An error occurs during the seek operation.
+ */
+ PLAYER_EVENT_SEEK_ERROR = 2,
+ /** The player changes the audio track for playback.
+ */
+ PLAYER_EVENT_AUDIO_TRACK_CHANGED = 5,
+ /** player buffer low
+ */
+ PLAYER_EVENT_BUFFER_LOW = 6,
+ /** player buffer recover
+ */
+ PLAYER_EVENT_BUFFER_RECOVER = 7,
+ /** The video or audio is interrupted
+ */
+ PLAYER_EVENT_FREEZE_START = 8,
+ /** Interrupt at the end of the video or audio
+ */
+ PLAYER_EVENT_FREEZE_STOP = 9,
+ /** switch source begin
+ */
+ PLAYER_EVENT_SWITCH_BEGIN = 10,
+ /** switch source complete
+ */
+ PLAYER_EVENT_SWITCH_COMPLETE = 11,
+ /** switch source error
+ */
+ PLAYER_EVENT_SWITCH_ERROR = 12,
+ /** An application can render the video to less than a second
+ */
+ PLAYER_EVENT_FIRST_DISPLAYED = 13,
+ /** cache resources exceed the maximum file count
+ */
+ PLAYER_EVENT_REACH_CACHE_FILE_MAX_COUNT = 14,
+ /** cache resources exceed the maximum file size
+ */
+ PLAYER_EVENT_REACH_CACHE_FILE_MAX_SIZE = 15,
+ /** Triggered when a retry is required to open the media
+ */
+ PLAYER_EVENT_TRY_OPEN_START = 16,
+ /** Triggered when the retry to open the media is successful
+ */
+ PLAYER_EVENT_TRY_OPEN_SUCCEED = 17,
+ /** Triggered when retrying to open media fails
+ */
+ PLAYER_EVENT_TRY_OPEN_FAILED = 18,
+};
+
+/**
+ * @brief The play preload another source event.
+ *
+ */
+enum PLAYER_PRELOAD_EVENT {
+ /** preload source begin
+ */
+ PLAYER_PRELOAD_EVENT_BEGIN = 0,
+ /** preload source complete
+ */
+ PLAYER_PRELOAD_EVENT_COMPLETE = 1,
+ /** preload source error
+ */
+ PLAYER_PRELOAD_EVENT_ERROR = 2,
+};
+
+/**
+ * @brief The information of the media stream object.
+ *
+ */
+struct PlayerStreamInfo {
+ /** The index of the media stream. */
+ int streamIndex;
+
+ /** The type of the media stream. See {@link MEDIA_STREAM_TYPE}. */
+ MEDIA_STREAM_TYPE streamType;
+
+ /** The codec of the media stream. */
+ char codecName[kMaxCharBufferLength];
+
+ /** The language of the media stream. */
+ char language[kMaxCharBufferLength];
+
+ /** The frame rate (fps) if the stream is video. */
+ int videoFrameRate;
+
+ /** The video bitrate (bps) if the stream is video. */
+ int videoBitRate;
+
+ /** The video width (pixel) if the stream is video. */
+ int videoWidth;
+
+ /** The video height (pixel) if the stream is video. */
+ int videoHeight;
+
+ /** The rotation angle if the steam is video. */
+ int videoRotation;
+
+ /** The sample rate if the stream is audio. */
+ int audioSampleRate;
+
+ /** The number of audio channels if the stream is audio. */
+ int audioChannels;
+
+ /** The number of bits per sample if the stream is audio. */
+ int audioBitsPerSample;
+
+ /** The total duration (millisecond) of the media stream. */
+ int64_t duration;
+
+ PlayerStreamInfo() : streamIndex(0),
+ streamType(STREAM_TYPE_UNKNOWN),
+ videoFrameRate(0),
+ videoBitRate(0),
+ videoWidth(0),
+ videoHeight(0),
+ videoRotation(0),
+ audioSampleRate(0),
+ audioChannels(0),
+ audioBitsPerSample(0),
+ duration(0) {
+ memset(codecName, 0, sizeof(codecName));
+ memset(language, 0, sizeof(language));
+ }
+};
+
+/**
+ * @brief The information of the media stream object.
+ *
+ */
+struct SrcInfo {
+ /** The bitrate of the media stream. The unit of the number is kbps.
+ *
+ */
+ int bitrateInKbps;
+
+ /** The name of the media stream.
+ *
+ */
+ const char* name;
+
+};
+
+/**
+ * @brief The type of the media metadata.
+ *
+ */
+enum MEDIA_PLAYER_METADATA_TYPE {
+ /** The type is unknown.
+ */
+ PLAYER_METADATA_TYPE_UNKNOWN = 0,
+ /** The type is SEI.
+ */
+ PLAYER_METADATA_TYPE_SEI = 1,
+};
+
+struct CacheStatistics {
+ /** total data size of uri
+ */
+ int64_t fileSize;
+ /** data of uri has cached
+ */
+ int64_t cacheSize;
+ /** data of uri has downloaded
+ */
+ int64_t downloadSize;
+};
+
+/**
+ * @brief The real time statistics of the media stream being played.
+ *
+ */
+struct PlayerPlaybackStats {
+ /** Video fps.
+ */
+ int videoFps;
+ /** Video bitrate (Kbps).
+ */
+ int videoBitrateInKbps;
+ /** Audio bitrate (Kbps).
+ */
+ int audioBitrateInKbps;
+ /** Total bitrate (Kbps).
+ */
+ int totalBitrateInKbps;
+};
+
+/**
+ * @brief The updated information of media player.
+ *
+ */
+struct PlayerUpdatedInfo {
+ /** @technical preview
+ */
+ const char* internalPlayerUuid;
+ /** The device ID of the playback device.
+ */
+ const char* deviceId;
+ /** Video height.
+ */
+ int videoHeight;
+ /** Video width.
+ */
+ int videoWidth;
+ /** Audio sample rate.
+ */
+ int audioSampleRate;
+ /** The audio channel number.
+ */
+ int audioChannels;
+ /** The bit number of each audio sample.
+ */
+ int audioBitsPerSample;
+
+ PlayerUpdatedInfo()
+ : internalPlayerUuid(NULL),
+ deviceId(NULL),
+ videoHeight(0),
+ videoWidth(0),
+ audioSampleRate(0),
+ audioChannels(0),
+ audioBitsPerSample(0) {}
+};
+
+/**
+ * The custom data source provides a data stream input callback, and the player will continue to call back this interface, requesting the user to fill in the data that needs to be played.
+ */
+class IMediaPlayerCustomDataProvider {
+public:
+
+ /**
+ * @brief The player requests to read the data callback, you need to fill the specified length of data into the buffer
+ * @param buffer the buffer pointer that you need to fill data.
+ * @param bufferSize the bufferSize need to fill of the buffer pointer.
+ * @return you need return offset value if succeed. return 0 if failed.
+ */
+ virtual int onReadData(unsigned char *buffer, int bufferSize) = 0;
+
+ /**
+ * @brief The Player seek event callback, you need to operate the corresponding stream seek operation, You can refer to the definition of lseek() at https://man7.org/linux/man-pages/man2/lseek.2.html
+ * @param offset the value of seek offset.
+ * @param whence the postion of start seeking, the directive whence as follows:
+ * 0 - SEEK_SET : The file offset is set to offset bytes.
+ * 1 - SEEK_CUR : The file offset is set to its current location plus offset bytes.
+ * 2 - SEEK_END : The file offset is set to the size of the file plus offset bytes.
+ * 65536 - AVSEEK_SIZE : Optional. Passing this as the "whence" parameter to a seek function causes it to return the filesize without seeking anywhere.
+ * @return
+ * whence == 65536, return filesize if you need.
+ * whence >= 0 && whence < 3 , return offset value if succeed. return -1 if failed.
+ */
+ virtual int64_t onSeek(int64_t offset, int whence) = 0;
+
+ virtual ~IMediaPlayerCustomDataProvider() {}
+};
+
+struct MediaSource {
+ /**
+ * The URL of the media file that you want to play.
+ */
+ const char* url;
+ /**
+ * The URI of the media file
+ *
+ * When caching is enabled, if the url cannot distinguish the cache file name,
+ * the uri must be able to ensure that the cache file name corresponding to the url is unique.
+ */
+ const char* uri;
+ /**
+ * Set the starting position for playback, in ms.
+ */
+ int64_t startPos;
+ /**
+ * Determines whether to autoplay after opening a media resource.
+ * - true: (Default) Autoplay after opening a media resource.
+ * - false: Do not autoplay after opening a media resource.
+ */
+ bool autoPlay;
+ /**
+ * Determines whether to enable cache streaming to local files. If enable cached, the media player will
+ * use the url or uri as the cache index.
+ *
+ * @note
+ * The local cache function only supports on-demand video/audio streams and does not support live streams.
+ * Caching video and audio files based on the HLS protocol (m3u8) to your local device is not supported.
+ *
+ * - true: Enable cache.
+ * - false: (Default) Disable cache.
+ */
+ bool enableCache;
+ /**
+ * Determines whether to enable multi-track audio stream decoding.
+ * Then you can select multi audio track of the media file for playback or publish to channel
+ *
+ * @note
+ * If you use the selectMultiAudioTrack API, you must set enableMultiAudioTrack to true.
+ *
+ * - true: Enable MultiAudioTrack;.
+ * - false: (Default) Disable MultiAudioTrack;.
+ */
+ bool enableMultiAudioTrack;
+ /**
+ * Determines whether the opened media resource is a stream through the Agora Broadcast Streaming Network(CDN).
+ * - true: It is a stream through the Agora Broadcast Streaming Network.
+ * - false: (Default) It is not a stream through the Agora Broadcast Streaming Network.
+ */
+ Optional isAgoraSource;
+ /**
+ * Determines whether the opened media resource is a live stream. If is a live stream, it can speed up the opening of media resources.
+ * - true: It is a live stream.
+ * - false: (Default) It is not is a live stream.
+ */
+ Optional isLiveSource;
+ /**
+ * External custom data source object
+ */
+ IMediaPlayerCustomDataProvider* provider;
+
+ MediaSource() : url(NULL), uri(NULL), startPos(0), autoPlay(true), enableCache(false),
+ enableMultiAudioTrack(false), provider(NULL){
+ }
+};
+
+} // namespace base
+} // namespace media
+} // namespace agora
diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraOptional.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraOptional.h
new file mode 100644
index 000000000..97595be45
--- /dev/null
+++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraOptional.h
@@ -0,0 +1,891 @@
+// Copyright (c) 2019 Agora.io. All rights reserved
+
+// This program is confidential and proprietary to Agora.io.
+// And may not be copied, reproduced, modified, disclosed to others, published
+// or used, in whole or in part, without the express prior written permission
+// of Agora.io.
+#pragma once
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+#include
+#endif
+#include
+
+#ifndef CONSTEXPR
+#if __cplusplus >= 201103L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201103L)
+#define CONSTEXPR constexpr
+#else
+#define CONSTEXPR
+#endif
+#endif // !CONSTEXPR
+
+#ifndef NOEXCEPT
+#if __cplusplus >= 201103L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201103L)
+#define NOEXCEPT(Expr) noexcept(Expr)
+#else
+#define NOEXCEPT(Expr)
+#endif
+#endif // !NOEXCEPT
+
+namespace agora {
+
+// Specification:
+// http://en.cppreference.com/w/cpp/utility/optional/in_place_t
+struct in_place_t {};
+
+// Specification:
+// http://en.cppreference.com/w/cpp/utility/optional/nullopt_t
+struct nullopt_t {
+ CONSTEXPR explicit nullopt_t(int) {}
+};
+
+// Specification:
+// http://en.cppreference.com/w/cpp/utility/optional/in_place
+/*CONSTEXPR*/ const in_place_t in_place = {};
+
+// Specification:
+// http://en.cppreference.com/w/cpp/utility/optional/nullopt
+/*CONSTEXPR*/ const nullopt_t nullopt(0);
+
+// Forward declaration, which is refered by following helpers.
+template
+class Optional;
+
+namespace internal {
+
+template
+struct OptionalStorageBase {
+ // Initializing |empty_| here instead of using default member initializing
+ // to avoid errors in g++ 4.8.
+ CONSTEXPR OptionalStorageBase() : is_populated_(false), empty_('\0') {}
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ CONSTEXPR explicit OptionalStorageBase(in_place_t, Args&&... args)
+ : is_populated_(true), value_(std::forward(args)...) {}
+#else
+ CONSTEXPR explicit OptionalStorageBase(in_place_t, const T& _value)
+ : is_populated_(true), value_(_value) {}
+#endif
+ // When T is not trivially destructible we must call its
+ // destructor before deallocating its memory.
+ // Note that this hides the (implicitly declared) move constructor, which
+ // would be used for constexpr move constructor in OptionalStorage.
+ // It is needed iff T is trivially move constructible. However, the current
+ // is_trivially_{copy,move}_constructible implementation requires
+ // is_trivially_destructible (which looks a bug, cf:
+ // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=51452 and
+ // http://cplusplus.github.io/LWG/lwg-active.html#2116), so it is not
+ // necessary for this case at the moment. Please see also the destructor
+ // comment in "is_trivially_destructible = true" specialization below.
+ ~OptionalStorageBase() {
+ if (is_populated_)
+ value_.~T();
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ void Init(Args&&... args) {
+ ::new (&value_) T(std::forward(args)...);
+ is_populated_ = true;
+ }
+#else
+ void Init(const T& _value) {
+ ::new (&value_) T(_value);
+ is_populated_ = true;
+ }
+#endif
+
+ bool is_populated_;
+
+ union {
+ // |empty_| exists so that the union will always be initialized, even when
+ // it doesn't contain a value. Union members must be initialized for the
+ // constructor to be 'constexpr'.
+ char empty_;
+ T value_;
+ };
+};
+
+// Implement conditional constexpr copy and move constructors. These are
+// constexpr if is_trivially_{copy,move}_constructible::value is true
+// respectively. If each is true, the corresponding constructor is defined as
+// "= default;", which generates a constexpr constructor (In this case,
+// the condition of constexpr-ness is satisfied because the base class also has
+// compiler generated constexpr {copy,move} constructors). Note that
+// placement-new is prohibited in constexpr.
+template
+struct OptionalStorage : OptionalStorageBase {
+ // This is no trivially {copy,move} constructible case. Other cases are
+ // defined below as specializations.
+
+ // Accessing the members of template base class requires explicit
+ // declaration.
+ using OptionalStorageBase::is_populated_;
+ using OptionalStorageBase::value_;
+ using OptionalStorageBase::Init;
+
+ // Inherit constructors (specifically, the in_place constructor).
+ //using OptionalStorageBase::OptionalStorageBase;
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ CONSTEXPR explicit OptionalStorage(in_place_t in_place, Args&&... args)
+ : OptionalStorageBase(in_place, std::forward(args)...) {}
+#else
+ CONSTEXPR explicit OptionalStorage(in_place_t in_place, const T& _value)
+ : OptionalStorageBase(in_place, _value) {}
+#endif
+
+ // User defined constructor deletes the default constructor.
+ // Define it explicitly.
+ OptionalStorage() {}
+
+ OptionalStorage(const OptionalStorage& other) {
+ if (other.is_populated_)
+ Init(other.value_);
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ OptionalStorage(OptionalStorage&& other) NOEXCEPT(std::is_nothrow_move_constructible::value) {
+ if (other.is_populated_)
+ Init(std::move(other.value_));
+ }
+#endif
+};
+
+// Base class to support conditionally usable copy-/move- constructors
+// and assign operators.
+template
+class OptionalBase {
+ // This class provides implementation rather than public API, so everything
+ // should be hidden. Often we use composition, but we cannot in this case
+ // because of C++ language restriction.
+ protected:
+ CONSTEXPR OptionalBase() {}
+ CONSTEXPR OptionalBase(const OptionalBase& other) : storage_(other.storage_) {}
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR OptionalBase(OptionalBase&& other) : storage_(std::move(other.storage_)) {}
+
+ template
+ CONSTEXPR explicit OptionalBase(in_place_t, Args&&... args)
+ : storage_(in_place, std::forward(args)...) {}
+#else
+ CONSTEXPR explicit OptionalBase(in_place_t, const T& _value)
+ : storage_(in_place, _value) {}
+#endif
+
+ // Implementation of converting constructors.
+ template
+ explicit OptionalBase(const OptionalBase& other) {
+ if (other.storage_.is_populated_)
+ storage_.Init(other.storage_.value_);
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ explicit OptionalBase(OptionalBase&& other) {
+ if (other.storage_.is_populated_)
+ storage_.Init(std::move(other.storage_.value_));
+ }
+#endif
+
+ ~OptionalBase() {}
+
+ OptionalBase& operator=(const OptionalBase& other) {
+ CopyAssign(other);
+ return *this;
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ OptionalBase& operator=(OptionalBase&& other) NOEXCEPT(
+ std::is_nothrow_move_assignable::value &&
+ std::is_nothrow_move_constructible::value) {
+ MoveAssign(std::move(other));
+ return *this;
+ }
+#endif
+
+ template
+ void CopyAssign(const OptionalBase& other) {
+ if (other.storage_.is_populated_)
+ InitOrAssign(other.storage_.value_);
+ else
+ FreeIfNeeded();
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ void MoveAssign(OptionalBase&& other) {
+ if (other.storage_.is_populated_)
+ InitOrAssign(std::move(other.storage_.value_));
+ else
+ FreeIfNeeded();
+ }
+#endif
+
+ template
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ void InitOrAssign(U&& value) {
+ if (storage_.is_populated_)
+ storage_.value_ = std::forward(value);
+ else
+ storage_.Init(std::forward(value));
+ }
+#else
+ void InitOrAssign(const U& value) {
+ if (storage_.is_populated_)
+ storage_.value_ = value;
+ else
+ storage_.Init(value);
+ }
+#endif
+
+
+ void FreeIfNeeded() {
+ if (!storage_.is_populated_)
+ return;
+ storage_.value_.~T();
+ storage_.is_populated_ = false;
+ }
+
+ // For implementing conversion, allow access to other typed OptionalBase
+ // class.
+ template
+ friend class OptionalBase;
+
+ OptionalStorage storage_;
+};
+
+// The following {Copy,Move}{Constructible,Assignable} structs are helpers to
+// implement constructor/assign-operator overloading. Specifically, if T is
+// is not movable but copyable, Optional's move constructor should not
+// participate in overload resolution. This inheritance trick implements that.
+template
+struct CopyConstructible {};
+
+template <>
+struct CopyConstructible {
+ CONSTEXPR CopyConstructible() {}
+ CopyConstructible& operator=(const CopyConstructible&) { return *this; }
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR CopyConstructible(CopyConstructible&&) {}
+ CopyConstructible& operator=(CopyConstructible&&) { return *this; }
+#endif
+ private:
+ CONSTEXPR CopyConstructible(const CopyConstructible&);
+};
+
+template
+struct MoveConstructible {};
+
+template <>
+struct MoveConstructible {
+ CONSTEXPR MoveConstructible() {}
+ CONSTEXPR MoveConstructible(const MoveConstructible&) {}
+ MoveConstructible& operator=(const MoveConstructible&) { return *this; }
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ MoveConstructible& operator=(MoveConstructible&&) { return *this; }
+ private:
+ CONSTEXPR MoveConstructible(MoveConstructible&&);
+#endif
+};
+
+template
+struct CopyAssignable {};
+
+template <>
+struct CopyAssignable {
+ CONSTEXPR CopyAssignable() {}
+ CONSTEXPR CopyAssignable(const CopyAssignable&) {}
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR CopyAssignable(CopyAssignable&&) {}
+ CopyAssignable& operator=(CopyAssignable&&) { return *this; }
+#endif
+ private:
+ CopyAssignable& operator=(const CopyAssignable&);
+};
+
+template
+struct MoveAssignable {};
+
+template <>
+struct MoveAssignable {
+ CONSTEXPR MoveAssignable() {}
+ CONSTEXPR MoveAssignable(const MoveAssignable&) {}
+ MoveAssignable& operator=(const MoveAssignable&) { return *this; }
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR MoveAssignable(MoveAssignable&&) {}
+
+ private:
+ MoveAssignable& operator=(MoveAssignable&&);
+#endif
+};
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+// Helper to conditionally enable converting constructors and assign operators.
+template
+struct IsConvertibleFromOptional
+ : std::integral_constant<
+ bool,
+ std::is_constructible&>::value ||
+ std::is_constructible&>::value ||
+ std::is_constructible&&>::value ||
+ std::is_constructible&&>::value ||
+ std::is_convertible&, T>::value ||
+ std::is_convertible&, T>::value ||
+ std::is_convertible&&, T>::value ||
+ std::is_convertible&&, T>::value> {};
+
+template
+struct IsAssignableFromOptional
+ : std::integral_constant<
+ bool,
+ IsConvertibleFromOptional::value ||
+ std::is_assignable&>::value ||
+ std::is_assignable&>::value ||
+ std::is_assignable&&>::value ||
+ std::is_assignable&&>::value> {};
+
+// Forward compatibility for C++17.
+// Introduce one more deeper nested namespace to avoid leaking using std::swap.
+namespace swappable_impl {
+using std::swap;
+
+struct IsSwappableImpl {
+ // Tests if swap can be called. Check(0) returns true_type iff swap
+ // is available for T. Otherwise, Check's overload resolution falls back
+ // to Check(...) declared below thanks to SFINAE, so returns false_type.
+ template
+ static auto Check(int)
+ -> decltype(swap(std::declval(), std::declval()), std::true_type());
+
+ template
+ static std::false_type Check(...);
+};
+} // namespace swappable_impl
+template
+struct IsSwappable : decltype(swappable_impl::IsSwappableImpl::Check(0)) {};
+#endif
+} // namespace internal
+
+// On Windows, by default, empty-base class optimization does not work,
+// which means even if the base class is empty struct, it still consumes one
+// byte for its body. __declspec(empty_bases) enables the optimization.
+// cf)
+// https://blogs.msdn.microsoft.com/vcblog/2016/03/30/optimizing-the-layout-of-empty-base-classes-in-vs2015-update-2-3/
+#if defined(_WIN32)
+#define OPTIONAL_DECLSPEC_EMPTY_BASES __declspec(empty_bases)
+#else
+#define OPTIONAL_DECLSPEC_EMPTY_BASES
+#endif
+
+// Optional is a Chromium version of the C++17 optional class:
+// std::optional documentation:
+// http://en.cppreference.com/w/cpp/utility/optional
+// Chromium documentation:
+// https://chromium.googlesource.com/chromium/src/+/master/docs/optional.md
+//
+// These are the differences between the specification and the implementation:
+// - Constructors do not use 'constexpr' as it is a C++14 extension.
+// - 'constexpr' might be missing in some places for reasons specified locally.
+// - No exceptions are thrown, because they are banned from Chromium.
+// Marked noexcept for only move constructor and move assign operators.
+// - All the non-members are in the 'base' namespace instead of 'std'.
+//
+// Note that T cannot have a constructor T(Optional) etc. Optional checks
+// T's constructor (specifically via IsConvertibleFromOptional), and in the
+// check whether T can be constructible from Optional, which is recursive
+// so it does not work. As of Feb 2018, std::optional C++17 implementation in
+// both clang and gcc has same limitation. MSVC SFINAE looks to have different
+// behavior, but anyway it reports an error, too.
+template
+class OPTIONAL_DECLSPEC_EMPTY_BASES Optional
+ : public internal::OptionalBase
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ , public internal::CopyConstructible::value>,
+ public internal::MoveConstructible::value>,
+ public internal::CopyAssignable::value &&
+ std::is_copy_assignable::value>,
+ public internal::MoveAssignable::value &&
+ std::is_move_assignable::value>
+#endif
+{
+ public:
+#undef OPTIONAL_DECLSPEC_EMPTY_BASES
+
+ typedef T value_type;
+
+ // Defer default/copy/move constructor implementation to OptionalBase.
+ CONSTEXPR Optional() {}
+ CONSTEXPR Optional(const Optional& other) : internal::OptionalBase(other) {}
+
+ CONSTEXPR Optional(nullopt_t) {} // NOLINT(runtime/explicit)
+
+ // Converting copy constructor. "explicit" only if
+ // std::is_convertible::value is false. It is implemented by
+ // declaring two almost same constructors, but that condition in enable_if_t
+ // is different, so that either one is chosen, thanks to SFINAE.
+ template
+ Optional(const Optional& other) : internal::OptionalBase(other) {}
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ // Converting move constructor. Similar to converting copy constructor,
+ // declaring two (explicit and non-explicit) constructors.
+ template
+ Optional(Optional&& other) : internal::OptionalBase(std::move(other)) {}
+
+ template
+ CONSTEXPR explicit Optional(in_place_t, Args&&... args)
+ : internal::OptionalBase(in_place, std::forward(args)...) {}
+
+ template
+ CONSTEXPR explicit Optional(in_place_t,
+ std::initializer_list il,
+ Args&&... args)
+ : internal::OptionalBase(in_place, il, std::forward(args)...) {}
+#else
+ CONSTEXPR explicit Optional(in_place_t, const T& _value)
+ : internal::OptionalBase(in_place, _value) {}
+ template
+ CONSTEXPR explicit Optional(in_place_t,
+ const U il[],
+ const T& _value)
+ : internal::OptionalBase(in_place, il, _value) {}
+#endif
+
+ // Forward value constructor. Similar to converting constructors,
+ // conditionally explicit.
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ CONSTEXPR Optional(U&& value)
+ : internal::OptionalBase(in_place, std::forward(value)) {}
+#else
+ template
+ CONSTEXPR Optional(const U& value)
+ : internal::OptionalBase(in_place, value) {}
+#endif
+
+ ~Optional() {}
+
+ // Defer copy-/move- assign operator implementation to OptionalBase.
+ Optional& operator=(const Optional& other) {
+ if (&other == this) {
+ return *this;
+ }
+
+ internal::OptionalBase::operator=(other);
+ return *this;
+ }
+
+ Optional& operator=(nullopt_t) {
+ FreeIfNeeded();
+ return *this;
+ }
+
+ // Perfect-forwarded assignment.
+ template
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ Optional& operator=(U&& value) {
+ InitOrAssign(std::forward(value));
+ return *this;
+ }
+#else
+ Optional& operator=(const U& value) {
+ InitOrAssign(value);
+ return *this;
+ }
+#endif
+
+ // Copy assign the state of other.
+ template
+ Optional& operator=(const Optional& other) {
+ CopyAssign(other);
+ return *this;
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ // Move assign the state of other.
+ template
+ Optional& operator=(Optional&& other) {
+ MoveAssign(std::move(other));
+ return *this;
+ }
+#endif
+
+ const T* operator->() const {
+ return &storage_.value_;
+ }
+
+ T* operator->() {
+ return &storage_.value_;
+ }
+
+ const T& operator*() const {
+ return storage_.value_;
+ }
+
+ T& operator*() {
+ return storage_.value_;
+ }
+
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR explicit operator bool() const { return storage_.is_populated_; }
+#else
+ CONSTEXPR operator bool() const { return storage_.is_populated_; }
+#endif
+
+ CONSTEXPR bool has_value() const { return storage_.is_populated_; }
+
+#if 1
+ const T& value() const {
+ return storage_.value_;
+ }
+
+ template
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR T value_or(U&& default_value) const {
+ // TODO(mlamouri): add the following assert when possible:
+ // static_assert(std::is_copy_constructible::value,
+ // "T must be copy constructible");
+ static_assert(std::is_convertible::value,
+ "U must be convertible to T");
+ return storage_.is_populated_
+ ? value()
+ : static_cast(std::forward(default_value));
+ }
+#else
+ CONSTEXPR T value_or(const U& default_value) const {
+ return storage_.is_populated_
+ ? value()
+ : static_cast(default_value);
+ }
+#endif
+#else
+ const T& value() const & {
+ return storage_.value_;
+ }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ const T&& value() const && {
+ return std::move(storage_.value_);
+ }
+#endif
+
+ template
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR T value_or(U&& default_value) const & {
+ // TODO(mlamouri): add the following assert when possible:
+ // static_assert(std::is_copy_constructible::value,
+ // "T must be copy constructible");
+ static_assert(std::is_convertible::value,
+ "U must be convertible to T");
+ return storage_.is_populated_
+ ? value()
+ : static_cast(std::forward(default_value));
+ }
+#else
+ CONSTEXPR T value_or(const U& default_value) const & {
+ // TODO(mlamouri): add the following assert when possible:
+ // static_assert(std::is_copy_constructible::value,
+ // "T must be copy constructible");
+ static_assert(std::is_convertible::value,
+ "U must be convertible to T");
+ return storage_.is_populated_
+ ? value()
+ : static_cast(default_value);
+ }
+#endif
+
+ template
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ CONSTEXPR T value_or(U&& default_value) const && {
+ // TODO(mlamouri): add the following assert when possible:
+ // static_assert(std::is_move_constructible::value,
+ // "T must be move constructible");
+ static_assert(std::is_convertible::value,
+ "U must be convertible to T");
+ return storage_.is_populated_
+ ? std::move(value())
+ : static_cast(std::forward(default_value));
+ }
+#endif
+#endif // 1
+
+ void swap(Optional& other) {
+ if (!storage_.is_populated_ && !other.storage_.is_populated_)
+ return;
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ if (storage_.is_populated_ != other.storage_.is_populated_) {
+ if (storage_.is_populated_) {
+ other.storage_.Init(std::move(storage_.value_));
+ FreeIfNeeded();
+ } else {
+ storage_.Init(std::move(other.storage_.value_));
+ other.FreeIfNeeded();
+ }
+ return;
+ }
+#endif
+ using std::swap;
+ swap(**this, *other);
+ }
+
+ void reset() { FreeIfNeeded(); }
+
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ template
+ T& emplace(Args&&... args) {
+ FreeIfNeeded();
+ storage_.Init(std::forward(args)...);
+ return storage_.value_;
+ }
+
+ template
+ T& emplace(std::initializer_list il, Args&&... args) {
+ FreeIfNeeded();
+ storage_.Init(il, std::forward(args)...);
+ return storage_.value_;
+ }
+#else
+ T& emplace(const T& _value) {
+ FreeIfNeeded();
+ storage_.Init(_value);
+ return storage_.value_;
+ }
+ template
+ T& emplace(const U il[], const T& _value) {
+ FreeIfNeeded();
+ storage_.Init(il, _value);
+ return storage_.value_;
+ }
+#endif
+
+ private:
+ // Accessing template base class's protected member needs explicit
+ // declaration to do so.
+ using internal::OptionalBase::CopyAssign;
+ using internal::OptionalBase::FreeIfNeeded;
+ using internal::OptionalBase::InitOrAssign;
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+ using internal::OptionalBase::MoveAssign;
+#endif
+ using internal::OptionalBase::storage_;
+};
+
+// Here after defines comparation operators. The definition follows
+// http://en.cppreference.com/w/cpp/utility/optional/operator_cmp
+// while bool() casting is replaced by has_value() to meet the chromium
+// style guide.
+template