diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 720118ef2..44d99e200 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -24,7 +24,7 @@ runs:
if: ${{ runner.os == 'macOS' }}
uses: maxim-lobanov/setup-cocoapods@v1
with:
- version: 1.13.0
+ version: 1.16.2
- name: Cache dependencies
id: yarn-cache
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 68133e1f3..5179344f9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,8 +14,6 @@ jobs:
matrix:
newArch: [false, true]
env:
- TURBO_CACHE_DIR: .turbo/android
- turbo_cache_hit: 0
ORG_GRADLE_PROJECT_newArchEnabled: ${{ matrix.newArch }}
steps:
- name: Checkout
@@ -24,71 +22,40 @@ jobs:
- name: Setup
uses: ./.github/actions/setup
- - name: Cache turborepo for Android
- uses: actions/cache@v3
- with:
- path: ${{ env.TURBO_CACHE_DIR }}
- key: ${{ runner.os }}-turborepo-android-${{ matrix.newArch }}-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- ${{ runner.os }}-turborepo-android-${{ matrix.newArch }}-
-
- - name: Check turborepo cache for Android
- run: |
- TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status")
-
- if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
- echo "turbo_cache_hit=1" >> $GITHUB_ENV
- fi
-
- name: Install JDK
- if: env.turbo_cache_hit != 1
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
- if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- - name: Cache Gradle
- if: env.turbo_cache_hit != 1
- uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/wrapper
- ~/.gradle/caches
- key: ${{ runner.os }}-gradle-${{ matrix.newArch }}-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-${{ matrix.newArch }}-
-
- name: Modify APP ID
run: |
sed "s/localAppId = '\(.*\)'/localAppId = '${{ secrets.APP_ID }}'/g" agora.config.ts > tmp
mv tmp agora.config.ts
- working-directory: example/src/config
+ working-directory: examples/expo/src/config
- name: Build example for Android
run: |
- yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --force=true
+ yarn example build:android
- name: Upload APK
uses: actions/upload-artifact@v4
with:
- name: AgoraRtcNgExample-Android-${{ matrix.newArch && 'NewArch' || 'OldArch' }}-${{ github.run_id }}
+ name: AgoraRtcNgExampleExpo-Android-${{ matrix.newArch && 'NewArch' || 'OldArch' }}-${{ github.run_id }}
path: |
- example/android/app/build/outputs/apk/release/*.apk
+ examples/expo/android/app/build/outputs/apk/release/*.apk
if-no-files-found: error
build-ios:
- runs-on: macos-latest
+ runs-on: macos-15
strategy:
matrix:
newArch: [false, true]
env:
- TURBO_CACHE_DIR: .turbo/ios
- turbo_cache_hit: 0
RCT_NEW_ARCH_ENABLED: ${{ matrix.newArch }}
steps:
- name: Checkout
@@ -101,34 +68,20 @@ jobs:
run: |
brew install fastlane
- - name: Cache turborepo for iOS
- uses: actions/cache@v3
- with:
- path: ${{ env.TURBO_CACHE_DIR }}
- key: ${{ runner.os }}-turborepo-ios-${{ matrix.newArch }}-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- ${{ runner.os }}-turborepo-ios-${{ matrix.newArch }}-
-
- - name: Cache cocoapods
- if: env.turbo_cache_hit != 1
- id: cocoapods-cache
- uses: actions/cache@v3
- with:
- path: |
- **/ios/Pods
- key: ${{ runner.os }}-cocoapods-${{ matrix.newArch }}-${{ hashFiles('example/ios/Podfile.lock') }}
- restore-keys: |
- ${{ runner.os }}-cocoapods-${{ matrix.newArch }}-
+ - name: Switch Xcode
+ run: |
+ #https://github.com/actions/runner-images/issues/12758
+ sudo xcode-select --switch /Applications/Xcode_16.4.app
- name: Install cocoapods
run: |
- yarn pod-install example/ios
+ yarn pod-install examples/expo/ios
- name: Modify APP ID
run: |
sed "s/localAppId = '\(.*\)'/localAppId = '${{ secrets.APP_ID }}'/g" agora.config.ts > tmp
mv tmp agora.config.ts
- working-directory: example/src/config
+ working-directory: examples/expo/src/config
- name: Install the Apple certificate and provisioning profile
env:
@@ -161,22 +114,22 @@ jobs:
- name: Build example for iOS
run: |
- yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --force=true
+ yarn example build:ios
- name: Upload IPA
uses: actions/upload-artifact@v4
with:
- name: AgoraRtcNgExample-iOS-${{ matrix.newArch && 'NewArch' || 'OldArch' }}-${{ github.run_id }}
+ name: AgoraRtcNgExampleExpo-iOS-${{ matrix.newArch && 'NewArch' || 'OldArch' }}-${{ github.run_id }}
path: |
- example/ios/*.ipa
+ examples/expo/ios/*.ipa
if-no-files-found: error
- name: Upload dSYM
uses: actions/upload-artifact@v4
with:
- name: AgoraRtcNgExampleSymbol-${{ matrix.newArch && 'NewArch' || 'OldArch' }}-${{ github.run_id }}
+ name: AgoraRtcNgExampleExpoSymbol-${{ matrix.newArch && 'NewArch' || 'OldArch' }}-${{ github.run_id }}
path: |
- example/ios/*.dSYM.zip
+ examples/expo/ios/*.dSYM.zip
if-no-files-found: error
notification:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9f7f5f18e..8183725a7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -68,9 +68,7 @@ jobs:
newArch: [true, false]
runs-on: ubuntu-latest
env:
- TURBO_CACHE_DIR: .turbo/android
ORG_GRADLE_PROJECT_newArchEnabled: ${{ matrix.newArch }}
- turbo_cache_hit: 0
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -78,45 +76,16 @@ jobs:
- name: Setup
uses: ./.github/actions/setup
- - name: Cache turborepo for Android
- uses: actions/cache@v3
- with:
- path: ${{ env.TURBO_CACHE_DIR }}
- key: ${{ runner.os }}-turborepo-android-detox-${{ matrix.newArch }}-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- ${{ runner.os }}-turborepo-android-detox-${{ matrix.newArch }}-
-
- - name: Check turborepo cache for Android
- run: |
- TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run detox:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'detox:android').cache.status")
-
- if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then
- echo "turbo_cache_hit=1" >> $GITHUB_ENV
- fi
-
- name: Install JDK
- if: env.turbo_cache_hit != 1
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Finalize Android SDK
- if: env.turbo_cache_hit != 1
run: |
/bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null"
- - name: Cache Gradle
- if: env.turbo_cache_hit != 1
- uses: actions/cache@v3
- with:
- path: |
- ~/.gradle/wrapper
- ~/.gradle/caches
- key: ${{ runner.os }}-gradle-${{ matrix.newArch }}-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }}
- restore-keys: |
- ${{ runner.os }}-gradle-${{ matrix.newArch }}-
-
- name: Install Detox dependencies
shell: bash
run: |
@@ -126,23 +95,33 @@ jobs:
run: |
sed "s/localAppId = '\(.*\)'/localAppId = '${{ secrets.APP_ID }}'/g" agora.config.ts > tmp
mv tmp agora.config.ts
- working-directory: example/src/config
+ working-directory: examples/expo/src/config
- name: Build example for Android
run: |
- yarn turbo run detox:android --cache-dir="${{ env.TURBO_CACHE_DIR }}"
+ yarn example detox:android
- name: Clean Useless cache
run: |
- rm -rf "${{ env.TURBO_CACHE_DIR }}" || true
rm -rf ~/.gradle/caches || true
rm -rf ~/.gradle/wrapper || true
sudo apt-get clean
npm cache clean --force
- rm -rf example/ios
+ rm -rf examples/**/ios
yarn cache clean
df -h
+ - name: Free Disk Space
+ uses: jlumbroso/free-disk-space@main
+ with:
+ tool-cache: false
+ android: false
+ dotnet: true
+ haskell: true
+ large-packages: true
+ docker-images: true
+ swap-storage: true
+
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
@@ -157,16 +136,15 @@ jobs:
api-level: 31
arch: x86_64
avd-name: emulator
- working-directory: example
+ working-directory: examples/expo
script: detox test -c android.emu.release
test-ios:
strategy:
matrix:
newArch: [1, 0]
- runs-on: macos-latest
+ runs-on: macos-15
env:
- TURBO_CACHE_DIR: .turbo/ios
RCT_NEW_ARCH_ENABLED: ${{ matrix.newArch }}
steps:
- name: Checkout
@@ -177,7 +155,12 @@ jobs:
- name: Install cocoapods
run: |
- yarn pod-install example/ios
+ yarn pod-install examples/expo/ios
+
+ - name: Switch Xcode
+ run: |
+ #https://github.com/actions/runner-images/issues/12758
+ sudo xcode-select --switch /Applications/Xcode_16.4.app
- name: Install Detox dependencies
shell: bash
@@ -190,20 +173,20 @@ jobs:
run: |
sed "s/localAppId = '\(.*\)'/localAppId = '${{ secrets.APP_ID }}'/g" agora.config.ts > tmp
mv tmp agora.config.ts
- working-directory: example/src/config
+ working-directory: examples/expo/src/config
- name: Build example for iOS
run: |
- yarn turbo run detox:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}"
+ yarn example detox:ios
- uses: futureware-tech/simulator-action@v4
with:
- model: 'iPhone 15'
+ model: 'iPhone 16'
- name: Run e2e tests
# https://github.com/wix/Detox/issues/3720#issuecomment-1347855162
if: ${{ matrix.newArch == 0 }}
- working-directory: example
+ working-directory: examples/expo
run: |
detox clean-framework-cache
detox build-framework-cache
diff --git a/.github/workflows/dep.yml b/.github/workflows/dep.yml
index 322609b6b..764a4c841 100644
--- a/.github/workflows/dep.yml
+++ b/.github/workflows/dep.yml
@@ -30,8 +30,8 @@ jobs:
- name: Update example
run: |
- rm -rf example/ios/Podfile.lock
- yarn pod-install example/ios
+ rm -rf examples/expo/ios/Podfile.lock
+ yarn pod-install examples/expo/ios
- name: Create pull request
uses: AgoraIO-Extensions/actions/.github/actions/pr@main
diff --git a/.gitignore b/.gitignore
index a93b523ed..2c1f2a5c7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,11 +44,11 @@ android.iml
# Cocoapods
#
-example/ios/Pods
+examples/**/ios/Pods
**/Pods/
# Ruby
-example/vendor/
+examples/**/vendor/
# node.js
#
diff --git a/.yarnrc.yml b/.yarnrc.yml
index d6817f4ff..5237ebe99 100644
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -4,7 +4,6 @@ nmHoistingLimits: workspaces
plugins:
- path: scripts/bootstrap.cjs
- - path: scripts/pod-install.cjs
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
diff --git a/examples/expo/.detoxrc.js b/examples/expo/.detoxrc.js
new file mode 100644
index 000000000..e7d6875e4
--- /dev/null
+++ b/examples/expo/.detoxrc.js
@@ -0,0 +1,87 @@
+/** @type {Detox.DetoxConfig} */
+module.exports = {
+ testRunner: {
+ args: {
+ $0: 'jest',
+ config: 'e2e/jest.config.js',
+ },
+ jest: {
+ setupTimeout: 2100000,
+ },
+ },
+ apps: {
+ 'ios.debug': {
+ type: 'ios.app',
+ binaryPath:
+ 'ios/build/Build/Products/Debug-iphonesimulator/reactnativeagoraexampleexpo.app',
+ build:
+ 'xcodebuild -workspace ios/reactnativeagoraexampleexpo.xcworkspace -scheme reactnativeagoraexampleexpo -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
+ },
+ 'ios.release': {
+ type: 'ios.app',
+ binaryPath:
+ 'ios/build/Build/Products/Release-iphonesimulator/reactnativeagoraexampleexpo.app',
+ build:
+ 'xcodebuild -workspace ios/reactnativeagoraexampleexpo.xcworkspace -scheme reactnativeagoraexampleexpo -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
+ },
+ 'android.debug': {
+ type: 'android.apk',
+ binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
+ build:
+ 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -',
+ reversePorts: [8081],
+ },
+ 'android.release': {
+ type: 'android.apk',
+ binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
+ build:
+ 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -',
+ },
+ },
+ devices: {
+ simulator: {
+ type: 'ios.simulator',
+ device: {
+ type: 'iPhone 16',
+ },
+ },
+ attached: {
+ type: 'android.attached',
+ device: {
+ adbName: '.*',
+ },
+ },
+ emulator: {
+ type: 'android.emulator',
+ device: {
+ avdName: 'emulator',
+ },
+ },
+ },
+ configurations: {
+ 'ios.sim.debug': {
+ device: 'simulator',
+ app: 'ios.debug',
+ },
+ 'ios.sim.release': {
+ device: 'simulator',
+ app: 'ios.release',
+ },
+ 'android.att.debug': {
+ device: 'attached',
+ app: 'android.debug',
+ },
+ 'android.att.release': {
+ device: 'attached',
+ app: 'android.release',
+ },
+ 'android.emu.debug': {
+ device: 'emulator',
+ app: 'android.debug',
+ },
+ 'android.emu.release': {
+ device: 'emulator',
+ app: 'android.release',
+ },
+ },
+};
diff --git a/examples/expo/.gitignore b/examples/expo/.gitignore
new file mode 100644
index 000000000..4957fbdb6
--- /dev/null
+++ b/examples/expo/.gitignore
@@ -0,0 +1,40 @@
+# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
+
+# dependencies
+node_modules/
+
+# Expo
+.expo/
+dist/
+web-build/
+expo-env.d.ts
+
+# Native
+.kotlin/
+*.orig.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+
+# Metro
+.metro-health-check*
+
+# debug
+npm-debug.*
+yarn-debug.*
+yarn-error.*
+
+# macOS
+.DS_Store
+*.pem
+
+# local env files
+.env*.local
+
+# typescript
+*.tsbuildinfo
+
+
+*.dSYM.zip
diff --git a/example/.node-version b/examples/expo/.node-version
similarity index 100%
rename from example/.node-version
rename to examples/expo/.node-version
diff --git a/example/.ruby-version b/examples/expo/.ruby-version
similarity index 100%
rename from example/.ruby-version
rename to examples/expo/.ruby-version
diff --git a/example/Gemfile b/examples/expo/Gemfile
similarity index 100%
rename from example/Gemfile
rename to examples/expo/Gemfile
diff --git a/example/Gemfile.lock b/examples/expo/Gemfile.lock
similarity index 100%
rename from example/Gemfile.lock
rename to examples/expo/Gemfile.lock
diff --git a/examples/expo/README.md b/examples/expo/README.md
new file mode 100644
index 000000000..8d75e9882
--- /dev/null
+++ b/examples/expo/README.md
@@ -0,0 +1,361 @@
+# React Native Agora Expo Example
+
+## Overview
+
+This guide will walk you through simple instructions to create a real-time communication app using Agora SDK with Expo and test it using an emulator or your mobile device. The example demonstrates how to implement voice and video calling, stream sharing, and various advanced features using Agora's React Native SDK in an Expo-managed application.
+
+## Getting Started
+
+This section contains instructions to create a simple Expo app with Agora integration. We'll help you understand the project setup and provide complete code samples to implement functionality quickly.
+
+### Prerequisites
+
+- An Agora account - [Sign up](https://console.agora.io/) if you don't have one
+- Working Expo development environment
+- Familiarity with React Native basics
+- VS Code or any other IDE / code editor
+
+### Setup Your Agora App
+
+1. Create a project in the [Agora Console](https://console.agora.io/)
+2. Obtain your App ID from the Agora Console
+3. For token-based authentication (recommended for production), generate a temporary token or set up an authentication server
+
+### Create an Expo App
+
+1. Open your Terminal and navigate to the directory where you'd like to create your app
+2. Run the following command to create an Expo app:
+
+```bash
+npx create-expo-app my-agora-app && cd my-agora-app
+```
+
+3. Install expo-dev-client to enable native module usage:
+
+```bash
+npx expo install expo-dev-client
+```
+
+4. Test run your app:
+
+**Android**
+
+```bash
+npx expo run:android
+```
+
+**iOS**
+
+```bash
+npx expo run:ios
+```
+
+The above commands compile your project into a debug build of your app using locally installed Android SDK or Xcode.
+
+### Install Agora SDK and Dependencies
+
+After successfully testing your app, install the React Native Agora package:
+
+```bash
+npx expo install react-native-agora
+```
+
+### Configure App Permissions
+
+1. Set up necessary permissions in your app.json:
+
+```json
+{
+ "expo": {
+ "android": {
+ "permissions": [
+ "android.permission.CAMERA",
+ "android.permission.RECORD_AUDIO",
+ "android.permission.MODIFY_AUDIO_SETTINGS",
+ "android.permission.ACCESS_WIFI_STATE",
+ "android.permission.ACCESS_NETWORK_STATE",
+ "android.permission.BLUETOOTH",
+ "android.permission.FOREGROUND_SERVICE"
+ ]
+ }
+ }
+}
+```
+
+2. For iOS, add the following to your app.json to configure the permission request messages:
+
+```json
+{
+ "expo": {
+ "plugins": [
+ [
+ "expo-camera",
+ {
+ "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera for video calls",
+ "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone for audio calls"
+ }
+ ]
+ ]
+ }
+}
+```
+
+### Setup Minimum Deployment Targets
+
+Agora SDK requires minimum SDK versions:
+
+- Android: minSdkVersion = 24
+- iOS: iOS 12.4+
+
+Install expo-build-properties to configure these:
+
+```bash
+npx expo install expo-build-properties --save-dev
+```
+
+Add the following to your app.json:
+
+```json
+{
+ "expo": {
+ "plugins": [
+ [
+ "expo-build-properties",
+ {
+ "android": {
+ "minSdkVersion": 24 // depends on react-native and expo version that you choose
+ },
+ "ios": {
+ "deploymentTarget": "12.4" // depends on react-native and expo version that you choose
+ }
+ }
+ ]
+ ]
+ }
+}
+```
+
+### Configure Your Agora App ID
+
+Create a configuration file to store your Agora credentials:
+
+1. Create a file at `src/config/appID.js`:
+
+```javascript
+export const appId = 'YOUR_AGORA_APP_ID';
+```
+
+2. Make sure to add this file to your .gitignore to avoid exposing your App ID
+
+### Basic Integration Example
+
+Here's a simple example of how to implement a basic video call:
+
+```javascript
+import React, { useEffect, useState } from 'react';
+import { Button, StyleSheet, View } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ createAgoraRtcEngine,
+ IRtcEngineEventHandler,
+ RtcSurfaceView,
+ VideoSourceType,
+} from 'react-native-agora';
+
+import Config from '../config/agora.config';
+import { askMediaAccess } from '../utils/permissions';
+
+export default function BasicVideoCall() {
+ const [engine, setEngine] = useState(undefined);
+ const [isJoined, setIsJoined] = useState(false);
+ const [remoteUsers, setRemoteUsers] = useState([]);
+
+ useEffect(() => {
+ // Initialize Agora engine when component mounts
+ const init = async () => {
+ if (!Config.appId) {
+ console.error('App ID is missing');
+ return;
+ }
+
+ // Create Agora engine instance
+ const rtcEngine = createAgoraRtcEngine();
+ setEngine(rtcEngine);
+
+ // Initialize the engine
+ rtcEngine.initialize({
+ appId: Config.appId,
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+
+ // Request permissions and enable video
+ await askMediaAccess([
+ 'android.permission.CAMERA',
+ 'android.permission.RECORD_AUDIO',
+ ]);
+ rtcEngine.enableVideo();
+
+ // Register event handlers
+ rtcEngine.addListener('onJoinChannelSuccess', () => {
+ setIsJoined(true);
+ console.log('Successfully joined the channel');
+ });
+
+ rtcEngine.addListener('onUserJoined', (connection, uid) => {
+ console.log('Remote user joined:', uid);
+ setRemoteUsers((prev) => [...prev, uid]);
+ });
+
+ rtcEngine.addListener('onUserOffline', (connection, uid) => {
+ console.log('Remote user left:', uid);
+ setRemoteUsers((prev) => prev.filter((id) => id !== uid));
+ });
+ };
+
+ init();
+ return () => {
+ // Clean up
+ engine?.leaveChannel();
+ engine?.unregisterEventHandler({});
+ };
+ }, []);
+
+ const joinChannel = async () => {
+ if (!engine) return;
+
+ // Join a channel
+ engine.joinChannel(Config.token, Config.channelId, 0, {
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ const leaveChannel = () => {
+ if (!engine) return;
+ engine.leaveChannel();
+ setIsJoined(false);
+ setRemoteUsers([]);
+ };
+
+ return (
+
+ {isJoined && (
+
+ {/* Local video */}
+
+
+ {/* Remote videos */}
+ {remoteUsers.map((uid) => (
+
+ ))}
+
+ )}
+
+
+ {!isJoined ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ videoContainer: {
+ flex: 1,
+ width: '100%',
+ },
+ localVideo: {
+ width: '100%',
+ height: 300,
+ },
+ remoteVideo: {
+ width: '100%',
+ height: 300,
+ marginTop: 10,
+ },
+ buttonContainer: {
+ width: '100%',
+ padding: 20,
+ },
+});
+```
+
+### Apply Changes in Native Platform Folders
+
+After making these configuration changes, you'll need to rebuild your project:
+
+```bash
+npx expo prebuild
+```
+
+## Available Examples
+
+The Agora React Native Expo sample app includes various examples organized in three categories:
+
+### Basic Examples
+
+- JoinChannelAudio: Simple voice call implementation
+- JoinChannelVideo: Basic video calling functionality
+- StringUid: Using string user IDs instead of integers
+
+### Advanced Examples
+
+- AudioCallRoute: Control audio route settings
+- AudioMixing: Mix audio during calls
+- BeautyEffect: Implement video beauty effects
+- ChannelMediaRelay: Stream across multiple channels
+- Encryption: Secure communications with encryption
+- JoinMultipleChannel: Join multiple channels simultaneously
+- MediaPlayer: Play media during calls
+- PictureInPicture: Enable Picture-in-Picture mode
+- ScreenShare: Share device screen
+- And many more...
+
+### Hook-based Examples
+
+- Implementation using React hooks for cleaner, more functional code
+- Examples include all basic features plus several advanced ones
+
+## Running the Sample App
+
+To run the full example app:
+
+```bash
+# For Android
+npx expo run:android
+
+# For iOS
+npx expo run:ios
+```
+
+## Additional Resources
+
+- [Agora API Reference](https://docs.agora.io/en/video-calling/reference/api-ref)
+- [Agora Developer Center](https://www.agora.io/en/developer-center/)
+- [React Native Documentation](https://reactnative.dev/docs/getting-started)
+- [Expo Documentation](https://docs.expo.dev/)
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.
diff --git a/examples/expo/android/.gitignore b/examples/expo/android/.gitignore
new file mode 100644
index 000000000..8a6be0771
--- /dev/null
+++ b/examples/expo/android/.gitignore
@@ -0,0 +1,16 @@
+# OSX
+#
+.DS_Store
+
+# Android/IntelliJ
+#
+build/
+.idea
+.gradle
+local.properties
+*.iml
+*.hprof
+.cxx/
+
+# Bundle artifacts
+*.jsbundle
diff --git a/examples/expo/android/app/build.gradle b/examples/expo/android/app/build.gradle
new file mode 100644
index 000000000..e64c0f895
--- /dev/null
+++ b/examples/expo/android/app/build.gradle
@@ -0,0 +1,185 @@
+apply plugin: "com.android.application"
+apply plugin: "org.jetbrains.kotlin.android"
+apply plugin: "com.facebook.react"
+
+def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
+
+/**
+ * This is the configuration block to customize your React Native Android app.
+ * By default you don't need to apply any configuration, just uncomment the lines you need.
+ */
+react {
+ entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
+ reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
+ hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
+ codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
+
+ enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
+ // Use Expo CLI to bundle the app, this ensures the Metro config
+ // works correctly with Expo projects.
+ cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
+ bundleCommand = "export:embed"
+
+ /* Folders */
+ // The root of your project, i.e. where "package.json" lives. Default is '../..'
+ // root = file("../../")
+ // The folder where the react-native NPM package is. Default is ../../node_modules/react-native
+ // reactNativeDir = file("../../node_modules/react-native")
+ // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
+ // codegenDir = file("../../node_modules/@react-native/codegen")
+
+ /* Variants */
+ // The list of variants to that are debuggable. For those we're going to
+ // skip the bundling of the JS bundle and the assets. By default is just 'debug'.
+ // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
+ // debuggableVariants = ["liteDebug", "prodDebug"]
+
+ /* Bundling */
+ // A list containing the node command and its flags. Default is just 'node'.
+ // nodeExecutableAndArgs = ["node"]
+
+ //
+ // The path to the CLI configuration file. Default is empty.
+ // bundleConfig = file(../rn-cli.config.js)
+ //
+ // The name of the generated asset file containing your JS bundle
+ // bundleAssetName = "MyApplication.android.bundle"
+ //
+ // The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
+ // entryFile = file("../js/MyApplication.android.js")
+ //
+ // A list of extra flags to pass to the 'bundle' commands.
+ // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
+ // extraPackagerArgs = []
+
+ /* Hermes Commands */
+ // The hermes compiler command to run. By default it is 'hermesc'
+ // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
+ //
+ // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
+ // hermesFlags = ["-O", "-output-source-map"]
+
+ /* Autolinking */
+ autolinkLibrariesWithApp()
+}
+
+/**
+ * Set this to true to Run Proguard on Release builds to minify the Java bytecode.
+ */
+def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
+
+/**
+ * The preferred build flavor of JavaScriptCore (JSC)
+ *
+ * For example, to use the international variant, you can use:
+ * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
+ *
+ * The international variant includes ICU i18n library and necessary data
+ * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
+ * give correct results when using with locales other than en-US. Note that
+ * this variant is about 6MiB larger per architecture than default.
+ */
+def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
+
+android {
+ ndkVersion rootProject.ext.ndkVersion
+
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ compileSdk rootProject.ext.compileSdkVersion
+
+ namespace 'com.reactnativeagoraexampleexpo'
+ defaultConfig {
+ applicationId 'com.reactnativeagoraexampleexpo'
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode 1
+ versionName "1.0.0"
+ testBuildType System.getProperty('testBuildType', 'debug')
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ }
+ signingConfigs {
+ debug {
+ storeFile file('debug.keystore')
+ storePassword 'android'
+ keyAlias 'androiddebugkey'
+ keyPassword 'android'
+ }
+ }
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ }
+ release {
+ // Caution! In production, you need to generate your own keystore file.
+ // see https://reactnative.dev/docs/signed-apk-android.
+ signingConfig signingConfigs.debug
+ shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
+ minifyEnabled enableProguardInReleaseBuilds
+ proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+ proguardFile "${rootProject.projectDir}/../../node_modules/detox/android/detox/proguard-rules-app.pro"
+ crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
+ }
+ }
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
+ }
+ }
+ androidResources {
+ ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
+ }
+}
+
+// Apply static values from `gradle.properties` to the `android.packagingOptions`
+// Accepts values in comma delimited lists, example:
+// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
+["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
+ // Split option: 'foo,bar' -> ['foo', 'bar']
+ def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
+ // Trim all elements in place.
+ for (i in 0.. 0) {
+ println "android.packagingOptions.$prop += $options ($options.length)"
+ // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
+ options.each {
+ android.packagingOptions[prop] += it
+ }
+ }
+}
+
+dependencies {
+ androidTestImplementation('com.wix:detox:+')
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+ // The version of react-native is set by the React Native Gradle Plugin
+ implementation("com.facebook.react:react-android")
+
+ def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
+ def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
+ def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
+
+ if (isGifEnabled) {
+ // For animated gif support
+ implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
+ }
+
+ if (isWebpEnabled) {
+ // For webp support
+ implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
+ if (isWebpAnimatedEnabled) {
+ // Animated webp support
+ implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
+ }
+ }
+
+ if (hermesEnabled.toBoolean()) {
+ implementation("com.facebook.react:hermes-android")
+ } else {
+ implementation jscFlavor
+ }
+}
diff --git a/example/android/app/debug.keystore b/examples/expo/android/app/debug.keystore
similarity index 100%
rename from example/android/app/debug.keystore
rename to examples/expo/android/app/debug.keystore
diff --git a/examples/expo/android/app/proguard-rules.pro b/examples/expo/android/app/proguard-rules.pro
new file mode 100644
index 000000000..551eb41da
--- /dev/null
+++ b/examples/expo/android/app/proguard-rules.pro
@@ -0,0 +1,14 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# react-native-reanimated
+-keep class com.swmansion.reanimated.** { *; }
+-keep class com.facebook.react.turbomodule.** { *; }
+
+# Add any project specific keep options here:
diff --git a/examples/expo/android/app/src/androidTest/java/com/reactnativeagoraexampleexpo/DetoxTest.java b/examples/expo/android/app/src/androidTest/java/com/reactnativeagoraexampleexpo/DetoxTest.java
new file mode 100644
index 000000000..f365bf932
--- /dev/null
+++ b/examples/expo/android/app/src/androidTest/java/com/reactnativeagoraexampleexpo/DetoxTest.java
@@ -0,0 +1,29 @@
+package com.reactnativeagoraexampleexpo;
+
+import com.wix.detox.Detox;
+import com.wix.detox.config.DetoxConfig;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class DetoxTest {
+ @Rule
+ public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false);
+
+ @Test
+ public void runDetoxTests() {
+ DetoxConfig detoxConfig = new DetoxConfig();
+ detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
+ detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
+ detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);
+
+ Detox.runTests(mActivityRule, detoxConfig);
+ }
+}
diff --git a/examples/expo/android/app/src/debug/AndroidManifest.xml b/examples/expo/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..3ec2507ba
--- /dev/null
+++ b/examples/expo/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/examples/expo/android/app/src/main/AndroidManifest.xml b/examples/expo/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..d9c9da963
--- /dev/null
+++ b/examples/expo/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/android/app/src/main/assets/agora-logo.png b/examples/expo/android/app/src/main/assets/agora-logo.png
similarity index 100%
rename from example/android/app/src/main/assets/agora-logo.png
rename to examples/expo/android/app/src/main/assets/agora-logo.png
diff --git a/example/android/app/src/main/assets/dang.mp3 b/examples/expo/android/app/src/main/assets/dang.mp3
similarity index 100%
rename from example/android/app/src/main/assets/dang.mp3
rename to examples/expo/android/app/src/main/assets/dang.mp3
diff --git a/example/android/app/src/main/assets/ding.mp3 b/examples/expo/android/app/src/main/assets/ding.mp3
similarity index 100%
rename from example/android/app/src/main/assets/ding.mp3
rename to examples/expo/android/app/src/main/assets/ding.mp3
diff --git a/example/android/app/src/main/assets/effect.mp3 b/examples/expo/android/app/src/main/assets/effect.mp3
similarity index 100%
rename from example/android/app/src/main/assets/effect.mp3
rename to examples/expo/android/app/src/main/assets/effect.mp3
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraForegroundService.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraForegroundService.kt
new file mode 100644
index 000000000..cf68691c4
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraForegroundService.kt
@@ -0,0 +1,69 @@
+package com.reactnativeagoraexampleexpo
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+
+class AgoraForegroundService : Service() {
+ companion object {
+ private const val CHANNEL_ID = "AgoraForegroundServiceChannel"
+ private const val NOTIFICATION_ID = 1
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ createNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val notification = createNotification()
+ startForeground(NOTIFICATION_ID, notification)
+ return START_STICKY
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
+ }
+
+ private fun createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val serviceChannel = NotificationChannel(
+ CHANNEL_ID,
+ "Agora Foreground Service Channel",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Used to keep Agora SDK running in background"
+ enableVibration(false)
+ enableLights(false)
+ }
+
+ val manager = getSystemService(NotificationManager::class.java)
+ manager?.createNotificationChannel(serviceChannel)
+ }
+ }
+
+ private fun createNotification(): Notification {
+ val notificationIntent = Intent(this, MainActivity::class.java)
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ notificationIntent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+
+ return NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Agora RTC")
+ .setContentText("the call is ongoing...")
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentIntent(pendingIntent)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(true)
+ .build()
+ }
+}
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraServiceManager.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraServiceManager.kt
new file mode 100644
index 000000000..47aa97a04
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraServiceManager.kt
@@ -0,0 +1,32 @@
+package com.reactnativeagoraexampleexpo
+
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+
+class AgoraServiceManager(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule() {
+
+ override fun getName(): String = "AgoraServiceManager"
+
+ @ReactMethod
+ fun startForegroundService() {
+ val context = reactContext.applicationContext
+ val serviceIntent = Intent(context, AgoraForegroundService::class.java)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(serviceIntent)
+ } else {
+ context.startService(serviceIntent)
+ }
+ }
+
+ @ReactMethod
+ fun stopForegroundService() {
+ val context = reactContext.applicationContext
+ val serviceIntent = Intent(context, AgoraForegroundService::class.java)
+ context.stopService(serviceIntent)
+ }
+}
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraServicePackage.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraServicePackage.kt
new file mode 100644
index 000000000..9a5dcaddc
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/AgoraServicePackage.kt
@@ -0,0 +1,16 @@
+package com.reactnativeagoraexampleexpo
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class AgoraServicePackage : ReactPackage {
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ return listOf(AgoraServiceManager(reactContext))
+ }
+}
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/MainActivity.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/MainActivity.kt
new file mode 100644
index 000000000..8d4fbcd3e
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/MainActivity.kt
@@ -0,0 +1,61 @@
+package com.reactnativeagoraexampleexpo
+
+import android.os.Build
+import android.os.Bundle
+
+import com.facebook.react.ReactActivityDelegate
+import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
+import com.facebook.react.defaults.DefaultReactActivityDelegate
+
+import expo.modules.ReactActivityDelegateWrapper
+import io.agora.rtc.ng.react.AgoraPIPActivity
+
+class MainActivity : AgoraPIPActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ // Set the theme to AppTheme BEFORE onCreate to support
+ // coloring the background, status bar, and navigation bar.
+ // This is required for expo-splash-screen.
+ setTheme(R.style.AppTheme);
+ super.onCreate(null)
+ }
+
+ /**
+ * Returns the name of the main component registered from JavaScript. This is used to schedule
+ * rendering of the component.
+ */
+ override fun getMainComponentName(): String = "main"
+
+ /**
+ * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
+ * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
+ */
+ override fun createReactActivityDelegate(): ReactActivityDelegate {
+ return ReactActivityDelegateWrapper(
+ this,
+ BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
+ object : DefaultReactActivityDelegate(
+ this,
+ mainComponentName,
+ fabricEnabled
+ ){})
+ }
+
+ /**
+ * Align the back button behavior with Android S
+ * where moving root activities to background instead of finishing activities.
+ * @see onBackPressed
+ */
+ override fun invokeDefaultOnBackPressed() {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
+ if (!moveTaskToBack(false)) {
+ // For non-root activities, use the default implementation to finish them.
+ super.invokeDefaultOnBackPressed()
+ }
+ return
+ }
+
+ // Use the default back button implementation on Android S
+ // because it's doing more than [Activity.moveTaskToBack] in fact.
+ super.invokeDefaultOnBackPressed()
+ }
+}
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/MainApplication.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/MainApplication.kt
new file mode 100644
index 000000000..d872967fb
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/MainApplication.kt
@@ -0,0 +1,58 @@
+package com.reactnativeagoraexampleexpo
+
+import android.app.Application
+import android.content.res.Configuration
+
+import com.facebook.react.PackageList
+import com.facebook.react.ReactApplication
+import com.facebook.react.ReactNativeHost
+import com.facebook.react.ReactPackage
+import com.facebook.react.ReactHost
+import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
+import com.facebook.react.defaults.DefaultReactNativeHost
+import com.facebook.react.soloader.OpenSourceMergedSoMapping
+import com.facebook.soloader.SoLoader
+
+import expo.modules.ApplicationLifecycleDispatcher
+import expo.modules.ReactNativeHostWrapper
+
+class MainApplication : Application(), ReactApplication {
+
+ override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
+ this,
+ object : DefaultReactNativeHost(this) {
+ override fun getPackages(): List =
+ PackageList(this).packages.apply {
+ // Packages that cannot be autolinked yet can be added manually here, for example:
+ // add(MyReactNativePackage())
+ add(VideoRawDataNativeModulePackage())
+ add(AgoraServicePackage())
+ }
+
+ override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
+
+ override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
+
+ override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
+ override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
+ }
+ )
+
+ override val reactHost: ReactHost
+ get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
+
+ override fun onCreate() {
+ super.onCreate()
+ SoLoader.init(this, OpenSourceMergedSoMapping)
+ if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
+ // If you opted-in for the New Architecture, we load the native entry point for this app.
+ load()
+ }
+ ApplicationLifecycleDispatcher.onApplicationCreate(this)
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
+ }
+}
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/VideoRawDataNativeModule.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/VideoRawDataNativeModule.kt
new file mode 100644
index 000000000..9c013871e
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/VideoRawDataNativeModule.kt
@@ -0,0 +1,100 @@
+package com.reactnativeagoraexampleexpo
+
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.bridge.ReactContextBaseJavaModule
+import com.facebook.react.bridge.ReactMethod
+import io.agora.base.VideoFrame
+import io.agora.rtc2.IRtcEngineEventHandler
+import io.agora.rtc2.RtcEngine
+import io.agora.rtc2.RtcEngineConfig
+import io.agora.rtc2.video.IVideoFrameObserver
+import java.nio.ByteBuffer
+
+class VideoRawDataNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
+ private var appId: String? = null
+ private var rtcEngine: RtcEngine? = null
+ private val reactContext: ReactApplicationContext = reactContext
+
+ override fun getName(): String {
+ return "VideoRawDataNativeModule"
+ }
+
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ fun initialize(appId: String) {
+ this.appId = appId
+ try {
+ val config = RtcEngineConfig()
+ config.mAppId = appId
+ config.mContext = reactContext
+ config.mEventHandler = object : IRtcEngineEventHandler() {}
+
+ rtcEngine = RtcEngine.create(config)
+
+ rtcEngine?.registerVideoFrameObserver(object : IVideoFrameObserver {
+ override fun onCaptureVideoFrame(sourceType: Int, videoFrame: VideoFrame): Boolean {
+ videoFrame?.apply {
+ val i420Buffer = buffer.toI420()
+ // Make it grey: Set U and V (chroma) components to neutral value
+ val neutralValue: Byte = 128.toByte()
+ val dataU = i420Buffer.dataU
+ val dataV = i420Buffer.dataV
+
+ while (dataU.hasRemaining()) {
+ dataU.put(neutralValue)
+ }
+
+ while (dataV.hasRemaining()) {
+ dataV.put(neutralValue)
+ }
+
+ videoFrame.replaceBuffer(i420Buffer, videoFrame.rotation, videoFrame.timestampNs)
+ }
+ return true
+ }
+
+ override fun onPreEncodeVideoFrame(sourceType: Int, videoFrame: VideoFrame): Boolean {
+ return false
+ }
+
+ override fun onMediaPlayerVideoFrame(videoFrame: VideoFrame, mediaPlayerId: Int): Boolean {
+ return false
+ }
+
+ override fun onRenderVideoFrame(channelId: String, uid: Int, videoFrame: VideoFrame): Boolean {
+ return false
+ }
+
+ override fun getVideoFrameProcessMode(): Int {
+ return IVideoFrameObserver.PROCESS_MODE_READ_WRITE
+ }
+
+ override fun getVideoFormatPreference(): Int {
+ return IVideoFrameObserver.VIDEO_PIXEL_I420
+ }
+
+ override fun getRotationApplied(): Boolean {
+ return false
+ }
+
+ override fun getMirrorApplied(): Boolean {
+ return false
+ }
+
+ override fun getObservedFramePosition(): Int {
+ return IVideoFrameObserver.POSITION_POST_CAPTURER
+ }
+ })
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ fun releaseModule() {
+ rtcEngine?.let {
+ it.registerVideoFrameObserver(null)
+ RtcEngine.destroy()
+ rtcEngine = null
+ }
+ }
+}
diff --git a/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/VideoRawDataNativeModulePackage.kt b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/VideoRawDataNativeModulePackage.kt
new file mode 100644
index 000000000..f5c36e43c
--- /dev/null
+++ b/examples/expo/android/app/src/main/java/com/reactnativeagoraexampleexpo/VideoRawDataNativeModulePackage.kt
@@ -0,0 +1,20 @@
+package com.reactnativeagoraexampleexpo
+
+import com.facebook.react.ReactPackage
+import com.facebook.react.bridge.NativeModule
+import com.facebook.react.bridge.ReactApplicationContext
+import com.facebook.react.uimanager.ViewManager
+
+class VideoRawDataNativeModulePackage:ReactPackage
+{
+
+ override fun createNativeModules(reactContext: ReactApplicationContext): List {
+ val modules: MutableList = ArrayList()
+ modules.add(VideoRawDataNativeModule(reactContext))
+ return modules
+ }
+
+ override fun createViewManagers(reactContext: ReactApplicationContext): List> {
+ return emptyList()
+ }
+}
diff --git a/examples/expo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png b/examples/expo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png
new file mode 100644
index 000000000..31df827b1
Binary files /dev/null and b/examples/expo/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png differ
diff --git a/examples/expo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png b/examples/expo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png
new file mode 100644
index 000000000..ef243aab6
Binary files /dev/null and b/examples/expo/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png differ
diff --git a/examples/expo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png b/examples/expo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png
new file mode 100644
index 000000000..e9d547451
Binary files /dev/null and b/examples/expo/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png differ
diff --git a/examples/expo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png b/examples/expo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png
new file mode 100644
index 000000000..d61da15d2
Binary files /dev/null and b/examples/expo/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png differ
diff --git a/examples/expo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png b/examples/expo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png
new file mode 100644
index 000000000..4aeed11d0
Binary files /dev/null and b/examples/expo/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png differ
diff --git a/examples/expo/android/app/src/main/res/drawable/ic_launcher_background.xml b/examples/expo/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..883b2a080
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,6 @@
+
+
+ -
+
+
+
\ No newline at end of file
diff --git a/examples/expo/android/app/src/main/res/drawable/rn_edit_text_material.xml b/examples/expo/android/app/src/main/res/drawable/rn_edit_text_material.xml
new file mode 100644
index 000000000..5c25e728e
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/drawable/rn_edit_text_material.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..3941bea9b
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..3941bea9b
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..7fae0ccbc
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 000000000..ac03dbf69
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..afa0a4ef4
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..78aaf4541
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 000000000..e1173a94d
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..c4f6e101e
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..7a0f085fa
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 000000000..ff086fdc3
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..6c2d40bf5
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..730e3fa55
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 000000000..f7f1d0690
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..345261586
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..b11a322ab
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 000000000..49a464ee3
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b51fd15c2
Binary files /dev/null and b/examples/expo/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/examples/expo/android/app/src/main/res/values-night/colors.xml b/examples/expo/android/app/src/main/res/values-night/colors.xml
new file mode 100644
index 000000000..3c05de5be
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/values-night/colors.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/expo/android/app/src/main/res/values/colors.xml b/examples/expo/android/app/src/main/res/values/colors.xml
new file mode 100644
index 000000000..f387b9011
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+ #ffffff
+ #ffffff
+ #023c69
+ #ffffff
+
\ No newline at end of file
diff --git a/examples/expo/android/app/src/main/res/values/strings.xml b/examples/expo/android/app/src/main/res/values/strings.xml
new file mode 100644
index 000000000..a5a82c328
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ react-native-agora-example-expo
+ contain
+ false
+
\ No newline at end of file
diff --git a/examples/expo/android/app/src/main/res/values/styles.xml b/examples/expo/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..72a3b967f
--- /dev/null
+++ b/examples/expo/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/example/android/app/src/main/res/xml/network_security_config.xml b/examples/expo/android/app/src/main/res/xml/network_security_config.xml
similarity index 100%
rename from example/android/app/src/main/res/xml/network_security_config.xml
rename to examples/expo/android/app/src/main/res/xml/network_security_config.xml
diff --git a/examples/expo/android/build.gradle b/examples/expo/android/build.gradle
new file mode 100644
index 000000000..175dbe271
--- /dev/null
+++ b/examples/expo/android/build.gradle
@@ -0,0 +1,64 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath('com.android.tools.build:gradle')
+ classpath('com.facebook.react:react-native-gradle-plugin')
+ classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
+ }
+}
+
+def reactNativeAndroidDir = new File(
+ providers.exec {
+ workingDir(rootDir)
+ commandLine("node", "--print", "require.resolve('react-native/package.json')")
+ }.standardOutput.asText.get().trim(),
+ "../android"
+)
+
+allprojects {
+ repositories {
+ maven {
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
+ url(reactNativeAndroidDir)
+ }
+
+ google()
+ mavenCentral()
+ maven {
+ url("$rootDir/../node_modules/detox/Detox-android")
+ }
+ maven { url 'https://www.jitpack.io' }
+ }
+
+ afterEvaluate { project ->
+ if (project.hasProperty('android')) {
+ android {
+ packagingOptions {
+ resources {
+ excludes += [
+ 'META-INF/LICENSE.md',
+ 'META-INF/LICENSE-notice.md',
+ 'META-INF/DEPENDENCIES'
+ ]
+ }
+ jniLibs {
+ pickFirsts += [
+ 'lib/arm64-v8a/libfbjni.so',
+ 'lib/armeabi-v7a/libfbjni.so',
+ 'lib/x86/libfbjni.so',
+ 'lib/x86_64/libfbjni.so'
+ ]
+ }
+ }
+ }
+ }
+ }
+}
+
+apply plugin: "expo-root-project"
+apply plugin: "com.facebook.react.rootproject"
diff --git a/examples/expo/android/gradle.properties b/examples/expo/android/gradle.properties
new file mode 100644
index 000000000..79e324254
--- /dev/null
+++ b/examples/expo/android/gradle.properties
@@ -0,0 +1,61 @@
+# 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.
+# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
+org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
+
+# 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
+
+# Enable AAPT2 PNG crunching
+android.enablePngCrunchInReleaseBuilds=true
+
+# Use this property to specify which architecture you want to build.
+# You can also override it from the CLI using
+# ./gradlew -PreactNativeArchitectures=x86_64
+reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
+
+# Use this property to enable support to the new architecture.
+# This will allow you to use TurboModules and the Fabric render in
+# your application. You should enable this flag either if you want
+# to write custom TurboModules/Fabric components OR use libraries that
+# are providing them.
+newArchEnabled=true
+
+# Use this property to enable or disable the Hermes JS engine.
+# If set to false, you will be using JSC instead.
+hermesEnabled=true
+
+# Enable GIF support in React Native images (~200 B increase)
+expo.gif.enabled=true
+# Enable webp support in React Native images (~85 KB increase)
+expo.webp.enabled=true
+# Enable animated webp support (~3.4 MB increase)
+# Disabled by default because iOS doesn't support animated webp
+expo.webp.animated=false
+
+# Enable network inspector
+EX_DEV_CLIENT_NETWORK_INSPECTOR=true
+
+# Use legacy packaging to compress native libraries in the resulting APK.
+expo.useLegacyPackaging=false
+
+
+# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
+expo.edgeToEdgeEnabled=true
+android.minSdkVersion=24
\ No newline at end of file
diff --git a/examples/expo/android/gradle/wrapper/gradle-wrapper.jar b/examples/expo/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..a4b76b953
Binary files /dev/null and b/examples/expo/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/examples/expo/android/gradle/wrapper/gradle-wrapper.properties b/examples/expo/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..37f853b1c
--- /dev/null
+++ b/examples/expo/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/examples/expo/android/gradlew b/examples/expo/android/gradlew
new file mode 100755
index 000000000..f3b75f3b0
--- /dev/null
+++ b/examples/expo/android/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# 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
+#
+# https://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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# 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 ;; #(
+ MSYS* | 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
+ if ! command -v java >/dev/null 2>&1
+ then
+ 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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/example/android/gradlew.bat b/examples/expo/android/gradlew.bat
similarity index 100%
rename from example/android/gradlew.bat
rename to examples/expo/android/gradlew.bat
diff --git a/examples/expo/android/settings.gradle b/examples/expo/android/settings.gradle
new file mode 100644
index 000000000..dbdd2b742
--- /dev/null
+++ b/examples/expo/android/settings.gradle
@@ -0,0 +1,39 @@
+pluginManagement {
+ def reactNativeGradlePlugin = new File(
+ providers.exec {
+ workingDir(rootDir)
+ commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
+ }.standardOutput.asText.get().trim()
+ ).getParentFile().absolutePath
+ includeBuild(reactNativeGradlePlugin)
+
+ def expoPluginsPath = new File(
+ providers.exec {
+ workingDir(rootDir)
+ commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
+ }.standardOutput.asText.get().trim(),
+ "../android/expo-gradle-plugin"
+ ).absolutePath
+ includeBuild(expoPluginsPath)
+}
+
+plugins {
+ id("com.facebook.react.settings")
+ id("expo-autolinking-settings")
+}
+
+extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
+ if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
+ ex.autolinkLibrariesFromCommand()
+ } else {
+ ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
+ }
+}
+expoAutolinking.useExpoModules()
+
+rootProject.name = 'react-native-agora-example-expo'
+
+expoAutolinking.useExpoVersionCatalog()
+
+include ':app'
+includeBuild(expoAutolinking.reactNativeGradlePlugin)
diff --git a/examples/expo/app.json b/examples/expo/app.json
new file mode 100644
index 000000000..61c8fa8b7
--- /dev/null
+++ b/examples/expo/app.json
@@ -0,0 +1,62 @@
+{
+ "expo": {
+ "plugins": [
+ "expo-router",
+ [
+ "expo-camera",
+ {
+ "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera for video calls",
+ "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone for audio calls"
+ }
+ ],
+ [
+ "expo-build-properties",
+ {
+ "android": {
+ "minSdkVersion": 24
+ },
+ "ios": {
+ "deploymentTarget": "15.1"
+ }
+ }
+ ]
+ ],
+ "name": "react-native-agora-example-expo",
+ "slug": "react-native-agora-example-expo",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "scheme": "agoraexpo",
+ "newArchEnabled": true,
+ "splash": {
+ "image": "./assets/splash-icon.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "io.agora.react-native-agora-example-expo"
+ },
+ "android": {
+ "permissions": [
+ "android.permission.CAMERA",
+ "android.permission.RECORD_AUDIO",
+ "android.permission.MODIFY_AUDIO_SETTINGS",
+ "android.permission.ACCESS_WIFI_STATE",
+ "android.permission.ACCESS_NETWORK_STATE",
+ "android.permission.BLUETOOTH",
+ "android.permission.FOREGROUND_SERVICE"
+ ],
+ "allowBackup": false,
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#ffffff"
+ },
+ "edgeToEdgeEnabled": true,
+ "package": "com.reactnativeagoraexampleexpo"
+ },
+ "web": {
+ "favicon": "./assets/favicon.png"
+ }
+ }
+}
diff --git a/examples/expo/app/_layout.tsx b/examples/expo/app/_layout.tsx
new file mode 100644
index 000000000..18a360d52
--- /dev/null
+++ b/examples/expo/app/_layout.tsx
@@ -0,0 +1,99 @@
+import { Stack } from 'expo-router';
+
+import React, { useState } from 'react';
+
+import { Keyboard, StyleSheet } from 'react-native';
+import { AgoraPipState } from 'react-native-agora';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import { LogSink } from '../src/components/LogSink';
+
+import { AgoraText } from '../src/components/ui';
+
+import { PipStateConsumer, PipStateProvider } from '../src/context/pip';
+
+import Advanced from './examples/advanced';
+import Basic from './examples/basic';
+import Hooks from './examples/hook';
+
+const Header = () => {
+ const [visible, setVisible] = useState(false);
+
+ const toggleOverlay = () => {
+ setVisible(!visible);
+ };
+
+ return (
+ <>
+ Logs
+ {visible && }
+ >
+ );
+};
+
+export default function RootLayout() {
+ return (
+
+
+
+ {(context) => (
+ {
+ Keyboard.dismiss();
+ return false;
+ }}
+ >
+
+
+ {Basic.data.map((item) => (
+ ,
+ headerShown:
+ context.pipState !== AgoraPipState.pipStateStarted,
+ }}
+ />
+ ))}
+ {Advanced.data.map((item) => (
+ ,
+ headerShown:
+ context.pipState !== AgoraPipState.pipStateStarted,
+ }}
+ />
+ ))}
+ {Hooks.data.map((item) => (
+ ,
+ headerShown:
+ context.pipState !== AgoraPipState.pipStateStarted,
+ }}
+ />
+ ))}
+
+
+ )}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+});
diff --git a/examples/expo/app/examples/advanced/AudioCallRoute/AudioCallRoute.tsx b/examples/expo/app/examples/advanced/AudioCallRoute/AudioCallRoute.tsx
new file mode 100644
index 000000000..cf1abc177
--- /dev/null
+++ b/examples/expo/app/examples/advanced/AudioCallRoute/AudioCallRoute.tsx
@@ -0,0 +1,162 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import { AgoraDivider, AgoraSwitch } from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ defaultToSpeaker: boolean;
+ speakerOn: boolean;
+}
+
+export default class AudioCallRoute
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ defaultToSpeaker: true,
+ speakerOn: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: setDefaultAudioRouteToSpeakerphone
+ */
+ protected setDefaultAudioRouteToSpeakerphone() {
+ const { defaultToSpeaker } = this.state;
+ this.engine?.setDefaultAudioRouteToSpeakerphone(!defaultToSpeaker);
+ this.setState({
+ defaultToSpeaker: !defaultToSpeaker,
+ });
+ }
+
+ /**
+ * Step 3-2: setEnableSpeakerphone
+ */
+ protected setEnableSpeakerphone() {
+ const { speakerOn } = this.state;
+ this.engine?.setEnableSpeakerphone(!speakerOn);
+ this.setState({
+ speakerOn: !speakerOn,
+ });
+ }
+
+ onAudioRoutingChanged(routing: number): void {
+ this.info('onAudioRoutingChanged', 'routing', routing);
+ }
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { defaultToSpeaker, speakerOn, joinChannelSuccess } = this.state;
+ return (
+ <>
+ {
+ this.setDefaultAudioRouteToSpeakerphone();
+ }}
+ />
+
+ {
+ this.setEnableSpeakerphone();
+ }}
+ />
+
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ // const { startAudioMixing, pauseAudioMixing } = this.state;
+ return <>>;
+ }
+}
diff --git a/examples/expo/app/examples/advanced/AudioMixing/AudioMixing.tsx b/examples/expo/app/examples/advanced/AudioMixing/AudioMixing.tsx
new file mode 100644
index 000000000..a9f37a0db
--- /dev/null
+++ b/examples/expo/app/examples/advanced/AudioMixing/AudioMixing.tsx
@@ -0,0 +1,270 @@
+import React, { ReactElement } from 'react';
+import {
+ AudioMixingReasonType,
+ AudioMixingStateType,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraSwitch,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { getResourcePath } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ filePath: string;
+ loopback: boolean;
+ cycle: number;
+ startPos: number;
+ startAudioMixing: boolean;
+ pauseAudioMixing: boolean;
+}
+
+export default class AudioMixing
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ filePath: getResourcePath('effect.mp3'),
+ loopback: false,
+ cycle: -1,
+ startPos: 0,
+ startAudioMixing: false,
+ pauseAudioMixing: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: startAudioMixing
+ */
+ startAudioMixing = () => {
+ const { filePath, loopback, cycle, startPos } = this.state;
+ if (!filePath) {
+ this.error('filePath is invalid');
+ return;
+ }
+ if (cycle < -1) {
+ this.error('cycle is invalid');
+ return;
+ }
+ if (startPos < 0) {
+ this.error('startPos is invalid');
+ return;
+ }
+
+ this.engine?.startAudioMixing(filePath, loopback, cycle, startPos);
+ };
+
+ /**
+ * Step 3-2 (Optional): pauseAudioMixing
+ */
+ pauseAudioMixing = () => {
+ this.engine?.pauseAudioMixing();
+ };
+
+ /**
+ * Step 3-3 (Optional): resumeAudioMixing
+ */
+ resumeAudioMixing = () => {
+ this.engine?.resumeAudioMixing();
+ };
+
+ /**
+ * Step 3-4 (Optional): getAudioMixingCurrentPosition
+ */
+ getAudioMixingCurrentPosition = () => {
+ const position = this.engine?.getAudioMixingCurrentPosition();
+ const duration = this.engine?.getAudioMixingDuration();
+ this.debug(
+ 'getAudioMixingCurrentPosition',
+ 'position',
+ position,
+ 'duration',
+ duration
+ );
+ };
+
+ /**
+ * Step 3-5: stopAudioMixing
+ */
+ stopAudioMixing = () => {
+ this.engine?.stopAudioMixing();
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onAudioMixingStateChanged(
+ state: AudioMixingStateType,
+ reason: AudioMixingReasonType
+ ) {
+ this.info('onAudioMixingStateChanged', 'state', state, 'reason', reason);
+ switch (state) {
+ case AudioMixingStateType.AudioMixingStatePlaying:
+ this.setState({ startAudioMixing: true, pauseAudioMixing: false });
+ break;
+ case AudioMixingStateType.AudioMixingStatePaused:
+ this.setState({ pauseAudioMixing: true });
+ break;
+ case AudioMixingStateType.AudioMixingStateStopped:
+ case AudioMixingStateType.AudioMixingStateFailed:
+ this.setState({ startAudioMixing: false });
+ break;
+ }
+ }
+
+ onAudioMixingFinished() {
+ this.info('AudioMixingFinished');
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { filePath, loopback } = this.state;
+ return (
+ <>
+ {
+ this.setState({ filePath: text });
+ }}
+ placeholder={'filePath'}
+ value={filePath}
+ />
+ {
+ this.setState({ loopback: value });
+ }}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ cycle: text === '' ? this.createState().cycle : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`cycle (defaults: ${this.createState().cycle})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ startPos: text === '' ? this.createState().startPos : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`startPos (defaults: ${this.createState().startPos})`}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startAudioMixing, pauseAudioMixing } = this.state;
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/AudioSpectrum/AudioSpectrum.tsx b/examples/expo/app/examples/advanced/AudioSpectrum/AudioSpectrum.tsx
new file mode 100644
index 000000000..e1d269085
--- /dev/null
+++ b/examples/expo/app/examples/advanced/AudioSpectrum/AudioSpectrum.tsx
@@ -0,0 +1,226 @@
+import React, { ReactElement } from 'react';
+import { Dimensions } from 'react-native';
+import {
+ AudioSpectrumData,
+ ChannelProfileType,
+ ClientRoleType,
+ IAudioSpectrumObserver,
+ IRtcEngineEventHandler,
+ UserAudioSpectrumInfo,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+import { LineChart } from 'react-native-chart-kit';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import { AgoraButton, AgoraTextInput } from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ intervalInMS: number;
+ enableAudioSpectrumMonitor: boolean;
+ audioSpectrumData: number[];
+}
+
+export default class AudioSpectrum
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, IAudioSpectrumObserver
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ intervalInMS: 500,
+ enableAudioSpectrumMonitor: false,
+ audioSpectrumData: [],
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+
+ this.registerAudioSpectrumObserver();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: registerAudioSpectrumObserver
+ */
+ registerAudioSpectrumObserver = () => {
+ this.engine?.registerAudioSpectrumObserver(this);
+ };
+
+ /**
+ * Step 3-2: enableAudioSpectrumMonitor
+ */
+ enableAudioSpectrumMonitor = () => {
+ const { intervalInMS } = this.state;
+ this.engine?.enableAudioSpectrumMonitor(intervalInMS);
+ this.setState({ enableAudioSpectrumMonitor: true });
+ };
+
+ /**
+ * Step 3-3: disableAudioSpectrumMonitor
+ */
+ disableAudioSpectrumMonitor = () => {
+ this.engine?.disableAudioSpectrumMonitor();
+ this.setState({ enableAudioSpectrumMonitor: false });
+ };
+
+ /**
+ * Step 3-4: unregisterAudioSpectrumObserver
+ */
+ unregisterAudioSpectrumObserver = () => {
+ this.engine?.unregisterAudioSpectrumObserver(this);
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.unregisterAudioSpectrumObserver();
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onLocalAudioSpectrum(data: AudioSpectrumData): boolean {
+ this.info('onLocalAudioSpectrum', 'data', data);
+ this.setState({ audioSpectrumData: data.audioSpectrumData ?? [] });
+ return true;
+ }
+
+ onRemoteAudioSpectrum(
+ spectrums: UserAudioSpectrumInfo[],
+ spectrumNumber: number
+ ): boolean {
+ this.info(
+ 'onRemoteAudioSpectrum',
+ 'spectrums',
+ spectrums,
+ 'spectrumNumber',
+ spectrumNumber
+ );
+ return true;
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { enableAudioSpectrumMonitor, audioSpectrumData } = this.state;
+ return (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ intervalInMS:
+ text === '' ? this.createState().intervalInMS : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`intervalInMS (defaults: ${
+ this.createState().intervalInMS
+ })`}
+ />
+ {enableAudioSpectrumMonitor && audioSpectrumData.length > 0 ? (
+ <>
+ 'white',
+ }}
+ bezier
+ />
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { enableAudioSpectrumMonitor } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/BeautyEffect/BeautyEffect.tsx b/examples/expo/app/examples/advanced/BeautyEffect/BeautyEffect.tsx
new file mode 100644
index 000000000..26db661fc
--- /dev/null
+++ b/examples/expo/app/examples/advanced/BeautyEffect/BeautyEffect.tsx
@@ -0,0 +1,256 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ LighteningContrastLevel,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ lighteningContrastLevel: LighteningContrastLevel;
+ lighteningLevel: number;
+ smoothnessLevel: number;
+ rednessLevel: number;
+ sharpnessLevel: number;
+ enableBeautyEffect: boolean;
+}
+
+export default class BeautyEffect
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ lighteningContrastLevel: LighteningContrastLevel.LighteningContrastNormal,
+ lighteningLevel: 0,
+ smoothnessLevel: 0,
+ rednessLevel: 0,
+ sharpnessLevel: 0,
+ enableBeautyEffect: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ this.engine?.enableExtension(
+ 'agora_video_filters_clear_vision',
+ 'clear_vision',
+ true
+ );
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // This case works if startPreview without joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: enableBeautyEffect
+ */
+ enableBeautyEffect = () => {
+ const {
+ lighteningContrastLevel,
+ lighteningLevel,
+ smoothnessLevel,
+ rednessLevel,
+ sharpnessLevel,
+ } = this.state;
+
+ this.engine?.setBeautyEffectOptions(true, {
+ lighteningContrastLevel,
+ lighteningLevel,
+ smoothnessLevel,
+ rednessLevel,
+ sharpnessLevel,
+ });
+ this.setState({ enableBeautyEffect: true });
+ };
+
+ /**
+ * Step 3-2: disableBeautyEffect
+ */
+ disableBeautyEffect = () => {
+ this.engine?.setBeautyEffectOptions(false, {});
+ this.setState({ enableBeautyEffect: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ lighteningContrastLevel,
+ lighteningLevel,
+ smoothnessLevel,
+ rednessLevel,
+ sharpnessLevel,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ lighteningContrastLevel: value });
+ }}
+ />
+
+ {
+ this.setState({
+ lighteningLevel: value,
+ });
+ }}
+ />
+
+ {
+ this.setState({
+ smoothnessLevel: value,
+ });
+ }}
+ />
+
+ {
+ this.setState({
+ rednessLevel: value,
+ });
+ }}
+ />
+
+ {
+ this.setState({
+ sharpnessLevel: value,
+ });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess, enableBeautyEffect } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/ChannelMediaRelay/ChannelMediaRelay.tsx b/examples/expo/app/examples/advanced/ChannelMediaRelay/ChannelMediaRelay.tsx
new file mode 100644
index 000000000..a79d4713a
--- /dev/null
+++ b/examples/expo/app/examples/advanced/ChannelMediaRelay/ChannelMediaRelay.tsx
@@ -0,0 +1,246 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelMediaRelayError,
+ ChannelMediaRelayState,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraText,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ destChannelNames: string[];
+ startChannelMediaRelay: boolean;
+ pauseAllChannelMediaRelay: boolean;
+}
+
+export default class ChannelMediaRelay
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ destChannelNames: [],
+ startChannelMediaRelay: false,
+ pauseAllChannelMediaRelay: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: startChannelMediaRelay
+ */
+ startOrUpdateChannelMediaRelay = () => {
+ const { channelId, token, uid, destChannelNames } = this.state;
+ if (destChannelNames.length <= 0) {
+ this.error('destChannelNames is invalid');
+ return;
+ }
+
+ this.engine?.startOrUpdateChannelMediaRelay({
+ // Configure src info
+ // Set channel name defaults to current
+ // Set uid defaults to local
+ srcInfo: { channelName: channelId, uid, token },
+ // Configure dest infos
+ destInfos: destChannelNames.map((value) => {
+ return {
+ channelName: value,
+ uid: 0,
+ token: '',
+ };
+ }),
+ destCount: destChannelNames.length,
+ });
+ };
+
+ /**
+ * Step 3-3 (Optional): pauseAllChannelMediaRelay
+ */
+ pauseAllChannelMediaRelay = () => {
+ this.engine?.pauseAllChannelMediaRelay();
+ this.setState({ pauseAllChannelMediaRelay: true });
+ };
+
+ /**
+ * Step 3-4 (Optional): resumeAllChannelMediaRelay
+ */
+ resumeAllChannelMediaRelay = () => {
+ this.engine?.resumeAllChannelMediaRelay();
+ this.setState({ pauseAllChannelMediaRelay: false });
+ };
+
+ /**
+ * Step 3-5: stopChannelMediaRelay
+ */
+ stopChannelMediaRelay = () => {
+ this.engine?.stopChannelMediaRelay();
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onChannelMediaRelayStateChanged(
+ state: ChannelMediaRelayState,
+ code: ChannelMediaRelayError
+ ) {
+ this.info('onChannelMediaRelayStateChanged', 'state', state, 'code', code);
+ switch (state) {
+ case ChannelMediaRelayState.RelayStateIdle:
+ this.setState({ startChannelMediaRelay: false });
+ break;
+ case ChannelMediaRelayState.RelayStateConnecting:
+ break;
+ case ChannelMediaRelayState.RelayStateRunning:
+ this.setState({
+ startChannelMediaRelay: true,
+ pauseAllChannelMediaRelay: false,
+ });
+ break;
+ case ChannelMediaRelayState.RelayStateFailure:
+ this.setState({ startChannelMediaRelay: false });
+ break;
+ }
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { destChannelNames } = this.state;
+ return (
+ <>
+ {
+ this.setState({ destChannelNames: text ? text.split(' ') : [] });
+ }}
+ placeholder={'destChannelNames (split by blank)'}
+ value={destChannelNames.join(' ')}
+ />
+ {`destCount: ${destChannelNames.length}`}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const {
+ joinChannelSuccess,
+ startChannelMediaRelay,
+ pauseAllChannelMediaRelay,
+ } = this.state;
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/ContentInspect/ContentInspect.tsx b/examples/expo/app/examples/advanced/ContentInspect/ContentInspect.tsx
new file mode 100644
index 000000000..6eb168637
--- /dev/null
+++ b/examples/expo/app/examples/advanced/ContentInspect/ContentInspect.tsx
@@ -0,0 +1,237 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ ContentInspectModule,
+ ContentInspectResult,
+ ContentInspectType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraText,
+ AgoraTextInput,
+ AgoraView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ modules: ContentInspectModule[];
+ type: ContentInspectType;
+ interval: number;
+ enableContentInspect: boolean;
+}
+
+export default class ContentInspect
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ modules: [],
+ type: ContentInspectType.ContentInspectModeration,
+ interval: 1,
+ enableContentInspect: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // This case works if startPreview without joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: enableContentInspect
+ */
+ enableContentInspect = () => {
+ const { modules } = this.state;
+ if (modules.length <= 0 || modules.length > 32) {
+ this.error('modules length is invalid');
+ return;
+ }
+
+ this.engine?.enableContentInspect(true, {
+ modules,
+ moduleCount: modules.length,
+ });
+ this.setState({ enableContentInspect: true });
+ };
+
+ /**
+ * Step 3-2: disableContentInspect
+ */
+ disableContentInspect = () => {
+ this.engine?.enableContentInspect(false, {});
+ this.setState({ enableContentInspect: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onContentInspectResult(result: ContentInspectResult) {
+ this.info('onContentInspectResult', 'result', result);
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { modules, type, interval } = this.state;
+ return (
+ <>
+ {
+ this.setState({ type: value });
+ }}
+ />
+
+ {
+ if (interval <= 0) {
+ this.error('interval is invalid');
+ return;
+ }
+ this.setState((preState) => {
+ return {
+ modules: [
+ ...preState.modules,
+ { type: preState.type, interval: preState.interval },
+ ],
+ };
+ });
+ }}
+ />
+ {
+ this.setState((preState) => {
+ preState.modules.pop();
+ return {
+ modules: preState.modules,
+ };
+ });
+ }}
+ />
+
+
+ {`moduleCount: ${modules.length}`}
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ interval: text === '' ? this.createState().interval : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`interval (defaults: ${this.createState().interval})`}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess, enableContentInspect } =
+ this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/DirectCdnStreaming/DirectCdnStreaming.tsx b/examples/expo/app/examples/advanced/DirectCdnStreaming/DirectCdnStreaming.tsx
new file mode 100644
index 000000000..db71b714b
--- /dev/null
+++ b/examples/expo/app/examples/advanced/DirectCdnStreaming/DirectCdnStreaming.tsx
@@ -0,0 +1,382 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ DegradationPreference,
+ DirectCdnStreamingReason,
+ DirectCdnStreamingState,
+ DirectCdnStreamingStats,
+ IDirectCdnStreamingEventHandler,
+ IRtcEngineEventHandler,
+ OrientationMode,
+ RtcConnection,
+ RtcStats,
+ VideoCodecType,
+ VideoMirrorModeType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraTextInput,
+ AgoraView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ url: string;
+ codecType: VideoCodecType;
+ width: number;
+ height: number;
+ frameRate: number;
+ bitrate: number;
+ minBitrate: number;
+ orientationMode: OrientationMode;
+ degradationPreference: DegradationPreference;
+ mirrorMode: VideoMirrorModeType;
+ startDirectCdnStreaming: boolean;
+}
+
+export default class DirectCdnStreaming
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, IDirectCdnStreamingEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ url: 'rtmp://vid-218.push.chinanetcenter.broadcastapp.agora.io/live/test',
+ codecType: VideoCodecType.VideoCodecH264,
+ width: 640,
+ height: 360,
+ frameRate: 15,
+ bitrate: 0,
+ minBitrate: -1,
+ // ⚠️ can not set OrientationMode.OrientationModeAdaptive
+ orientationMode: OrientationMode.OrientationModeFixedLandscape,
+ degradationPreference: DegradationPreference.MaintainQuality,
+ mirrorMode: VideoMirrorModeType.VideoMirrorModeDisabled,
+ startDirectCdnStreaming: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1 (Optional): setDirectCdnStreamingVideoConfiguration
+ */
+ setDirectCdnStreamingVideoConfiguration = () => {
+ const {
+ codecType,
+ width,
+ height,
+ frameRate,
+ bitrate,
+ minBitrate,
+ orientationMode,
+ degradationPreference,
+ mirrorMode,
+ } = this.state;
+ if (orientationMode === OrientationMode.OrientationModeAdaptive) {
+ this.error(
+ 'orientationMode is invalid, should not be OrientationMode.OrientationModeAdaptive'
+ );
+ return;
+ }
+ this.engine?.setDirectCdnStreamingVideoConfiguration({
+ codecType,
+ dimensions: {
+ width: width,
+ height: height,
+ },
+ frameRate,
+ bitrate,
+ minBitrate,
+ orientationMode,
+ degradationPreference,
+ mirrorMode,
+ });
+ };
+
+ /**
+ * Step 3-2: startDirectCdnStreaming
+ */
+ startDirectCdnStreaming = () => {
+ const { url } = this.state;
+ if (!url) {
+ this.error('url is invalid');
+ return;
+ }
+
+ this.engine?.startDirectCdnStreaming(this, url, {
+ publishCameraTrack: true,
+ publishMicrophoneTrack: true,
+ });
+ };
+
+ /**
+ * Step 3-3: stopDirectCdnStreaming
+ */
+ stopDirectCdnStreaming = () => {
+ this.engine?.stopDirectCdnStreaming();
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ const { startDirectCdnStreaming } = this.state;
+ if (startDirectCdnStreaming) {
+ this.stopDirectCdnStreaming();
+ }
+ super.onLeaveChannel(connection, stats);
+ }
+
+ onDirectCdnStreamingStateChanged(
+ state: DirectCdnStreamingState,
+ error: DirectCdnStreamingReason,
+ message: string
+ ) {
+ this.info(
+ 'onDirectCdnStreamingStateChanged',
+ 'state',
+ state,
+ 'error',
+ error,
+ 'message',
+ message
+ );
+ switch (state) {
+ case DirectCdnStreamingState.DirectCdnStreamingStateIdle:
+ break;
+ case DirectCdnStreamingState.DirectCdnStreamingStateRunning:
+ this.setState({ startDirectCdnStreaming: true });
+ break;
+ case DirectCdnStreamingState.DirectCdnStreamingStateStopped:
+ case DirectCdnStreamingState.DirectCdnStreamingStateFailed:
+ this.setState({ startDirectCdnStreaming: false });
+ break;
+ case DirectCdnStreamingState.DirectCdnStreamingStateRecovering:
+ break;
+ }
+ }
+
+ onDirectCdnStreamingStats(stats: DirectCdnStreamingStats) {
+ this.info('onDirectCdnStreamingStats', 'stats', stats);
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ url,
+ codecType,
+ orientationMode,
+ degradationPreference,
+ mirrorMode,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ url: text });
+ }}
+ placeholder={`url`}
+ value={url}
+ />
+ {
+ this.setState({ codecType: value });
+ }}
+ />
+
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ width: text === '' ? this.createState().width : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`width (defaults: ${this.createState().width})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ height: text === '' ? this.createState().height : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`height (defaults: ${this.createState().height})`}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ frameRate: text === '' ? this.createState().frameRate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`frameRate (defaults: ${this.createState().frameRate})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ bitrate: text === '' ? this.createState().bitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`bitrate (defaults: ${this.createState().bitrate})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ minBitrate: text === '' ? this.createState().minBitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`minBitrate (defaults: ${
+ this.createState().minBitrate
+ })`}
+ />
+ {
+ this.setState({ orientationMode: value });
+ }}
+ />
+
+ {
+ this.setState({ degradationPreference: value });
+ }}
+ />
+
+ {
+ this.setState({ mirrorMode: value });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startDirectCdnStreaming } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/Encryption/Encryption.tsx b/examples/expo/app/examples/advanced/Encryption/Encryption.tsx
new file mode 100644
index 000000000..b16bd6ff4
--- /dev/null
+++ b/examples/expo/app/examples/advanced/Encryption/Encryption.tsx
@@ -0,0 +1,210 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ EncryptionErrorType,
+ EncryptionMode,
+ IRtcEngineEventHandler,
+ RtcConnection,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraText,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ encryptionMode: EncryptionMode;
+ encryptionKey: string;
+ encryptionKdfSalt: number[];
+ enableEncryption: boolean;
+}
+
+export default class Encryption
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ encryptionMode: EncryptionMode.Aes128Xts,
+ encryptionKey: '',
+ encryptionKdfSalt: new Array(32).fill(1, 0, 32),
+ enableEncryption: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: enableEncryption
+ */
+ enableEncryption = () => {
+ const { encryptionMode, encryptionKey, encryptionKdfSalt } = this.state;
+ if (!encryptionKey) {
+ this.error('encryptionKey is invalid');
+ return;
+ }
+
+ this.engine?.enableEncryption(true, {
+ encryptionMode,
+ encryptionKey,
+ encryptionKdfSalt,
+ });
+ this.setState({ enableEncryption: true });
+ };
+
+ /**
+ * Step 3-2: disableEncryption
+ */
+ disableEncryption = () => {
+ this.engine?.enableEncryption(false, {});
+ this.setState({ enableEncryption: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onEncryptionError(connection: RtcConnection, errorType: EncryptionErrorType) {
+ this.info(
+ 'onEncryptionError',
+ 'connection',
+ connection,
+ 'errorType',
+ errorType
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { encryptionMode, encryptionKey, encryptionKdfSalt } = this.state;
+ return (
+ <>
+ {
+ this.setState({ encryptionMode: value });
+ }}
+ />
+
+ {
+ this.setState({ encryptionKey: text });
+ }}
+ placeholder={'encryptionKey'}
+ value={encryptionKey}
+ />
+ {
+ this.setState({
+ encryptionKdfSalt: text.split(' ').map((value) => +value),
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={'encryptionKdfSalt (split by blank)'}
+ value={encryptionKdfSalt.join(' ')}
+ />
+ {`encryptionKdfSaltLength: ${encryptionKdfSalt.length}`}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess, enableEncryption } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/Extension/Extension.tsx b/examples/expo/app/examples/advanced/Extension/Extension.tsx
new file mode 100644
index 000000000..81dde39e3
--- /dev/null
+++ b/examples/expo/app/examples/advanced/Extension/Extension.tsx
@@ -0,0 +1,258 @@
+import React, { ReactElement } from 'react';
+import { Platform } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ ExtensionContext,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import { AgoraButton, AgoraTextInput } from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ path: string;
+ provider: string;
+ extension: string;
+ enableExtension: boolean;
+}
+
+export default class Extension
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ path: '',
+ provider: '',
+ extension: '',
+ enableExtension: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2-1: enableExtension
+ */
+ enableExtension = () => {
+ const { path, provider, extension } = this.state;
+ if (!path) {
+ this.error('path is invalid');
+ return;
+ }
+ if (!provider) {
+ this.error('provider is invalid');
+ return;
+ }
+ if (!extension) {
+ this.error('extension is invalid');
+ return;
+ }
+
+ if (Platform.OS === 'android') {
+ this.engine?.loadExtensionProvider(path);
+ }
+
+ let result = this.engine?.enableExtension(provider, extension, true);
+ if (result && result < 0) {
+ this.error(`enableExtension failed: ${result}`);
+ }
+ };
+
+ /**
+ * Step 2-2: disableExtension
+ */
+ disableExtension = () => {
+ const { provider, extension } = this.state;
+ let result = this.engine?.enableExtension(provider, extension, false);
+ if (result && result < 0) {
+ this.error(`enableExtension failed: ${result}`);
+ }
+ };
+
+ /**
+ * Step 3: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onExtensionErrorWithContext(
+ context: ExtensionContext,
+ error: number,
+ msg: string
+ ) {
+ this.error(
+ 'onExtensionErrorWithContext',
+ 'context',
+ context,
+ 'error',
+ error,
+ 'msg',
+ msg
+ );
+ }
+
+ onExtensionEventWithContext(
+ context: ExtensionContext,
+ key: string,
+ value: string
+ ) {
+ this.info(
+ 'onExtensionEventWithContext',
+ 'context',
+ context,
+ 'key',
+ key,
+ 'value',
+ value
+ );
+ }
+
+ onExtensionStartedWithContext(context: ExtensionContext) {
+ this.info('onExtensionStartedWithContext', 'context', context);
+ if (
+ context.providerName === this.state.provider &&
+ context.extensionName === this.state.extension
+ ) {
+ this.setState({ enableExtension: true });
+ }
+ }
+
+ onExtensionStoppedWithContext(context: ExtensionContext) {
+ this.info('onExtensionStoppedWithContext', 'context', context);
+ if (
+ context.providerName === this.state.provider &&
+ context.extensionName === this.state.extension
+ ) {
+ this.setState({ enableExtension: false });
+ }
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { path, provider, extension } = this.state;
+ return (
+ <>
+ {Platform.OS === 'android' ? (
+ {
+ this.setState({
+ path: text,
+ });
+ }}
+ placeholder={'path'}
+ value={path}
+ />
+ ) : undefined}
+ {
+ this.setState({
+ provider: text,
+ });
+ }}
+ placeholder={'provider'}
+ value={provider}
+ />
+ {
+ this.setState({
+ extension: text,
+ });
+ }}
+ placeholder={'extension'}
+ value={extension}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { enableExtension } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/JoinMultipleChannel/JoinMultipleChannel.tsx b/examples/expo/app/examples/advanced/JoinMultipleChannel/JoinMultipleChannel.tsx
new file mode 100644
index 000000000..8797dfcc2
--- /dev/null
+++ b/examples/expo/app/examples/advanced/JoinMultipleChannel/JoinMultipleChannel.tsx
@@ -0,0 +1,498 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ IRtcEngineEx,
+ RemoteVideoState,
+ RemoteVideoStateReason,
+ RtcConnection,
+ RtcStats,
+ UserOfflineReasonType,
+ VideoCanvas,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraCard,
+ AgoraList,
+ AgoraStyle,
+ AgoraTextInput,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ channelId2: string;
+ token2: string;
+ uid2: number;
+ joinChannelSuccess2: boolean;
+ remoteUsers2: number[];
+}
+
+export default class JoinMultipleChannel
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ // @ts-ignore
+ protected engine?: IRtcEngineEx;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ channelId2: '',
+ token2: '',
+ uid2: 0,
+ joinChannelSuccess2: false,
+ remoteUsers2: [],
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine() as IRtcEngineEx;
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Need to startPreview before joinChannelEx
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2-1: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid <= 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannelEx(
+ token,
+ {
+ channelId,
+ localUid: uid,
+ },
+ {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ }
+ );
+ }
+
+ /**
+ * Step 2-2: joinChannel2
+ */
+ protected joinChannel2() {
+ const { channelId2, token2, uid2 } = this.state;
+ if (!channelId2) {
+ this.error('channelId2 is invalid');
+ return;
+ }
+ if (uid2 <= 0) {
+ this.error('uid2 is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannelEx(
+ token2,
+ {
+ channelId: channelId2,
+ localUid: uid2,
+ },
+ {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ }
+ );
+ }
+
+ /**
+ * Step 3-1: publishStreamToChannel
+ */
+ publishStreamToChannel = () => {
+ const { channelId, channelId2, uid, uid2 } = this.state;
+ this.engine?.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: false, publishCameraTrack: false },
+ {
+ channelId: channelId2,
+ localUid: uid2,
+ }
+ );
+ this.engine?.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: true, publishCameraTrack: true },
+ {
+ channelId,
+ localUid: uid,
+ }
+ );
+ };
+
+ /**
+ * Step 3-2: publishStreamToChannel2
+ */
+ publishStreamToChannel2 = () => {
+ const { channelId, channelId2, uid, uid2 } = this.state;
+ this.engine?.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: false, publishCameraTrack: false },
+ {
+ channelId,
+ localUid: uid,
+ }
+ );
+ this.engine?.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: true, publishCameraTrack: true },
+ {
+ channelId: channelId2,
+ localUid: uid2,
+ }
+ );
+ };
+
+ /**
+ * Step 4-1: leaveChannel
+ */
+ protected leaveChannel() {
+ const { channelId, uid } = this.state;
+ this.engine?.leaveChannelEx({
+ channelId,
+ localUid: uid,
+ });
+ }
+
+ /**
+ * Step 4-2: leaveChannel2
+ */
+ protected leaveChannel2() {
+ const { channelId2, uid2 } = this.state;
+ this.engine?.leaveChannelEx({
+ channelId: channelId2,
+ localUid: uid2,
+ });
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ this.info(
+ 'onJoinChannelSuccess',
+ 'connection',
+ connection,
+ 'elapsed',
+ elapsed
+ );
+ const { channelId, channelId2, uid, uid2 } = this.state;
+ if (connection.channelId === channelId && connection.localUid === uid) {
+ this.setState({
+ joinChannelSuccess: true,
+ });
+ } else if (
+ connection.channelId === channelId2 &&
+ connection.localUid === uid2
+ ) {
+ this.setState({
+ joinChannelSuccess2: true,
+ });
+ }
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ this.info('onLeaveChannel', 'connection', connection, 'stats', stats);
+ const { channelId, channelId2, uid, uid2 } = this.state;
+ if (connection.channelId === channelId && connection.localUid === uid) {
+ this.setState({
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ });
+ } else if (
+ connection.channelId === channelId2 &&
+ connection.localUid === uid2
+ ) {
+ this.setState({
+ joinChannelSuccess2: false,
+ remoteUsers2: [],
+ });
+ }
+ // Keep preview after leave channel
+ this.engine?.startPreview();
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ this.info(
+ 'onUserJoined',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'elapsed',
+ elapsed
+ );
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ this.info(
+ 'onUserOffline',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'reason',
+ reason
+ );
+ }
+
+ onRemoteVideoStateChanged(
+ connection: RtcConnection,
+ remoteUid: number,
+ state: RemoteVideoState,
+ reason: RemoteVideoStateReason,
+ elapsed: number
+ ) {
+ this.info(
+ 'onRemoteVideoStateChanged',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'state',
+ state,
+ 'reason',
+ reason,
+ 'elapsed',
+ elapsed
+ );
+ const { channelId, channelId2, uid, uid2 } = this.state;
+ if (state === RemoteVideoState.RemoteVideoStateStarting) {
+ if (connection.channelId === channelId && connection.localUid === uid) {
+ this.setState((preState) => {
+ return { remoteUsers: [...preState.remoteUsers, remoteUid] };
+ });
+ } else if (
+ connection.channelId === channelId2 &&
+ connection.localUid === uid2
+ ) {
+ this.setState((preState) => {
+ return { remoteUsers2: [...preState.remoteUsers2, remoteUid] };
+ });
+ }
+ } else if (state === RemoteVideoState.RemoteVideoStateStopped) {
+ if (connection.channelId === channelId && connection.localUid === uid) {
+ this.setState((preState) => {
+ return {
+ remoteUsers: preState.remoteUsers.filter(
+ (value) => value !== remoteUid
+ ),
+ };
+ });
+ } else if (
+ connection.channelId === channelId2 &&
+ connection.localUid === uid2
+ ) {
+ this.setState((preState) => {
+ return {
+ remoteUsers2: preState.remoteUsers2.filter(
+ (value) => value !== remoteUid
+ ),
+ };
+ });
+ }
+ }
+ }
+
+ protected renderChannel(): ReactElement | undefined {
+ const {
+ channelId,
+ channelId2,
+ uid,
+ uid2,
+ joinChannelSuccess,
+ joinChannelSuccess2,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ channelId: text });
+ }}
+ placeholder={`channelId`}
+ value={channelId}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ uid: text === '' ? this.createState().uid : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`uid (must > 0)`}
+ value={uid > 0 ? uid.toString() : ''}
+ />
+ {
+ joinChannelSuccess ? this.leaveChannel() : this.joinChannel();
+ }}
+ />
+ {
+ this.setState({ channelId2: text });
+ }}
+ placeholder={`channelId2`}
+ value={channelId2}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ uid2: text === '' ? this.createState().uid2 : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`uid2 (must > 0)`}
+ value={uid2 > 0 ? uid2.toString() : ''}
+ />
+ {
+ joinChannelSuccess2 ? this.leaveChannel2() : this.joinChannel2();
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const {
+ startPreview,
+ channelId,
+ channelId2,
+ uid,
+ uid2,
+ joinChannelSuccess,
+ joinChannelSuccess2,
+ remoteUsers,
+ remoteUsers2,
+ } = this.state;
+ return (
+ <>
+ {startPreview || joinChannelSuccess || joinChannelSuccess2 ? (
+ {
+ return this.renderVideo(
+ { uid: item },
+ remoteUsers2.indexOf(item) === -1 ? channelId : channelId2,
+ remoteUsers2.indexOf(item) === -1 ? uid : uid2
+ )!;
+ }}
+ />
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderVideo(
+ user: VideoCanvas,
+ channelId?: string,
+ localUid?: number
+ ): ReactElement | undefined {
+ return (
+
+
+
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess, joinChannelSuccess2 } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/LocalSpatialAudioEngine/LocalSpatialAudioEngine.tsx b/examples/expo/app/examples/advanced/LocalSpatialAudioEngine/LocalSpatialAudioEngine.tsx
new file mode 100644
index 000000000..11668086a
--- /dev/null
+++ b/examples/expo/app/examples/advanced/LocalSpatialAudioEngine/LocalSpatialAudioEngine.tsx
@@ -0,0 +1,313 @@
+import React, { ReactElement } from 'react';
+import {
+ AudioScenarioType,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraTextInput,
+ AgoraView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { arrayToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ range: number;
+ targetUid: number;
+ position: number[];
+ axisForward: number[];
+ axisRight: number[];
+ axisUp: number[];
+}
+
+export default class LocalSpatialAudioEngine
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ range: 50,
+ targetUid: 0,
+ position: [0, 0, 0],
+ axisForward: [1, 0, 0],
+ axisRight: [0, 1, 0],
+ axisUp: [0, 0, 1],
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ // ⚠️ Must use AudioScenarioGameStreaming on this case
+ audioScenario: AudioScenarioType.AudioScenarioGameStreaming,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+
+ this.engine.setParameters(
+ JSON.stringify({ 'rtc.audio.force_bluetooth_a2dp': true })
+ );
+
+ this.initializeLocalSpatialAudioEngine();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ // ⚠️ Must set autoSubscribeAudio to false
+ autoSubscribeAudio: false,
+ });
+ }
+
+ /**
+ * Step 3-1: initializeLocalSpatialAudioEngine
+ */
+ initializeLocalSpatialAudioEngine = () => {
+ this.engine?.getLocalSpatialAudioEngine().initialize();
+ };
+
+ /**
+ * Step 3-2: setAudioRecvRange
+ */
+ setAudioRecvRange = () => {
+ const { range } = this.state;
+ this.engine?.getLocalSpatialAudioEngine().setAudioRecvRange(range);
+ };
+
+ /**
+ * Step 3-3: setAudioRecvRange
+ */
+ updateSelfPosition = () => {
+ const { position, axisForward, axisRight, axisUp } = this.state;
+ this.engine
+ ?.getLocalSpatialAudioEngine()
+ .updateSelfPosition(position, axisForward, axisRight, axisUp);
+ };
+
+ /**
+ * Step 3-4: updateRemotePosition
+ */
+ updateRemotePosition = () => {
+ const { targetUid, position, axisForward } = this.state;
+ this.engine?.getLocalSpatialAudioEngine().updateRemotePosition(targetUid, {
+ position,
+ forward: axisForward,
+ });
+ };
+
+ /**
+ * Step 3-5: releaseLocalSpatialAudioEngine
+ */
+ releaseLocalSpatialAudioEngine = () => {
+ this.engine?.getLocalSpatialAudioEngine().release();
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.releaseLocalSpatialAudioEngine();
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ joinChannelSuccess,
+ remoteUsers,
+ targetUid,
+ position,
+ axisForward,
+ axisRight,
+ axisUp,
+ } = this.state;
+ return (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ range: text === '' ? this.createState().range : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`range (defaults: ${this.createState().range})`}
+ />
+
+
+ {
+ this.setState({ targetUid: value });
+ }}
+ />
+
+
+ {position.map((value, index) => (
+ {
+ if (isNaN(+text)) return;
+ this.setState((preState) => {
+ preState.position[index] = +text;
+ return { position: preState.position };
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`position (defaults: ${
+ this.createState().position[index]
+ })`}
+ />
+ ))}
+
+
+
+ {axisForward.map((value, index) => (
+ {
+ if (isNaN(+text)) return;
+ this.setState((preState) => {
+ preState.axisForward[index] = +text;
+ return { axisForward: preState.axisForward };
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`axisForward (defaults: ${
+ this.createState().axisForward[index]
+ })`}
+ />
+ ))}
+
+
+
+ {axisRight.map((value, index) => (
+ {
+ if (isNaN(+text)) return;
+ this.setState((preState) => {
+ preState.axisRight[index] = +text;
+ return { axisRight: preState.axisRight };
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`axisRight (defaults: ${
+ this.createState().axisRight[index]
+ })`}
+ />
+ ))}
+
+
+
+ {axisUp.map((value, index) => (
+ {
+ if (isNaN(+text)) return;
+ this.setState((preState) => {
+ preState.axisUp[index] = +text;
+ return { axisUp: preState.axisUp };
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`axisUp (defaults: ${
+ this.createState().axisUp[index]
+ })`}
+ />
+ ))}
+
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess, targetUid } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/LocalVideoTranscoder/LocalVideoTranscoder.tsx b/examples/expo/app/examples/advanced/LocalVideoTranscoder/LocalVideoTranscoder.tsx
new file mode 100644
index 000000000..e764c6dc0
--- /dev/null
+++ b/examples/expo/app/examples/advanced/LocalVideoTranscoder/LocalVideoTranscoder.tsx
@@ -0,0 +1,430 @@
+import React, { ReactElement } from 'react';
+import { Platform } from 'react-native';
+import createAgoraRtcEngine, {
+ ChannelProfileType,
+ ClientRoleType,
+ IMediaPlayer,
+ IMediaPlayerSourceObserver,
+ IRtcEngineEventHandler,
+ LocalTranscoderConfiguration,
+ MediaPlayerReason,
+ MediaPlayerState,
+ RenderModeType,
+ RtcConnection,
+ RtcStats,
+ TranscodingVideoStream,
+ VideoMirrorModeType,
+ VideoSourceType,
+ showRPSystemBroadcastPickerView,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraStyle,
+ AgoraTextInput,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { getAbsolutePath, getResourcePath } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ startScreenCapture: boolean;
+ url: string;
+ open: boolean;
+ imageUrl: string;
+ startLocalVideoTranscoder: boolean;
+ VideoInputStreams: TranscodingVideoStream[];
+}
+
+export default class LocalVideoTranscoder
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, IMediaPlayerSourceObserver
+{
+ protected player?: IMediaPlayer;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ startScreenCapture: false,
+ url: 'https://agora-adc-artifacts.oss-cn-beijing.aliyuncs.com/video/meta_live_mpk.mov',
+ open: false,
+ imageUrl: getResourcePath('agora-logo.png'),
+ startLocalVideoTranscoder: false,
+ VideoInputStreams: [],
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.registerEventHandler(this);
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Start preview before joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ publishTranscodedVideoTrack: true,
+ });
+ }
+
+ /**
+ * Step 3-1 (Optional): startScreenCapture
+ */
+ startScreenCapture = async () => {
+ this.engine?.startScreenCapture({
+ videoParams: {
+ dimensions: { width: 1920, height: 1080 },
+ bitrate: 1000,
+ frameRate: 15,
+ },
+ });
+ this.engine?.startPreview(VideoSourceType.VideoSourceScreen);
+ if (Platform.OS === 'ios') {
+ // Show the picker view for screen share, ⚠️ only support for iOS 12+
+ await showRPSystemBroadcastPickerView(true);
+ }
+ this.setState({ startScreenCapture: true });
+ };
+
+ /**
+ * Step 3-4 (Optional): stopScreenCapture
+ */
+ stopScreenCapture = () => {
+ this.engine?.stopScreenCapture();
+ this.setState({ startScreenCapture: false });
+ };
+
+ /**
+ * Step 3-3 (Optional): createMediaPlayer
+ */
+ createMediaPlayer = () => {
+ const { url } = this.state;
+
+ if (!url) {
+ this.error('url is invalid');
+ }
+
+ this.player = this.engine?.createMediaPlayer();
+ this.player?.registerPlayerSourceObserver(this);
+ this.player?.open(url, 0);
+ };
+
+ /**
+ * Step 3-4 (Optional): destroyMediaPlayer
+ */
+ destroyMediaPlayer = () => {
+ if (!this.player) {
+ return;
+ }
+
+ this.engine?.destroyMediaPlayer(this.player);
+ this.setState({ open: false });
+ };
+
+ /**
+ * Step 3-5: startLocalVideoTranscoder
+ */
+ startLocalVideoTranscoder = async () => {
+ const config = await this._generateLocalTranscoderConfiguration();
+
+ this.engine?.startLocalVideoTranscoder(config);
+ this.engine?.startPreview(VideoSourceType.VideoSourceTranscoded);
+ this.setState({ startLocalVideoTranscoder: true });
+ };
+
+ /**
+ * Step 3-6 (Optional): updateLocalTranscoderConfiguration
+ */
+ updateLocalTranscoderConfiguration = async () => {
+ this.engine?.updateLocalTranscoderConfiguration(
+ await this._generateLocalTranscoderConfiguration()
+ );
+ };
+
+ /**
+ * Step 3-7: stopLocalVideoTranscoder
+ */
+ stopLocalVideoTranscoder = () => {
+ this.engine?.stopLocalVideoTranscoder();
+ this.setState({ startLocalVideoTranscoder: false });
+ };
+
+ _generateLocalTranscoderConfiguration =
+ async (): Promise => {
+ const { startScreenCapture, open, imageUrl } = this.state;
+ const max_width = 1080,
+ max_height = 720,
+ width = 300,
+ height = 300;
+
+ const streams: TranscodingVideoStream[] = [];
+ streams.push({
+ sourceType: VideoSourceType.VideoSourceCamera,
+ });
+
+ if (startScreenCapture) {
+ streams.push({
+ sourceType: VideoSourceType.VideoSourceScreenPrimary,
+ });
+ }
+
+ if (open) {
+ streams.push({
+ sourceType: VideoSourceType.VideoSourceMediaPlayer,
+ mediaPlayerId: this.player?.getMediaPlayerId(),
+ });
+ }
+ let imageAbsoluteUrl = await getAbsolutePath(imageUrl);
+ if (imageAbsoluteUrl) {
+ const getImageType = (url: string): VideoSourceType | undefined => {
+ if (url.endsWith('.png')) {
+ return VideoSourceType.VideoSourceRtcImagePng;
+ } else if (url.endsWith('.jepg') || url.endsWith('.jpg')) {
+ return VideoSourceType.VideoSourceRtcImageJpeg;
+ } else if (url.endsWith('.gif')) {
+ return VideoSourceType.VideoSourceRtcImageGif;
+ }
+ return undefined;
+ };
+ streams.push({
+ sourceType: getImageType(imageAbsoluteUrl),
+ imageUrl: imageAbsoluteUrl,
+ });
+ }
+
+ streams.map((value, index) => {
+ const maxNumPerRow = Math.floor(max_width / width);
+ const numOfRow = Math.floor(index / maxNumPerRow);
+ const numOfColumn = Math.floor(index % maxNumPerRow);
+ value.x = numOfColumn * width;
+ value.y = numOfRow * height;
+ value.width = width;
+ value.height = height;
+ value.zOrder = 1;
+ value.alpha = 1;
+ value.mirror = false;
+ });
+
+ return {
+ streamCount: streams.length,
+ videoInputStreams: streams,
+ videoOutputConfiguration: {
+ dimensions: { width: max_width, height: max_height },
+ },
+ };
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.destroyMediaPlayer();
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.release();
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ this.info('onLeaveChannel', 'connection', connection, 'stats', stats);
+ const state = this.createState();
+ this.setState(state);
+ }
+
+ onPlayerSourceStateChanged(state: MediaPlayerState, ec: MediaPlayerReason) {
+ this.info('onPlayerSourceStateChanged', 'state', state, 'ec', ec);
+ switch (state) {
+ case MediaPlayerState.PlayerStateIdle:
+ break;
+ case MediaPlayerState.PlayerStateOpening:
+ break;
+ case MediaPlayerState.PlayerStateOpenCompleted:
+ this.setState({ open: true });
+ // Auto play on this case
+ this.player?.play();
+ break;
+ case MediaPlayerState.PlayerStatePlaying:
+ break;
+ case MediaPlayerState.PlayerStatePaused:
+ break;
+ case MediaPlayerState.PlayerStatePlaybackCompleted:
+ break;
+ case MediaPlayerState.PlayerStatePlaybackAllLoopsCompleted:
+ break;
+ case MediaPlayerState.PlayerStateStopped:
+ break;
+ case MediaPlayerState.PlayerStatePausingInternal:
+ break;
+ case MediaPlayerState.PlayerStateStoppingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSeekingInternal:
+ break;
+ case MediaPlayerState.PlayerStateGettingInternal:
+ break;
+ case MediaPlayerState.PlayerStateNoneInternal:
+ break;
+ case MediaPlayerState.PlayerStateDoNothingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSetTrackInternal:
+ break;
+ case MediaPlayerState.PlayerStateFailed:
+ break;
+ }
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess, startLocalVideoTranscoder } =
+ this.state;
+ return (
+ <>
+ {startLocalVideoTranscoder
+ ? this.renderUser({
+ renderMode: RenderModeType.RenderModeFit,
+ uid: 0,
+ sourceType: VideoSourceType.VideoSourceTranscoded,
+ mirrorMode: VideoMirrorModeType.VideoMirrorModeDisabled,
+ })
+ : undefined}
+ {startPreview || joinChannelSuccess
+ ? this.renderUser({
+ uid: 0,
+ sourceType: VideoSourceType.VideoSourceCamera,
+ })
+ : undefined}
+ >
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { startScreenCapture, url, open, imageUrl } = this.state;
+ return (
+ <>
+
+
+ {
+ this.setState({ url: text });
+ }}
+ placeholder={'url'}
+ value={url}
+ />
+ {open ? (
+
+ ) : undefined}
+
+
+ {
+ this.setState({ imageUrl: text });
+ }}
+ placeholder={'imageUrl'}
+ value={imageUrl}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startLocalVideoTranscoder } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/MediaPlayer/MediaPlayer.tsx b/examples/expo/app/examples/advanced/MediaPlayer/MediaPlayer.tsx
new file mode 100644
index 000000000..9a6517250
--- /dev/null
+++ b/examples/expo/app/examples/advanced/MediaPlayer/MediaPlayer.tsx
@@ -0,0 +1,391 @@
+import React, { ReactElement } from 'react';
+import {
+ IMediaPlayer,
+ IMediaPlayerSourceObserver,
+ IRtcEngineEventHandler,
+ MediaPlayerEvent,
+ MediaPlayerReason,
+ MediaPlayerState,
+ VideoSourceType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraSlider,
+ AgoraTextInput,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+
+interface State extends BaseComponentState {
+ url: string;
+ open: boolean;
+ play: boolean;
+ pause: boolean;
+ position: number;
+ duration: number;
+ mute: boolean;
+ playoutVolume: number;
+ loopCount: number;
+}
+
+export default class MediaPlayer
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, IMediaPlayerSourceObserver
+{
+ protected player?: IMediaPlayer;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ url: 'https://agora-adc-artifacts.oss-cn-beijing.aliyuncs.com/video/meta_live_mpk.mov',
+ open: false,
+ play: false,
+ pause: false,
+ position: 0,
+ duration: 0,
+ mute: false,
+ playoutVolume: 100,
+ loopCount: 1,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ });
+ this.engine.registerEventHandler(this);
+
+ this.createMediaPlayer();
+ }
+
+ /**
+ * Step 2: createMediaPlayer
+ */
+ createMediaPlayer = () => {
+ this.player = this.engine?.createMediaPlayer();
+ this.player?.registerPlayerSourceObserver(this);
+ };
+
+ /**
+ * Step 3-1: open
+ */
+ open = () => {
+ const { url } = this.state;
+ if (!url) {
+ this.error('url is invalid');
+ }
+
+ this.player?.open(url, 0);
+ };
+
+ /**
+ * Step 3-2: play
+ */
+ play = () => {
+ const { position, duration } = this.state;
+ if (position === duration && duration !== 0) {
+ this.player?.seek(0);
+ } else {
+ this.player?.play();
+ }
+ };
+
+ /**
+ * Step 3-3 (Optional): seek
+ */
+ seek = (position: number) => {
+ const { duration } = this.state;
+
+ if (duration <= 0) {
+ this.error(`duration is invalid`);
+ return;
+ }
+
+ if (position < 0 || position > duration) {
+ this.error(`percent is invalid`);
+ return;
+ }
+
+ this.player?.seek(position);
+ };
+
+ /**
+ * Step 3-4 (Optional): pause
+ */
+ pause = () => {
+ this.player?.pause();
+ };
+
+ /**
+ * Step 3-5 (Optional): resume
+ */
+ resume = () => {
+ this.player?.resume();
+ };
+
+ /**
+ * Step 3-6 (Optional): mute
+ */
+ mute = () => {
+ this.player?.mute(true);
+ this.setState({ mute: true });
+ };
+
+ /**
+ * Step 3-7 (Optional): unmute
+ */
+ unmute = () => {
+ this.player?.mute(false);
+ this.setState({ mute: false });
+ };
+
+ /**
+ * Step 3-8 (Optional): adjustPlayoutVolume
+ */
+ adjustPlayoutVolume = () => {
+ const { playoutVolume } = this.state;
+ this.player?.adjustPlayoutVolume(playoutVolume);
+ };
+
+ /**
+ * Step 3-9 (Optional): setLoopCount
+ */
+ setLoopCount = () => {
+ const { loopCount } = this.state;
+ this.player?.setLoopCount(loopCount);
+ };
+
+ /**
+ * Step 3-10 (Optional): getStreamInfo
+ */
+ getStreamInfo = () => {
+ const streamCount = this.player?.getStreamCount();
+ if (streamCount === undefined || streamCount <= 0) {
+ this.error(`streamCount is invalid`);
+ }
+
+ const streamInfo = this.player?.getStreamInfo(0);
+ if (streamInfo) {
+ this.debug('getStreamInfo', 'streamInfo', streamInfo);
+ } else {
+ this.error('getStreamInfo');
+ }
+ };
+
+ /**
+ * Step 3-11: stop
+ */
+ stop = () => {
+ this.player?.stop();
+ };
+
+ /**
+ * Step 4: destroyMediaPlayer
+ */
+ protected destroyMediaPlayer() {
+ if (!this.player) return;
+ this.engine?.destroyMediaPlayer(this.player);
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.destroyMediaPlayer();
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onPlayerSourceStateChanged(state: MediaPlayerState, ec: MediaPlayerReason) {
+ this.info('onPlayerSourceStateChanged', 'state', state, 'ec', ec);
+ switch (state) {
+ case MediaPlayerState.PlayerStateIdle:
+ break;
+ case MediaPlayerState.PlayerStateOpening:
+ break;
+ case MediaPlayerState.PlayerStateOpenCompleted: {
+ const duration = this.player?.getDuration()!;
+ this.setState({
+ open: true,
+ duration: duration < 0 ? 0 : duration,
+ });
+ break;
+ }
+ case MediaPlayerState.PlayerStatePlaying:
+ this.setState({ play: true, pause: false });
+ break;
+ case MediaPlayerState.PlayerStatePaused:
+ this.setState({ pause: true });
+ break;
+ case MediaPlayerState.PlayerStatePlaybackCompleted:
+ case MediaPlayerState.PlayerStatePlaybackAllLoopsCompleted:
+ this.setState({ play: false });
+ break;
+ case MediaPlayerState.PlayerStateStopped:
+ this.setState({ open: false, play: false, pause: false, mute: false });
+ break;
+ case MediaPlayerState.PlayerStatePausingInternal:
+ break;
+ case MediaPlayerState.PlayerStateStoppingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSeekingInternal:
+ break;
+ case MediaPlayerState.PlayerStateGettingInternal:
+ break;
+ case MediaPlayerState.PlayerStateNoneInternal:
+ break;
+ case MediaPlayerState.PlayerStateDoNothingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSetTrackInternal:
+ break;
+ case MediaPlayerState.PlayerStateFailed:
+ break;
+ }
+ }
+
+ onPositionChanged(position: number) {
+ this.info('onPositionChanged', 'position', position);
+ this.setState({ position: position });
+ }
+
+ onPlayerEvent(
+ eventCode: MediaPlayerEvent,
+ elapsedTime: number,
+ message: string
+ ) {
+ this.info(
+ 'onPlayerEvent',
+ 'eventCode',
+ eventCode,
+ 'elapsedTime',
+ elapsedTime,
+ 'message',
+ message
+ );
+ }
+
+ protected renderChannel(): ReactElement | undefined {
+ return undefined;
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { url, open, position, duration, playoutVolume } = this.state;
+ return (
+ <>
+ {
+ this.setState({ url: text });
+ }}
+ placeholder={'url'}
+ value={url}
+ />
+ {
+ this.seek(value);
+ }}
+ />
+
+ {
+ this.setState({ playoutVolume: value });
+ }}
+ />
+
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ loopCount: text === '' ? this.createState().loopCount : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`loopCount (defaults: ${this.createState().loopCount})`}
+ />
+
+ >
+ );
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const { open } = this.state;
+ return (
+ <>
+ {open ? (
+
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { open, play, pause, mute } = this.state;
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/MediaRecorder/MediaRecorder.tsx b/examples/expo/app/examples/advanced/MediaRecorder/MediaRecorder.tsx
new file mode 100644
index 000000000..7e87b1230
--- /dev/null
+++ b/examples/expo/app/examples/advanced/MediaRecorder/MediaRecorder.tsx
@@ -0,0 +1,312 @@
+import React, { ReactElement } from 'react';
+import { Platform } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IMediaRecorder,
+ IMediaRecorderObserver,
+ IRtcEngineEventHandler,
+ MediaRecorderContainerFormat,
+ MediaRecorderStreamType,
+ RecorderInfo,
+ RecorderReasonCode,
+ RecorderState,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+import RNFS from 'react-native-fs';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ storagePath: string;
+ containerFormat: MediaRecorderContainerFormat;
+ streamType: MediaRecorderStreamType;
+ maxDurationMs: number;
+ recorderInfoUpdateInterval: number;
+ startRecoding: boolean;
+}
+
+export default class MediaRecorder
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, IMediaRecorderObserver
+{
+ protected recorder?: IMediaRecorder;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ storagePath: `${
+ Platform.OS === 'android'
+ ? RNFS.ExternalCachesDirectoryPath
+ : RNFS.DocumentDirectoryPath
+ }`,
+ containerFormat: MediaRecorderContainerFormat.FormatMp4,
+ streamType: MediaRecorderStreamType.StreamTypeBoth,
+ maxDurationMs: 120000,
+ recorderInfoUpdateInterval: 1000,
+ startRecoding: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Start preview before joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+
+ this.createMediaRecorder();
+ }
+
+ /**
+ * Step 3-1: createMediaRecorder
+ */
+ createMediaRecorder = () => {
+ const { channelId, uid } = this.state;
+ this.recorder = this.engine?.createMediaRecorder({
+ channelId,
+ uid,
+ });
+ this.recorder?.setMediaRecorderObserver(this);
+ };
+
+ /**
+ * Step 3-2: startRecording
+ */
+ startRecording = () => {
+ const {
+ uid,
+ storagePath,
+ containerFormat,
+ streamType,
+ maxDurationMs,
+ recorderInfoUpdateInterval,
+ } = this.state;
+ this.recorder?.startRecording({
+ storagePath: `${storagePath}/${uid}.mp4`,
+ containerFormat,
+ streamType,
+ maxDurationMs,
+ recorderInfoUpdateInterval,
+ });
+ };
+
+ /**
+ * Step 3-3: stopRecording
+ */
+ stopRecording = () => {
+ this.recorder?.stopRecording();
+ };
+
+ /**
+ * Step 4: destroyMediaRecorder
+ */
+ protected destroyMediaRecorder() {
+ if (!this.recorder) return;
+ this.engine?.destroyMediaRecorder(this.recorder);
+ }
+
+ /**
+ * Step 5: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 6: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.destroyMediaRecorder();
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onRecorderInfoUpdated(channelId: string, uid: number, info: RecorderInfo) {
+ this.info(
+ 'onRecorderInfoUpdated',
+ 'channelId',
+ channelId,
+ 'uid',
+ uid,
+ 'info',
+ info
+ );
+ }
+
+ onRecorderStateChanged(
+ channelId: string,
+ uid: number,
+ state: RecorderState,
+ error: RecorderReasonCode
+ ) {
+ this.info(
+ 'onRecorderStateChanged',
+ 'channelId',
+ channelId,
+ 'uid',
+ uid,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ switch (state) {
+ case RecorderState.RecorderStateStart:
+ this.setState({ startRecoding: true });
+ break;
+ case RecorderState.RecorderStateError:
+ case RecorderState.RecorderStateStop:
+ // ⚠️ You should call stopRecording if received the event with state Error or Stop,
+ // otherwise you can't call startRecording again
+ this.stopRecording();
+ this.setState({ startRecoding: false });
+ break;
+ }
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ storagePath,
+ containerFormat,
+ streamType,
+ recorderInfoUpdateInterval,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ storagePath: text });
+ }}
+ placeholder={'storagePath'}
+ value={storagePath}
+ />
+ {
+ this.setState({ containerFormat: value });
+ }}
+ />
+
+ {
+ this.setState({ streamType: value });
+ }}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ maxDurationMs:
+ text === '' ? this.createState().maxDurationMs : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`maxDurationMs (defaults: ${
+ this.createState().maxDurationMs
+ })`}
+ />
+ {
+ this.setState({ recorderInfoUpdateInterval: value });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess, startRecoding } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/MusicContentCenter/MusicContentCenter.tsx b/examples/expo/app/examples/advanced/MusicContentCenter/MusicContentCenter.tsx
new file mode 100644
index 000000000..e8cc57053
--- /dev/null
+++ b/examples/expo/app/examples/advanced/MusicContentCenter/MusicContentCenter.tsx
@@ -0,0 +1,540 @@
+import React, { ReactElement } from 'react';
+import {
+ IMediaPlayerSourceObserver,
+ IMusicContentCenter,
+ IMusicContentCenterEventHandler,
+ IMusicPlayer,
+ MediaPlayerEvent,
+ MediaPlayerReason,
+ MediaPlayerState,
+ Music,
+ MusicChartInfo,
+ MusicCollection,
+ MusicContentCenterStateReason,
+ PreloadState,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDropdown,
+ AgoraImage,
+ AgoraSlider,
+ AgoraStyle,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+
+interface State extends BaseComponentState {
+ rtmAppId: string;
+ rtmToken: string; // generate for test https://webdemo.agora.io/token-builder/
+ mccUid: number;
+ musicChartInfos: MusicChartInfo[];
+ musicChartId: number;
+ page: number;
+ pageSize: number;
+ musicCollection?: MusicCollection;
+ musics: Music[];
+ songCode: number;
+ preload: boolean;
+ open: boolean;
+ play: boolean;
+ pause: boolean;
+ position: number;
+ duration: number;
+}
+
+export default class MusicContentCenter
+ extends BaseComponent<{}, State>
+ implements IMusicContentCenterEventHandler, IMediaPlayerSourceObserver
+{
+ protected musicContentCenter?: IMusicContentCenter;
+ protected player?: IMusicPlayer;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ rtmAppId: '',
+ rtmToken: '',
+ mccUid: 0,
+ musicChartInfos: [],
+ musicChartId: -1,
+ page: 0,
+ pageSize: 20,
+ musicCollection: undefined,
+ musics: [],
+ songCode: -1,
+ preload: false,
+ open: false,
+ play: false,
+ pause: false,
+ position: 0,
+ duration: 0,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ });
+ }
+
+ /**
+ * Step 2: initMusicContentCenter
+ */
+ initMusicContentCenter = () => {
+ const { rtmAppId, rtmToken, mccUid } = this.state;
+ if (!rtmAppId) {
+ this.error(`appId is invalid`);
+ }
+ if (!rtmToken) {
+ this.error(`rtmToken is invalid`);
+ }
+ if (!mccUid) {
+ this.error(`mccUid is invalid`);
+ }
+
+ this.musicContentCenter = this.engine?.getMusicContentCenter();
+ this.musicContentCenter?.registerEventHandler(this);
+
+ this.musicContentCenter?.initialize({
+ appId: rtmAppId,
+ token: rtmToken,
+ mccUid,
+ });
+
+ this.getMusicCharts();
+ };
+
+ /**
+ * Step 3: getMusicCharts
+ */
+ getMusicCharts = () => {
+ this.musicContentCenter?.getMusicCharts();
+ };
+
+ /**
+ * Step 4: getMusicCollectionByMusicChartId
+ */
+ getMusicCollectionByMusicChartId = () => {
+ const { musicChartId, page, pageSize } = this.state;
+ if (musicChartId < 0) {
+ this.error(`musicChartId is invalid`);
+ }
+
+ this.musicContentCenter?.getMusicCollectionByMusicChartId(
+ musicChartId,
+ page,
+ pageSize
+ );
+ };
+
+ /**
+ * Step 5: preload
+ */
+ preload = () => {
+ const { songCode } = this.state;
+ if (!songCode) {
+ this.error(`songCode is invalid`);
+ }
+
+ this.musicContentCenter?.preload(songCode);
+ };
+
+ /**
+ * Step 6: createMusicPlayer
+ */
+ createMusicPlayer = () => {
+ if (this.player) return;
+ this.player = this.musicContentCenter?.createMusicPlayer();
+ this.player?.registerPlayerSourceObserver(this);
+ };
+
+ /**
+ * Step 7-1: openWithSongCode
+ */
+ openWithSongCode = () => {
+ const { songCode } = this.state;
+ if (!songCode) {
+ this.error('songCode is invalid');
+ }
+
+ this.createMusicPlayer();
+ this.player?.openWithSongCode(songCode, 0);
+ };
+
+ /**
+ * Step 7-2: play
+ */
+ play = () => {
+ const { position, duration } = this.state;
+ if (position === duration) {
+ this.player?.seek(0);
+ } else {
+ this.player?.play();
+ }
+ };
+
+ /**
+ * Step 7-3 (Optional): seek
+ */
+ seek = (position: number) => {
+ const { duration } = this.state;
+
+ if (duration <= 0) {
+ this.error(`duration is invalid`);
+ return;
+ }
+
+ if (position < 0 || position > duration) {
+ this.error(`percent is invalid`);
+ return;
+ }
+
+ this.player?.seek(position);
+ };
+
+ /**
+ * Step 7-4 (Optional): pause
+ */
+ pause = () => {
+ this.player?.pause();
+ };
+
+ /**
+ * Step 7-5 (Optional): resume
+ */
+ resume = () => {
+ this.player?.resume();
+ };
+
+ /**
+ * Step 7-6: stop
+ */
+ stop = () => {
+ this.player?.stop();
+ };
+
+ /**
+ * Step 8: destroyMediaPlayer
+ */
+ protected destroyMediaPlayer() {
+ if (!this.player) return;
+ this.engine?.destroyMediaPlayer(this.player);
+ }
+
+ /**
+ * Step 9: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.destroyMediaPlayer();
+ this.musicContentCenter?.release();
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onMusicChartsResult(
+ requestId: string,
+ result: MusicChartInfo[],
+ errorCode: MusicContentCenterStateReason
+ ) {
+ this.info('onMusicChartsResult', requestId, result, errorCode);
+ this.setState({ musicChartInfos: result });
+ }
+
+ onMusicCollectionResult(
+ requestId: string,
+ result: MusicCollection,
+ errorCode: MusicContentCenterStateReason
+ ) {
+ this.info('onMusicCollectionResult', requestId, result, errorCode);
+ this.setState({
+ musicCollection: result,
+ musics: Array.from({ length: result.getCount() }, (value, index) => {
+ return result.getMusic(index);
+ }),
+ });
+ }
+
+ onPreLoadEvent(
+ requestId: string,
+ songCode: number,
+ percent: number,
+ lyricUrl: string,
+ status: PreloadState,
+ errorCode: MusicContentCenterStateReason
+ ) {
+ this.info(
+ 'onPreLoadEvent',
+ requestId,
+ songCode,
+ percent,
+ lyricUrl,
+ status,
+ errorCode
+ );
+ if (songCode === this.state.songCode) {
+ this.setState({
+ preload: status === PreloadState.KPreloadStateCompleted,
+ });
+ }
+ }
+
+ onLyricResult(
+ requestId: string,
+ songCode: number,
+ lyricUrl: string,
+ errorCode: MusicContentCenterStateReason
+ ) {
+ this.info('onLyricResult', requestId, songCode, lyricUrl, errorCode);
+ }
+
+ onPlayerSourceStateChanged(state: MediaPlayerState, ec: MediaPlayerReason) {
+ this.info('onPlayerSourceStateChanged', 'state', state, 'ec', ec);
+ switch (state) {
+ case MediaPlayerState.PlayerStateIdle:
+ break;
+ case MediaPlayerState.PlayerStateOpening:
+ break;
+ case MediaPlayerState.PlayerStateOpenCompleted:
+ this.setState({
+ open: true,
+ duration: this.player?.getDuration() ?? 0,
+ });
+ break;
+ case MediaPlayerState.PlayerStatePlaying:
+ this.setState({ play: true, pause: false });
+ break;
+ case MediaPlayerState.PlayerStatePaused:
+ this.setState({ pause: true });
+ break;
+ case MediaPlayerState.PlayerStatePlaybackCompleted:
+ case MediaPlayerState.PlayerStatePlaybackAllLoopsCompleted:
+ this.setState({ play: false });
+ break;
+ case MediaPlayerState.PlayerStateStopped:
+ this.setState({
+ open: false,
+ play: false,
+ pause: false,
+ });
+ break;
+ case MediaPlayerState.PlayerStatePausingInternal:
+ break;
+ case MediaPlayerState.PlayerStateStoppingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSeekingInternal:
+ break;
+ case MediaPlayerState.PlayerStateGettingInternal:
+ break;
+ case MediaPlayerState.PlayerStateNoneInternal:
+ break;
+ case MediaPlayerState.PlayerStateDoNothingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSetTrackInternal:
+ break;
+ case MediaPlayerState.PlayerStateFailed:
+ break;
+ }
+ }
+
+ onPositionChanged(position: number) {
+ this.info('onPositionChanged', 'position', position);
+ this.setState({ position: position });
+ }
+
+ onPlayerEvent(
+ eventCode: MediaPlayerEvent,
+ elapsedTime: number,
+ message: string
+ ) {
+ this.info(
+ 'onPlayerEvent',
+ 'eventCode',
+ eventCode,
+ 'elapsedTime',
+ elapsedTime,
+ 'message',
+ message
+ );
+ }
+
+ protected renderChannel(): ReactElement | undefined {
+ return undefined;
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ rtmAppId,
+ rtmToken,
+ musicChartInfos,
+ musicChartId,
+ musics,
+ songCode,
+ open,
+ preload,
+ position,
+ duration,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ rtmAppId: text });
+ }}
+ placeholder={`rtmAppId`}
+ value={rtmAppId}
+ />
+ {
+ this.setState({ rtmToken: text });
+ }}
+ placeholder={`rtmToken`}
+ value={rtmToken}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ mccUid: text === '' ? this.createState().mccUid : +text,
+ });
+ }}
+ placeholder={`mccUid (defaults: ${this.createState().mccUid})`}
+ />
+
+ {
+ return {
+ value: value.id!,
+ label: value.chartName!,
+ };
+ })}
+ value={musicChartId}
+ onValueChange={(value) => {
+ this.setState({ musicChartId: value });
+ }}
+ />
+ {musicChartId >= 0 ? (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ page: text === '' ? this.createState().page : +text,
+ });
+ }}
+ placeholder={`page (defaults: ${this.createState().page})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ pageSize: text === '' ? this.createState().pageSize : +text,
+ });
+ }}
+ placeholder={`pageSize (defaults: ${
+ this.createState().pageSize
+ })`}
+ />
+
+ {
+ return {
+ value: value.songCode!,
+ label: `${value.name}-${value.singer}`,
+ };
+ })}
+ value={songCode}
+ onValueChange={(value) => {
+ this.setState(
+ { songCode: value, preload: false, position: 0 },
+ () => {
+ setTimeout(() => {
+ this.stop();
+ this.preload();
+ });
+ }
+ );
+ }}
+ />
+ >
+ ) : undefined}
+ {songCode >= 0 ? (
+
+ ) : undefined}
+ {preload ? (
+ {
+ this.seek(value);
+ }}
+ />
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const { musics, songCode } = this.state;
+ return +songCode >= 0 ? (
+ {
+ return value.songCode == songCode;
+ })?.poster,
+ }}
+ />
+ ) : undefined;
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { open, play, pause } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/example/src/examples/advanced/PictureInPicture/PictureInPicture.md b/examples/expo/app/examples/advanced/PictureInPicture/PictureInPicture.md
similarity index 100%
rename from example/src/examples/advanced/PictureInPicture/PictureInPicture.md
rename to examples/expo/app/examples/advanced/PictureInPicture/PictureInPicture.md
diff --git a/examples/expo/app/examples/advanced/PictureInPicture/PictureInPicture.tsx b/examples/expo/app/examples/advanced/PictureInPicture/PictureInPicture.tsx
new file mode 100644
index 000000000..0ea6f9a32
--- /dev/null
+++ b/examples/expo/app/examples/advanced/PictureInPicture/PictureInPicture.tsx
@@ -0,0 +1,688 @@
+import React, { ReactElement } from 'react';
+import { AppState, AppStateStatus, Platform } from 'react-native';
+import {
+ AgoraPipContentViewLayout,
+ AgoraPipOptions,
+ AgoraPipState,
+ AgoraPipStateChangedObserver,
+ ChannelProfileType,
+ ClientRoleType,
+ ErrorCodeType,
+ IRtcEngineEventHandler,
+ RenderModeType,
+ RtcConnection,
+ RtcRendererViewProps,
+ RtcStats,
+ RtcSurfaceView,
+ RtcTextureView,
+ UserOfflineReasonType,
+ VideoCanvas,
+ VideoMirrorModeType,
+ VideoSourceType,
+ VideoViewSetupMode,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraCard,
+ AgoraDivider,
+ AgoraList,
+ AgoraStyle,
+ AgoraSwitch,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import AgoraServiceHelper from '../../../../src/utils/AgoraServiceHelper';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ pipContentWidth: number;
+ pipContentHeight: number;
+ pipState: number;
+ renderByTextureView: boolean;
+ isPipAutoEnterSupported: boolean;
+ isPipSupported: boolean;
+ isPipDisposed: boolean;
+ pipContentRow: number;
+ pipContentCol: number;
+}
+
+export default class PictureInPicture
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, AgoraPipStateChangedObserver
+{
+ appState: AppStateStatus = AppState.currentState;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ pipContentWidth: 960,
+ pipContentHeight: 540,
+ pipState: AgoraPipState.pipStateStopped,
+ renderByTextureView: false,
+ isPipAutoEnterSupported: true,
+ isPipSupported: true,
+ isPipDisposed: false,
+ pipContentRow: 1,
+ pipContentCol: 0,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+ this.engine.getAgoraPip().registerPipStateChangedObserver(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Start preview before joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+
+ this.setState({
+ isPipAutoEnterSupported: this.engine
+ .getAgoraPip()
+ .pipIsAutoEnterSupported(),
+ isPipSupported: this.engine.getAgoraPip().pipIsSupported(),
+ });
+
+ const appStateListener = (nextAppState: AppStateStatus) => {
+ if (
+ this.appState.match(/inactive|background/) &&
+ nextAppState === 'active'
+ ) {
+ this.setState({ pipState: AgoraPipState.pipStateStopped });
+ if (Platform.OS === 'android') {
+ if (this.updatePipState) {
+ this.updatePipState(AgoraPipState.pipStateStopped);
+ }
+ }
+ }
+
+ this.appState = nextAppState;
+ };
+ AppState.addEventListener('change', appStateListener);
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: setupPip
+ */
+ setupPip = () => {
+ const {
+ isPipSupported,
+ pipContentWidth,
+ pipContentHeight,
+ isPipAutoEnterSupported,
+ remoteUsers,
+ pipContentRow,
+ pipContentCol,
+ channelId,
+ uid,
+ } = this.state;
+
+ if (!isPipSupported) {
+ return this.error('Picture-in-Picture is not supported on this device');
+ }
+
+ let options: AgoraPipOptions = {
+ // Setting autoEnterEnabled to true enables seamless transition to PiP mode when the app enters background,
+ // providing the best user experience recommended by both Android and iOS platforms.
+ autoEnterEnabled: isPipAutoEnterSupported,
+ };
+ if (Platform.OS === 'android') {
+ options = {
+ // Keep the aspect ratio same as the video view. The aspectRatioX and aspectRatioY values
+ // should match your video dimensions for optimal display. For example, for 1080p video,
+ // use 16:9 ratio (1920:1080 simplified to 16:9).
+ aspectRatioX: pipContentWidth,
+ aspectRatioY: pipContentHeight,
+
+ // According to https://developer.android.com/develop/ui/views/picture-in-picture#set-sourcerecthint
+ // The sourceRectHint defines the initial position and size of the PiP window during the transition animation.
+ // Setting proper values helps create a smooth animation from your video view to the PiP window.
+ // If not set correctly, the system may apply a default content overlay, resulting in a jarring transition.
+ sourceRectHintLeft: 0,
+ sourceRectHintTop: 0,
+ sourceRectHintRight: 0,
+ sourceRectHintBottom: 0,
+
+ // According to https://developer.android.com/develop/ui/views/picture-in-picture#seamless-resizing
+ // The seamlessResizeEnabled flag enables smooth resizing of the PiP window.
+ // Set this to true for video content to allow continuous playback during resizing.
+ // Set this to false for non-video content where seamless resizing isn't needed.
+ seamlessResizeEnabled: true,
+
+ // The external state monitor checks the PiP view state at the interval specified by externalStateMonitorInterval (100ms).
+ useExternalStateMonitor: true,
+ externalStateMonitorInterval: 100,
+ };
+ } else {
+ let contentViewLayout: AgoraPipContentViewLayout = {
+ padding: 0,
+ spacing: 2,
+ row: pipContentRow,
+ column: pipContentCol,
+ };
+
+ let videoStreams: RtcRendererViewProps[] = [
+ //this is the local user, please do not set uid for it
+ {
+ connection: {
+ channelId,
+ localUid: uid,
+ },
+ canvas: {
+ sourceType: VideoSourceType.VideoSourceCamera,
+ setupMode: VideoViewSetupMode.VideoViewSetupAdd, //please use VideoViewSetupAdd only
+ renderMode: RenderModeType.RenderModeHidden,
+ mirrorMode: VideoMirrorModeType.VideoMirrorModeEnabled,
+ },
+ },
+ ...remoteUsers.map((userUid) => {
+ return {
+ connection: {
+ channelId,
+ localUid: userUid,
+ },
+ //this is the remote user, please set uid for it
+ canvas: {
+ uid: userUid,
+ sourceType: VideoSourceType.VideoSourceRemote,
+ setupMode: VideoViewSetupMode.VideoViewSetupAdd, //please use VideoViewSetupAdd only
+ renderMode: RenderModeType.RenderModeHidden,
+ },
+ };
+ }),
+ ];
+
+ options = {
+ // Use preferredContentWidth and preferredContentHeight to set the size of the PIP window.
+ // These values determine the initial dimensions and can be adjusted while PIP is active.
+ // For optimal user experience, we recommend matching these dimensions to your video view size.
+ // The system may adjust the final window size to maintain system constraints.
+ preferredContentWidth: pipContentWidth,
+ preferredContentHeight: pipContentHeight,
+
+ // The sourceContentView determines the source frame for the PiP animation and restore target.
+ // Pass 0 to use the app's root view. For optimal animation, set this to the view containing
+ // your video content. The system uses this view for the PiP enter/exit animations and as the
+ // restore target when returning to the app or stopping PiP.
+ sourceContentView: 0,
+
+ // The contentView determines which view will be displayed in the PIP window.
+ // If you pass 0, the PIP controller will automatically manage and display all video streams.
+ // If you pass a specific view ID, you become responsible for managing the content shown in the PIP window.
+ contentView: 0, // force to use native view
+
+ // The contentViewLayout determines the layout of video streams in the PIP window.
+ // You can customize the grid layout by specifying:
+ // - padding: Space between the window edge and content (in pixels)
+ // - spacing: Space between video streams (in pixels)
+ // - row: Number of rows in the grid layout
+ // - column: Number of columns in the grid layout
+ //
+ // The SDK provides a basic grid layout system that arranges video streams in a row x column matrix.
+ // For example:
+ // - row=2, column=2: Up to 4 video streams in a 2x2 grid
+ // - row=1, column=2: Up to 2 video streams side by side
+ // - row=2, column=1: Up to 2 video streams stacked vertically
+ //
+ // Note:
+ // - This layout configuration only takes effect when contentView is 0 (using native view)
+ // - The grid layout is filled from left-to-right, top-to-bottom
+ // - Empty cells will be left blank if there are fewer streams than grid spaces
+ // - For custom layouts beyond the grid system, set contentView to your own view ID
+ contentViewLayout,
+
+ // The videoStreams array specifies which video streams to display in the PIP window.
+ // Each stream can be configured with properties like uid, sourceType, setupMode, and renderMode.
+ // Note:
+ // - This configuration only takes effect when contentView is set to 0 (native view mode).
+ // - The streams will be laid out according to the contentViewLayout grid configuration.
+ // - The order of the video streams in the array determines the display order in the PIP window.
+ // - The SDK will automatically create and manage native views for each video stream.
+ // - The view property in VideoCanvas will be replaced by the SDK-managed native view.
+ // - You can customize the rendering of each stream using properties like renderMode and mirrorMode.
+ videoStreams,
+
+ // The controlStyle property determines which controls are visible in the PiP window.
+ // Available styles:
+ // * 0: Show all system controls (default) - includes play/pause, forward/backward, close and restore buttons
+ // * 1: Hide forward and backward buttons - shows only play/pause, close and restore buttons
+ // * 2: Hide play/pause button and progress bar - shows only close and restore buttons (recommended)
+ // * 3: Hide all system controls - no buttons visible, including close and restore
+ //
+ // Note: For most video conferencing use cases, style 2 is recommended since playback controls
+ // are not relevant and may confuse users. The close and restore buttons provide essential
+ // window management functionality.
+ // Note: We do not handle the event of other controls, so the recommended style is 2 or 3.
+ controlStyle: 2, // only show close and restore button
+ };
+ }
+ this.info(`[setupPip] options: ${JSON.stringify(options)}`);
+ this.engine?.getAgoraPip().pipSetup(options);
+ };
+
+ /**
+ * Step 3-2: startPip
+ */
+ startPip = () => {
+ this.engine?.getAgoraPip().pipStart();
+ };
+
+ /**
+ * Step 3-3: stopPip
+ */
+ stopPip = () => {
+ if (
+ Platform.OS !== 'android' &&
+ this.engine?.getAgoraPip().pipIsSupported()
+ ) {
+ this.engine?.getAgoraPip().pipStop();
+ }
+ };
+
+ /**
+ * Step 3-4: pipDispose
+ */
+ pipDispose = () => {
+ this.engine?.getAgoraPip().pipDispose();
+ this.setState({
+ isPipDisposed: true,
+ pipState: AgoraPipState.pipStateStopped,
+ });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.getAgoraPip().unregisterPipStateChangedObserver(this);
+ this.engine?.getAgoraPip().release();
+ this.pipDispose();
+ this.engine?.release();
+ }
+
+ onError(err: ErrorCodeType, msg: string) {
+ super.onError(err, msg);
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ super.onLeaveChannel(connection, stats);
+ this.pipDispose();
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ this.setState(
+ (preState) => {
+ return {
+ remoteUsers: [...(preState.remoteUsers ?? []), remoteUid],
+ };
+ },
+ () => {
+ // Because the window rendering and pip setup are asynchronous, we need to ensure that the window rendering is prioritized,
+ // so we need to use setTimeout to ensure that the window rendering is completed before the pip setup.
+ setTimeout(() => {
+ this.setupPip();
+ }, 0);
+ }
+ );
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ this.setState(
+ (preState) => {
+ return {
+ remoteUsers: preState.remoteUsers?.filter((uid) => uid !== remoteUid),
+ };
+ },
+ () => {
+ this.setupPip();
+ }
+ );
+ }
+
+ onPipStateChanged(state: AgoraPipState, error: string | null): void {
+ this.info('onPipStateChanged', 'state', state, 'error', error);
+
+ // iOS show the pip window by UIView, so you don't need to handle the UI by yourself
+ // Android show the pip window by Activity, so you need to handle the UI by yourself
+ if (Platform.OS === 'android') {
+ if (this.updatePipState) {
+ this.updatePipState(state);
+ }
+ }
+
+ if (state === AgoraPipState.pipStateFailed) {
+ // if you destroy the source view of pip controller, some error may happen,
+ // so we need to dispose the pip controller here.
+ this.pipDispose();
+ }
+
+ this.setState({ pipState: state });
+ }
+
+ componentDidMount() {
+ super.componentDidMount();
+ if (Platform.OS === 'android') {
+ AgoraServiceHelper.startForegroundService();
+ }
+ }
+
+ componentWillUnmount() {
+ super.componentWillUnmount();
+ if (Platform.OS === 'android') {
+ AgoraServiceHelper.stopForegroundService();
+ }
+ }
+
+ protected renderChannel(): ReactElement | undefined {
+ const { channelId, joinChannelSuccess, pipState } = this.state;
+ const isAndroidAndInPip =
+ Platform.OS === 'android' && pipState === AgoraPipState.pipStateStarted;
+
+ return !isAndroidAndInPip ? (
+ <>
+ {
+ this.setState({ channelId: text });
+ }}
+ placeholder={`channelId`}
+ value={channelId}
+ />
+ {
+ joinChannelSuccess ? this.leaveChannel() : this.joinChannel();
+ }}
+ />
+ >
+ ) : undefined;
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const {
+ enableVideo,
+ startPreview,
+ joinChannelSuccess,
+ remoteUsers,
+ pipState,
+ } = this.state;
+
+ return enableVideo ? (
+ <>
+ {!!startPreview || joinChannelSuccess
+ ? this.renderUser({
+ uid: 0,
+ sourceType: VideoSourceType.VideoSourceCamera,
+ })
+ : undefined}
+ {!!startPreview || joinChannelSuccess ? (
+
+ this.renderUser({
+ uid: item,
+ sourceType: VideoSourceType.VideoSourceRemote,
+ })!
+ }
+ />
+ ) : undefined}
+ >
+ ) : undefined;
+ }
+
+ protected renderUser(user: VideoCanvas): ReactElement | undefined {
+ const video = this.renderVideo(user);
+ const { pipState } = this.state;
+
+ const isAndroidAndInPip =
+ Platform.OS === 'android' && pipState === AgoraPipState.pipStateStarted;
+
+ return user.uid === 0 || isAndroidAndInPip ? (
+ video
+ ) : (
+
+ {video}
+
+ );
+ }
+
+ protected renderVideo(user: VideoCanvas): ReactElement | undefined {
+ const { renderByTextureView, pipState } = this.state;
+ return renderByTextureView ? (
+
+ ) : (
+ <>
+
+ >
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess, renderByTextureView, pipState } =
+ this.state;
+
+ const isAndroidAndInPip =
+ Platform.OS === 'android' && pipState === AgoraPipState.pipStateStarted;
+
+ return !isAndroidAndInPip ? (
+ <>
+ {
+ this.setState({ renderByTextureView: value });
+ }}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ pipContentWidth:
+ text === '' ? this.createState().pipContentWidth : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`pipContentWidth (defaults: ${
+ this.createState().pipContentWidth
+ })`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ pipContentHeight:
+ text === '' ? this.createState().pipContentHeight : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`pipContentHeight (defaults: ${
+ this.createState().pipContentHeight
+ })`}
+ />
+ {Platform.OS === 'ios' && (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ pipContentRow:
+ text === '' ? this.createState().pipContentRow : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`pipContentRow (defaults: ${
+ this.createState().pipContentRow
+ })`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ pipContentCol:
+ text === '' ? this.createState().pipContentCol : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`pipContentCol (defaults: ${
+ this.createState().pipContentCol
+ })`}
+ />
+ >
+ )}
+ >
+ ) : undefined;
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { pipState, isPipSupported } = this.state;
+ const isAndroidAndInPip =
+ Platform.OS === 'android' && pipState === AgoraPipState.pipStateStarted;
+
+ return isPipSupported && !isAndroidAndInPip ? (
+ <>
+ {
+ this.setupPip();
+ }}
+ />
+ {
+ if (pipState === AgoraPipState.pipStateStarted) {
+ this.stopPip();
+ } else {
+ this.startPip();
+ }
+ }}
+ />
+ {
+ this.pipDispose();
+ }}
+ />
+ >
+ ) : undefined;
+ }
+}
diff --git a/examples/expo/app/examples/advanced/PlayEffect/PlayEffect.tsx b/examples/expo/app/examples/advanced/PlayEffect/PlayEffect.tsx
new file mode 100644
index 000000000..14bc65080
--- /dev/null
+++ b/examples/expo/app/examples/advanced/PlayEffect/PlayEffect.tsx
@@ -0,0 +1,299 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraSlider,
+ AgoraSwitch,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { getAbsolutePath, getResourcePath } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ soundId: number;
+ filePath: string;
+ loopCount: number;
+ pitch: number;
+ pan: number;
+ gain: number;
+ publish: boolean;
+ startPos: number;
+ playEffect: boolean;
+ pauseEffect: boolean;
+}
+
+export default class PlayEffect
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ soundId: 0,
+ filePath: getResourcePath('effect.mp3'),
+ loopCount: 1,
+ pitch: 1.0,
+ pan: 0,
+ gain: 100,
+ publish: false,
+ startPos: 0,
+ playEffect: false,
+ pauseEffect: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: playEffect
+ */
+ playEffect = async () => {
+ const {
+ soundId,
+ filePath,
+ loopCount,
+ pitch,
+ pan,
+ gain,
+ publish,
+ startPos,
+ } = this.state;
+ if (!filePath) {
+ this.error('filePath is invalid');
+ return;
+ }
+ if (startPos < 0) {
+ this.error('startPos is invalid');
+ return;
+ }
+
+ this.engine?.playEffect(
+ soundId,
+ await getAbsolutePath(filePath),
+ loopCount,
+ pitch,
+ pan,
+ gain,
+ publish,
+ startPos
+ );
+ this.setState({ playEffect: true, pauseEffect: false });
+ };
+
+ /**
+ * Step 3-2 (Optional): pauseEffect
+ */
+ pauseEffect = () => {
+ const { soundId } = this.state;
+ this.engine?.pauseEffect(soundId);
+ this.setState({ pauseEffect: true });
+ };
+
+ /**
+ * Step 3-3 (Optional): resumeEffect
+ */
+ resumeEffect = () => {
+ const { soundId } = this.state;
+ this.engine?.resumeEffect(soundId);
+ this.setState({ pauseEffect: false });
+ };
+
+ /**
+ * Step 3-4: stopEffect
+ */
+ stopEffect = () => {
+ const { soundId } = this.state;
+ this.engine?.stopEffect(soundId);
+ this.setState({ playEffect: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onAudioEffectFinished(soundId: number) {
+ this.info('onAudioEffectFinished', 'soundId', soundId);
+ this.setState({ playEffect: false });
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { filePath, pitch, pan, gain, publish } = this.state;
+ return (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ soundId: text === '' ? this.createState().soundId : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`soundId (defaults: ${this.createState().soundId})`}
+ />
+ {
+ this.setState({ filePath: text });
+ }}
+ placeholder={'filePath'}
+ value={filePath}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ loopCount: text === '' ? this.createState().loopCount : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`loopCount (defaults: ${this.createState().loopCount})`}
+ />
+ {
+ this.setState({ pitch: value });
+ }}
+ />
+
+ {
+ this.setState({ pan: value });
+ }}
+ />
+
+ {
+ this.setState({ gain: value });
+ }}
+ />
+
+ {
+ this.setState({ publish: value });
+ }}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ startPos: text === '' ? this.createState().startPos : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`startPos (defaults: ${this.createState().startPos})`}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { playEffect, pauseEffect } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/ProcessVideoRawData/ProcessVideoRawData.tsx b/examples/expo/app/examples/advanced/ProcessVideoRawData/ProcessVideoRawData.tsx
new file mode 100644
index 000000000..243d3e029
--- /dev/null
+++ b/examples/expo/app/examples/advanced/ProcessVideoRawData/ProcessVideoRawData.tsx
@@ -0,0 +1,161 @@
+import React, { ReactElement } from 'react';
+import { NativeModules } from 'react-native';
+
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ ErrorCodeType,
+ IRtcEngineEventHandler,
+ RtcConnection,
+ RtcStats,
+ RtcSurfaceView,
+ UserOfflineReasonType,
+ VideoCanvas,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import { AgoraStyle } from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+const { VideoRawDataNativeModule } = NativeModules;
+
+interface State extends BaseVideoComponentState {}
+
+export default class ProcessVideoRawData
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ VideoRawDataNativeModule.initialize(appId);
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Start preview before joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ VideoRawDataNativeModule.releaseModule();
+ this.engine?.release();
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ return super.renderUsers();
+ }
+
+ onError(err: ErrorCodeType, msg: string) {
+ super.onError(err, msg);
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ super.onLeaveChannel(connection, stats);
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ super.onUserJoined(connection, remoteUid, elapsed);
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ super.onUserOffline(connection, remoteUid, reason);
+ }
+
+ protected renderVideo(user: VideoCanvas): ReactElement | undefined {
+ return (
+
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/PushVideoFrame/PushVideoFrame.tsx b/examples/expo/app/examples/advanced/PushVideoFrame/PushVideoFrame.tsx
new file mode 100644
index 000000000..1d69c67b5
--- /dev/null
+++ b/examples/expo/app/examples/advanced/PushVideoFrame/PushVideoFrame.tsx
@@ -0,0 +1,198 @@
+import React, { ReactElement } from 'react';
+import { Platform } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ ExternalVideoSourceType,
+ IRtcEngineEventHandler,
+ IRtcEngineEx,
+ VideoBufferType,
+ VideoPixelFormat,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+// @ts-ignore
+import ImageTools from 'react-native-image-tool';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraImage,
+ AgoraStyle,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { getAbsolutePath, getResourcePath } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ filePath: string;
+}
+
+export default class PushVideoFrame
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ // @ts-ignore
+ protected engine?: IRtcEngineEx;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ filePath: getResourcePath('agora-logo.png'),
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine() as IRtcEngineEx;
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ this.setExternalVideoSource();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishCameraTrack: false,
+ publishEncodedVideoTrack: true,
+ });
+ }
+
+ /**
+ * Step 3-1: setExternalVideoSource
+ */
+ setExternalVideoSource = () => {
+ this.engine
+ ?.getMediaEngine()
+ .setExternalVideoSource(true, false, ExternalVideoSourceType.VideoFrame);
+ };
+
+ /**
+ * Step 3-2: pushVideoFrame
+ */
+ pushVideoFrame = () => {
+ const { filePath } = this.state;
+ if (!filePath) {
+ this.error('filePath is invalid');
+ return;
+ }
+
+ getAbsolutePath(filePath).then((path) => {
+ ImageTools.GetImageRGBAs(path).then((value: any) => {
+ this.engine?.getMediaEngine().pushVideoFrame({
+ type: VideoBufferType.VideoBufferRawData,
+ format: VideoPixelFormat.VideoPixelRgba,
+ buffer: value.rgba,
+ stride: value.width,
+ height: value.height,
+ });
+ });
+ });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { filePath } = this.state;
+ return (
+ <>
+ {
+ this.setState({ filePath: text });
+ }}
+ placeholder={`filePath`}
+ value={filePath}
+ />
+
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/RTMPStreaming/RTMPStreaming.tsx b/examples/expo/app/examples/advanced/RTMPStreaming/RTMPStreaming.tsx
new file mode 100644
index 000000000..b9e61b33c
--- /dev/null
+++ b/examples/expo/app/examples/advanced/RTMPStreaming/RTMPStreaming.tsx
@@ -0,0 +1,541 @@
+import React, { ReactElement } from 'react';
+import {
+ AudioCodecProfileType,
+ AudioSampleRateType,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ LiveTranscoding,
+ RtmpStreamPublishReason,
+ RtmpStreamPublishState,
+ RtmpStreamingEvent,
+ TranscodingUser,
+ VideoCodecProfileType,
+ VideoCodecTypeForStream,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+import ColorPicker, { Panel1 } from 'reanimated-color-picker';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+ AgoraStyle,
+ AgoraSwitch,
+ AgoraText,
+ AgoraTextInput,
+ AgoraView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ url: string;
+ startRtmpStreamWithTranscoding: boolean;
+ width: number;
+ height: number;
+ videoBitrate: number;
+ videoFramerate: number;
+ videoGop: number;
+ videoCodecProfile: VideoCodecProfileType;
+ backgroundColor: string;
+ videoCodecType: VideoCodecTypeForStream;
+ transcodingUsers: TranscodingUser[];
+ watermarkUrl: string;
+ backgroundImageUrl: string;
+ audioSampleRate: AudioSampleRateType;
+ audioBitrate: number;
+ audioChannels: number;
+ audioCodecProfile: AudioCodecProfileType;
+ startRtmpStream: boolean;
+}
+
+export default class RTMPStreaming
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ url: 'rtmp://vid-218.push.chinanetcenter.broadcastapp.agora.io/live/test',
+ startRtmpStreamWithTranscoding: false,
+ width: 360,
+ height: 640,
+ videoBitrate: 400,
+ videoFramerate: 15,
+ videoGop: 30,
+ videoCodecProfile: VideoCodecProfileType.VideoCodecProfileHigh,
+ backgroundColor: '#000000',
+ videoCodecType: VideoCodecTypeForStream.VideoCodecH264ForStream,
+ transcodingUsers: [
+ {
+ uid: 0,
+ x: 0,
+ y: 0,
+ width: AgoraStyle.image.width,
+ height: AgoraStyle.image.height,
+ zOrder: 50,
+ },
+ ],
+ watermarkUrl: 'https://web-cdn.agora.io/doc-center/image/agora-logo.png',
+ backgroundImageUrl:
+ 'https://web-cdn.agora.io/doc-center/image/agora-logo.png',
+ audioSampleRate: AudioSampleRateType.AudioSampleRate48000,
+ audioBitrate: 48,
+ audioChannels: 1,
+ audioCodecProfile: AudioCodecProfileType.AudioCodecProfileLcAac,
+ startRtmpStream: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: startRtmpStream
+ */
+ startRtmpStream = () => {
+ const { url, startRtmpStreamWithTranscoding } = this.state;
+ if (!url) {
+ this.error('url is invalid');
+ return;
+ }
+
+ if (startRtmpStreamWithTranscoding) {
+ this.engine?.startRtmpStreamWithTranscoding(
+ url,
+ this._generateLiveTranscoding()
+ );
+ } else {
+ this.engine?.startRtmpStreamWithoutTranscoding(url);
+ }
+ };
+
+ /**
+ * Step 3-2 (Optional): updateRtmpTranscoding
+ */
+ updateRtmpTranscoding = () => {
+ this.engine?.updateRtmpTranscoding(this._generateLiveTranscoding());
+ };
+
+ _generateLiveTranscoding = (): LiveTranscoding => {
+ const {
+ remoteUsers,
+ width,
+ height,
+ videoBitrate,
+ videoFramerate,
+ videoGop,
+ videoCodecProfile,
+ backgroundColor,
+ videoCodecType,
+ transcodingUsers,
+ watermarkUrl,
+ backgroundImageUrl,
+ audioSampleRate,
+ audioBitrate,
+ audioChannels,
+ audioCodecProfile,
+ } = this.state;
+
+ return {
+ width,
+ height,
+ videoBitrate,
+ videoFramerate,
+ videoGop,
+ videoCodecProfile,
+ backgroundColor: +backgroundColor.replace('#', '0x'),
+ videoCodecType,
+ transcodingUsers: [
+ ...transcodingUsers,
+ ...remoteUsers.map((value, index) => {
+ const maxNumPerRow = Math.floor(width / AgoraStyle.image.width);
+ const numOfRow = Math.floor((index + 1) / maxNumPerRow);
+ const numOfColumn = Math.floor((index + 1) % maxNumPerRow);
+ return {
+ uid: value,
+ x: numOfColumn * AgoraStyle.image.width,
+ y: numOfRow * AgoraStyle.image.height,
+ width: AgoraStyle.image.width,
+ height: AgoraStyle.image.height,
+ zOrder: 50,
+ };
+ }),
+ ],
+ userCount: transcodingUsers.length + remoteUsers.length,
+ watermark: [
+ {
+ url: watermarkUrl,
+ x: width - AgoraStyle.image.width,
+ y: height - AgoraStyle.image.height,
+ width: AgoraStyle.image.width,
+ height: AgoraStyle.image.height,
+ zOrder: 100,
+ },
+ ],
+ watermarkCount: 1,
+ backgroundImage: [
+ {
+ url: backgroundImageUrl,
+ x: 0,
+ y: 0,
+ width,
+ height,
+ zOrder: 1,
+ },
+ ],
+ backgroundImageCount: 1,
+ audioSampleRate,
+ audioBitrate,
+ audioChannels,
+ audioCodecProfile,
+ };
+ };
+
+ /**
+ * Step 3-3: stopRtmpStream
+ */
+ stopRtmpStream = () => {
+ const { url } = this.state;
+ if (!url) {
+ this.error('url is invalid');
+ return;
+ }
+
+ this.engine?.stopRtmpStream(url);
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onRtmpStreamingEvent(url: string, eventCode: RtmpStreamingEvent) {
+ this.info('onRtmpStreamingEvent', 'url', url, 'eventCode', eventCode);
+ }
+
+ onRtmpStreamingStateChanged(
+ url: string,
+ state: RtmpStreamPublishState,
+ errCode: RtmpStreamPublishReason
+ ) {
+ this.info(
+ 'onRtmpStreamingStateChanged',
+ 'url',
+ url,
+ 'state',
+ state,
+ 'errCode',
+ errCode
+ );
+ switch (state) {
+ case RtmpStreamPublishState.RtmpStreamPublishStateIdle:
+ break;
+ case RtmpStreamPublishState.RtmpStreamPublishStateConnecting:
+ break;
+ case RtmpStreamPublishState.RtmpStreamPublishStateRunning:
+ this.setState({ startRtmpStream: true });
+ break;
+ case RtmpStreamPublishState.RtmpStreamPublishStateRecovering:
+ break;
+ case RtmpStreamPublishState.RtmpStreamPublishStateFailure:
+ case RtmpStreamPublishState.RtmpStreamPublishStateDisconnecting:
+ this.setState({ startRtmpStream: false });
+ break;
+ }
+ }
+
+ onTranscodingUpdated() {
+ this.debug('onTranscodingUpdated');
+ }
+
+ onSelectColor = ({ hex }) => {
+ this.setState({
+ backgroundColor: hex,
+ });
+ };
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ url,
+ startRtmpStreamWithTranscoding,
+ videoCodecProfile,
+ backgroundColor,
+ videoCodecType,
+ watermarkUrl,
+ backgroundImageUrl,
+ audioSampleRate,
+ audioChannels,
+ audioCodecProfile,
+ startRtmpStream,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ url: text });
+ }}
+ placeholder={`url`}
+ value={url}
+ />
+ {
+ this.setState({ startRtmpStreamWithTranscoding: value });
+ }}
+ />
+ {startRtmpStreamWithTranscoding ? (
+ <>
+
+ <>
+ backgroundColor
+
+
+
+ >
+
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ width: text === '' ? this.createState().width : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`width (defaults: ${this.createState().width})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ height: text === '' ? this.createState().height : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`height (defaults: ${this.createState().height})`}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ videoBitrate:
+ text === '' ? this.createState().videoBitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`videoBitrate (defaults: ${
+ this.createState().videoBitrate
+ })`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ videoFramerate:
+ text === '' ? this.createState().videoFramerate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`videoFramerate (defaults: ${
+ this.createState().videoFramerate
+ })`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ videoGop: text === '' ? this.createState().videoGop : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`videoGop (defaults: ${
+ this.createState().videoGop
+ })`}
+ />
+ {
+ this.setState({ videoCodecProfile: value });
+ }}
+ />
+
+ {
+ this.setState({ videoCodecType: value });
+ }}
+ />
+
+ {
+ this.setState({ watermarkUrl: text });
+ }}
+ placeholder={'watermarkUrl'}
+ value={watermarkUrl}
+ />
+ {
+ this.setState({ backgroundImageUrl: text });
+ }}
+ placeholder={'backgroundImageUrl'}
+ value={backgroundImageUrl}
+ />
+ {
+ this.setState({ audioSampleRate: value });
+ }}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ audioBitrate:
+ text === '' ? this.createState().audioBitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`audioBitrate (defaults: ${
+ this.createState().audioBitrate
+ })`}
+ />
+ {
+ this.setState({ audioChannels: value });
+ }}
+ />
+
+ {
+ this.setState({ audioCodecProfile: value });
+ }}
+ />
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const {
+ joinChannelSuccess,
+ startRtmpStreamWithTranscoding,
+ startRtmpStream,
+ } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/RhythmPlayer/RhythmPlayer.tsx b/examples/expo/app/examples/advanced/RhythmPlayer/RhythmPlayer.tsx
new file mode 100644
index 000000000..6f213e248
--- /dev/null
+++ b/examples/expo/app/examples/advanced/RhythmPlayer/RhythmPlayer.tsx
@@ -0,0 +1,262 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ RhythmPlayerReason,
+ RhythmPlayerStateType,
+ RtcConnection,
+ RtcStats,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraSlider,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { getAbsolutePath, getResourcePath } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ sound1: string;
+ sound2: string;
+ beatsPerMeasure: number;
+ beatsPerMinute: number;
+ startRhythmPlayer?: boolean;
+}
+
+export default class RhythmPlayer
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ sound1: getResourcePath('ding.mp3'),
+ sound2: getResourcePath('dang.mp3'),
+ beatsPerMeasure: 4,
+ beatsPerMinute: 60,
+ startRhythmPlayer: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ // ⚠️ Must be true, if you want to publish to remote
+ publishRhythmPlayerTrack: true,
+ });
+ }
+
+ /**
+ * Step 3-1: startRhythmPlayer
+ */
+ startRhythmPlayer = async () => {
+ const { sound1, sound2, beatsPerMeasure, beatsPerMinute } = this.state;
+ if (!sound1) {
+ this.error('sound1 is invalid');
+ return;
+ }
+ if (!sound2) {
+ this.error('sound2 is invalid');
+ return;
+ }
+
+ this.engine?.startRhythmPlayer(
+ await getAbsolutePath(sound1),
+ await getAbsolutePath(sound2),
+ {
+ beatsPerMeasure,
+ beatsPerMinute,
+ }
+ );
+ this.engine?.updateChannelMediaOptions({ publishRhythmPlayerTrack: true });
+ };
+
+ /**
+ * Step 3-2 (Optional): configRhythmPlayer
+ */
+ configRhythmPlayer = () => {
+ const { beatsPerMeasure, beatsPerMinute } = this.state;
+ this.engine?.configRhythmPlayer({
+ beatsPerMeasure,
+ beatsPerMinute,
+ });
+ };
+
+ /**
+ * Step 3-3: stopRhythmPlayer
+ */
+ stopRhythmPlayer = () => {
+ this.engine?.stopRhythmPlayer();
+ this.setState({ startRhythmPlayer: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ this.info('onLeaveChannel', 'connection', connection, 'stats', stats);
+ const state = this.createState();
+ delete state.startRhythmPlayer;
+ this.setState(state);
+ }
+
+ onRhythmPlayerStateChanged(
+ state: RhythmPlayerStateType,
+ errorCode: RhythmPlayerReason
+ ) {
+ this.info(
+ 'onRhythmPlayerStateChanged',
+ 'state',
+ state,
+ 'errorCode',
+ errorCode
+ );
+ switch (state) {
+ case RhythmPlayerStateType.RhythmPlayerStateIdle:
+ break;
+ case RhythmPlayerStateType.RhythmPlayerStateOpening:
+ break;
+ case RhythmPlayerStateType.RhythmPlayerStateDecoding:
+ break;
+ case RhythmPlayerStateType.RhythmPlayerStatePlaying:
+ this.setState({ startRhythmPlayer: true });
+ break;
+ case RhythmPlayerStateType.RhythmPlayerStateFailed:
+ break;
+ }
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { sound1, sound2, beatsPerMeasure, beatsPerMinute } = this.state;
+ return (
+ <>
+ {
+ this.setState({ sound1: text });
+ }}
+ placeholder={'sound1'}
+ value={sound1}
+ />
+ {
+ this.setState({ sound2: text });
+ }}
+ placeholder={'sound2'}
+ value={sound2}
+ />
+ {
+ this.setState({ beatsPerMeasure: value });
+ }}
+ />
+
+ {
+ this.setState({ beatsPerMinute: value });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startRhythmPlayer } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/ScreenShare/ScreenShare.tsx b/examples/expo/app/examples/advanced/ScreenShare/ScreenShare.tsx
new file mode 100644
index 000000000..a143023f7
--- /dev/null
+++ b/examples/expo/app/examples/advanced/ScreenShare/ScreenShare.tsx
@@ -0,0 +1,590 @@
+import React, { ReactElement } from 'react';
+import { Platform } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ IRtcEngineEx,
+ LocalVideoStreamReason,
+ LocalVideoStreamState,
+ PermissionType,
+ RenderModeType,
+ RtcConnection,
+ RtcStats,
+ UserOfflineReasonType,
+ VideoCanvas,
+ VideoContentHint,
+ VideoSourceType,
+ createAgoraRtcEngine,
+ showRPSystemBroadcastPickerView,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+ AgoraStyle,
+ AgoraSwitch,
+ AgoraTextInput,
+ AgoraView,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ token2: string;
+ uid2: number;
+ captureAudio: boolean;
+ sampleRate: number;
+ channels: number;
+ captureSignalVolume: number;
+ captureVideo: boolean;
+ width: number;
+ height: number;
+ frameRate: number;
+ bitrate: number;
+ contentHint: VideoContentHint;
+ startScreenCapture: boolean;
+ publishScreenCapture: boolean;
+}
+
+export default class ScreenShare
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ // @ts-ignore
+ protected engine?: IRtcEngineEx;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ token2: '',
+ uid2: 0,
+ captureAudio: false,
+ sampleRate: 16000,
+ channels: 2,
+ captureSignalVolume: 100,
+ captureVideo: true,
+ width: 1280,
+ height: 720,
+ frameRate: 15,
+ bitrate: 0,
+ contentHint: VideoContentHint.ContentHintMotion,
+ startScreenCapture: false,
+ publishScreenCapture: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine() as IRtcEngineEx;
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Start preview before joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: startScreenCapture
+ */
+ startScreenCapture = async () => {
+ const {
+ captureAudio,
+ sampleRate,
+ channels,
+ captureSignalVolume,
+ captureVideo,
+ width,
+ height,
+ frameRate,
+ bitrate,
+ contentHint,
+ } = this.state;
+ this.engine?.startScreenCapture({
+ captureAudio,
+ audioParams: {
+ sampleRate,
+ channels,
+ captureSignalVolume,
+ },
+ captureVideo,
+ videoParams: {
+ dimensions: { width, height },
+ frameRate,
+ bitrate,
+ contentHint,
+ },
+ });
+ this.engine?.startPreview(VideoSourceType.VideoSourceScreen);
+
+ if (Platform.OS === 'ios') {
+ // Show the picker view for screen share, ⚠️ only support for iOS 12+
+ await showRPSystemBroadcastPickerView(true);
+ }
+
+ if (captureAudio && !captureVideo) {
+ this.setState({ startScreenCapture: true });
+ }
+ };
+
+ /**
+ * Step 3-2 (Optional): updateScreenCaptureParameters
+ */
+ updateScreenCaptureParameters = () => {
+ const {
+ captureAudio,
+ sampleRate,
+ channels,
+ captureSignalVolume,
+ captureVideo,
+ width,
+ height,
+ frameRate,
+ bitrate,
+ contentHint,
+ } = this.state;
+ this.engine?.updateScreenCapture({
+ captureAudio,
+ audioParams: {
+ sampleRate,
+ channels,
+ captureSignalVolume,
+ },
+ captureVideo,
+ videoParams: {
+ dimensions: { width, height },
+ frameRate,
+ bitrate,
+ contentHint,
+ },
+ });
+
+ if (!captureAudio && !captureVideo) {
+ this.setState({ startScreenCapture: false });
+ } else {
+ // ⚠️ You should updateChannelMediaOptionsEx if you change captureAudio or captureVideo
+ const { channelId, uid2, publishScreenCapture } = this.state;
+ if (publishScreenCapture) {
+ this.engine?.updateChannelMediaOptionsEx(
+ {
+ publishScreenCaptureAudio: captureAudio,
+ publishScreenCaptureVideo: captureVideo,
+ },
+ { channelId, localUid: uid2 }
+ );
+ }
+ }
+ };
+
+ /**
+ * Step 3-3: publishScreenCapture
+ */
+ publishScreenCapture = () => {
+ const { channelId, token2, uid2 } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid2 <= 0) {
+ this.error('uid2 is invalid');
+ return;
+ }
+
+ // publish screen share stream
+ this.engine?.joinChannelEx(
+ token2,
+ { channelId, localUid: uid2 },
+ {
+ autoSubscribeAudio: false,
+ autoSubscribeVideo: false,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishScreenCaptureAudio: true,
+ publishScreenCaptureVideo: true,
+ }
+ );
+ };
+
+ /**
+ * Step 3-4: stopScreenCapture
+ */
+ stopScreenCapture = () => {
+ this.engine?.stopScreenCapture();
+ this.setState({ startScreenCapture: false });
+ };
+
+ /**
+ * Step 3-5: unpublishScreenCapture
+ */
+ unpublishScreenCapture = () => {
+ const { channelId, uid2 } = this.state;
+ this.engine?.leaveChannelEx({ channelId, localUid: uid2 });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2) {
+ this.info(
+ 'onJoinChannelSuccess',
+ 'connection',
+ connection,
+ 'elapsed',
+ elapsed
+ );
+ this.setState({ publishScreenCapture: true });
+ return;
+ }
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2) {
+ this.info('onLeaveChannel', 'connection', connection, 'stats', stats);
+ this.setState({ publishScreenCapture: false });
+ return;
+ }
+ super.onLeaveChannel(connection, stats);
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2 || remoteUid === uid2) {
+ // ⚠️ mute the streams from screen sharing
+ this.engine?.muteRemoteAudioStream(uid2, true);
+ this.engine?.muteRemoteVideoStream(uid2, true);
+ return;
+ }
+ super.onUserJoined(connection, remoteUid, elapsed);
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2 || remoteUid === uid2) return;
+ super.onUserOffline(connection, remoteUid, reason);
+ }
+
+ onPermissionError(permissionType: PermissionType) {
+ this.info('onPermissionError', 'permissionType', permissionType);
+ // ⚠️ You should call stopScreenCapture if received the event with permissionType ScreenCapture,
+ // otherwise you can not startScreenCapture again
+ this.stopScreenCapture();
+ this.setState({
+ startScreenCapture: false,
+ });
+ }
+
+ onLocalVideoStateChanged(
+ source: VideoSourceType,
+ state: LocalVideoStreamState,
+ error: LocalVideoStreamReason
+ ) {
+ this.info(
+ 'onLocalVideoStateChanged',
+ 'source',
+ source,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ if (source === VideoSourceType.VideoSourceScreen) {
+ switch (state) {
+ case LocalVideoStreamState.LocalVideoStreamStateStopped:
+ case LocalVideoStreamState.LocalVideoStreamStateFailed:
+ break;
+ case LocalVideoStreamState.LocalVideoStreamStateCapturing:
+ case LocalVideoStreamState.LocalVideoStreamStateEncoding:
+ this.setState({ startScreenCapture: true });
+ break;
+ }
+ }
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const { startScreenCapture } = this.state;
+ return (
+ <>
+ {super.renderUsers()}
+ {startScreenCapture ? (
+
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderVideo(user: VideoCanvas): ReactElement | undefined {
+ return super.renderVideo({
+ ...user,
+ renderMode: RenderModeType.RenderModeFit,
+ });
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ uid2,
+ captureAudio,
+ captureSignalVolume,
+ captureVideo,
+ contentHint,
+ publishScreenCapture,
+ } = this.state;
+ return (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ uid2: text === '' ? this.createState().uid2 : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`uid2 (must > 0)`}
+ value={uid2 > 0 ? uid2.toString() : ''}
+ />
+ {
+ this.setState({ captureAudio: value });
+ }}
+ />
+
+ {captureAudio ? (
+ <>
+ {Platform.OS === 'android' ? (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ sampleRate:
+ text === '' ? this.createState().sampleRate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`sampleRate (defaults: ${
+ this.createState().sampleRate
+ })`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ channels:
+ text === '' ? this.createState().channels : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`channels (defaults: ${
+ this.createState().channels
+ })`}
+ />
+ >
+ ) : undefined}
+ {
+ this.setState({ captureSignalVolume: value });
+ }}
+ />
+
+ >
+ ) : undefined}
+ {
+ this.setState({ captureVideo: value });
+ }}
+ />
+
+ {captureVideo ? (
+ <>
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ width: text === '' ? this.createState().width : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`width (defaults: ${this.createState().width})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ height: text === '' ? this.createState().height : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`height (defaults: ${this.createState().height})`}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ frameRate: text === '' ? this.createState().frameRate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`frameRate (defaults: ${
+ this.createState().frameRate
+ })`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ bitrate: text === '' ? this.createState().bitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`bitrate (defaults: ${this.createState().bitrate})`}
+ />
+ {
+ this.setState({ contentHint: value });
+ }}
+ />
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startScreenCapture, publishScreenCapture } = this.state;
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/SendMetadata/SendMetadata.tsx b/examples/expo/app/examples/advanced/SendMetadata/SendMetadata.tsx
new file mode 100644
index 000000000..476c0638e
--- /dev/null
+++ b/examples/expo/app/examples/advanced/SendMetadata/SendMetadata.tsx
@@ -0,0 +1,183 @@
+import { Buffer } from 'buffer';
+
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IMetadataObserver,
+ IRtcEngineEventHandler,
+ Metadata,
+ MetadataType,
+ VideoSourceType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import { AgoraButton, AgoraTextInput } from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ metadataBuffer: string;
+}
+
+export default class SendMetadata
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler, IMetadataObserver
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ metadataBuffer: '',
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ this.registerMediaMetadataObserver();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: registerMediaMetadataObserver
+ */
+ registerMediaMetadataObserver = () => {
+ this.engine?.registerMediaMetadataObserver(
+ this,
+ MetadataType.VideoMetadata
+ );
+ };
+
+ /**
+ * Step 3-2: sendMetaData
+ */
+ sendMetaData = () => {
+ const { metadataBuffer } = this.state;
+ if (!metadataBuffer) {
+ this.error('metadataBuffer is invalid');
+ return;
+ }
+
+ const buffer = Buffer.from(metadataBuffer);
+ this.engine?.sendMetaData(
+ {
+ buffer: buffer,
+ size: buffer.length,
+ },
+ VideoSourceType.VideoSourceCamera
+ );
+ this.setState({ metadataBuffer: '' });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onMetadataReceived(metadata: Metadata) {
+ this.info('onMetadataReceived', 'metadata', metadata);
+ this.alert(
+ `Receive from uid:${metadata.uid}`,
+ `${metadata.buffer?.toString()}`
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { metadataBuffer } = this.state;
+ return (
+ <>
+ {
+ this.setState({ metadataBuffer: text });
+ }}
+ placeholder={`metadataBuffer`}
+ value={metadataBuffer}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/SendMultiVideoStream/SendMultiVideoStream.tsx b/examples/expo/app/examples/advanced/SendMultiVideoStream/SendMultiVideoStream.tsx
new file mode 100644
index 000000000..62ee7c3d1
--- /dev/null
+++ b/examples/expo/app/examples/advanced/SendMultiVideoStream/SendMultiVideoStream.tsx
@@ -0,0 +1,364 @@
+import React, { ReactElement } from 'react';
+import {
+ AudioFrame,
+ AudioPcmFrame,
+ ChannelProfileType,
+ ClientRoleType,
+ IAudioFrameObserver,
+ IAudioPcmFrameSink,
+ IMediaPlayer,
+ IMediaPlayerSourceObserver,
+ IMediaPlayerVideoFrameObserver,
+ IRtcEngineEventHandler,
+ IRtcEngineEx,
+ IVideoFrameObserver,
+ MediaPlayerReason,
+ MediaPlayerState,
+ RtcConnection,
+ UserOfflineReasonType,
+ VideoFrame,
+ VideoSourceType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraTextInput,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ token2: string;
+ uid2: number;
+ url: string;
+ open: boolean;
+}
+
+export default class SendMultiVideoStream
+ extends BaseComponent<{}, State>
+ implements
+ IRtcEngineEventHandler,
+ IMediaPlayerSourceObserver,
+ IAudioFrameObserver,
+ IVideoFrameObserver,
+ IAudioPcmFrameSink,
+ IMediaPlayerVideoFrameObserver
+{
+ // @ts-ignore
+ protected engine?: IRtcEngineEx;
+ protected player?: IMediaPlayer;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ token2: '',
+ uid2: 0,
+ url: 'https://agora-adc-artifacts.oss-cn-beijing.aliyuncs.com/video/meta_live_mpk.mov',
+ open: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine() as IRtcEngineEx;
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+ // this.engine.getMediaEngine().registerAudioFrameObserver(this);
+ // this.engine.getMediaEngine().registerVideoFrameObserver(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: createMediaPlayer
+ */
+ createMediaPlayer = () => {
+ const { url } = this.state;
+ if (!url) {
+ this.error('url is invalid');
+ }
+
+ this.player = this.engine?.createMediaPlayer();
+ // this.player?.registerAudioFrameObserver(this);
+ // this.player?.registerVideoFrameObserver(this);
+ this.player?.registerPlayerSourceObserver(this);
+ this.player?.open(url, 0);
+ };
+
+ /**
+ * Step 3-2: publishMediaPlayerTrack
+ */
+ publishMediaPlayerTrack = () => {
+ const { channelId, token2, uid2 } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid2 <= 0) {
+ this.error('uid2 is invalid');
+ return;
+ }
+
+ // publish media player stream
+ this.engine?.joinChannelEx(
+ token2,
+ { channelId, localUid: uid2 },
+ {
+ autoSubscribeAudio: false,
+ autoSubscribeVideo: false,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishMediaPlayerAudioTrack: true,
+ publishMediaPlayerVideoTrack: true,
+ publishMediaPlayerId: this.player?.getMediaPlayerId(),
+ }
+ );
+ };
+
+ /**
+ * Step 3-3: destroyMediaPlayer
+ */
+ destroyMediaPlayer = () => {
+ if (!this.player) {
+ return;
+ }
+
+ // this.player?.unregisterAudioFrameObserver(this);
+ // this.player?.unregisterVideoFrameObserver(this);
+ this.engine?.destroyMediaPlayer(this.player);
+ this.setState({ open: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.destroyMediaPlayer();
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ // this.engine?.getMediaEngine().unregisterAudioFrameObserver(this);
+ // this.engine?.getMediaEngine().unregisterVideoFrameObserver(this);
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2) return;
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2 || remoteUid === uid2) return;
+ super.onUserJoined(connection, remoteUid, elapsed);
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ const { uid2 } = this.state;
+ if (connection.localUid === uid2 || remoteUid === uid2) return;
+ super.onUserOffline(connection, remoteUid, reason);
+ }
+
+ onPlayerSourceStateChanged(state: MediaPlayerState, ec: MediaPlayerReason) {
+ this.info('onPlayerSourceStateChanged', 'state', state, 'ec', ec);
+ switch (state) {
+ case MediaPlayerState.PlayerStateIdle:
+ break;
+ case MediaPlayerState.PlayerStateOpening:
+ break;
+ case MediaPlayerState.PlayerStateOpenCompleted:
+ this.setState({ open: true });
+ // Auto play on this case
+ this.player?.play();
+ break;
+ case MediaPlayerState.PlayerStatePlaying:
+ break;
+ case MediaPlayerState.PlayerStatePaused:
+ break;
+ case MediaPlayerState.PlayerStatePlaybackCompleted:
+ break;
+ case MediaPlayerState.PlayerStatePlaybackAllLoopsCompleted:
+ break;
+ case MediaPlayerState.PlayerStateStopped:
+ break;
+ case MediaPlayerState.PlayerStatePausingInternal:
+ break;
+ case MediaPlayerState.PlayerStateStoppingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSeekingInternal:
+ break;
+ case MediaPlayerState.PlayerStateGettingInternal:
+ break;
+ case MediaPlayerState.PlayerStateNoneInternal:
+ break;
+ case MediaPlayerState.PlayerStateDoNothingInternal:
+ break;
+ case MediaPlayerState.PlayerStateSetTrackInternal:
+ break;
+ case MediaPlayerState.PlayerStateFailed:
+ break;
+ }
+ }
+
+ onCompleted() {
+ this.info('onCompleted');
+ // Auto replay on this case
+ this.player?.seek(0);
+ this.player?.play();
+ }
+
+ onRecordAudioFrame(channelId: string, audioFrame: AudioFrame): boolean {
+ this.info('onRecordAudioFrame', channelId, audioFrame);
+ return true;
+ }
+
+ onCaptureVideoFrame(
+ sourceType: VideoSourceType,
+ videoFrame: VideoFrame
+ ): boolean {
+ this.info('onCaptureVideoFrame', sourceType, videoFrame);
+ return true;
+ }
+
+ onMediaPlayerVideoFrame(
+ videoFrame: VideoFrame,
+ mediaPlayerId: number
+ ): boolean {
+ this.info('onMediaPlayerVideoFrame', videoFrame, mediaPlayerId);
+ return true;
+ }
+
+ onFrame(frame: AudioPcmFrame | VideoFrame) {
+ this.info('onFrame', frame);
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { uid2, url } = this.state;
+ return (
+ <>
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ uid2: text === '' ? this.createState().uid2 : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`uid2 (must > 0)`}
+ value={uid2 > 0 ? uid2.toString() : ''}
+ />
+ {
+ this.setState({ url: text });
+ }}
+ placeholder={`url`}
+ value={url}
+ />
+ >
+ );
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const { open } = this.state;
+ return (
+ <>
+ {super.renderUsers()}
+ {open ? (
+
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { open } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/SpatialAudio/SpatialAudio.tsx b/examples/expo/app/examples/advanced/SpatialAudio/SpatialAudio.tsx
new file mode 100644
index 000000000..9cfd7c4e3
--- /dev/null
+++ b/examples/expo/app/examples/advanced/SpatialAudio/SpatialAudio.tsx
@@ -0,0 +1,281 @@
+import React, { ReactElement } from 'react';
+import {
+ AudioScenarioType,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+ AgoraSwitch,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { arrayToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ targetUid: number;
+ speaker_azimuth: number;
+ speaker_elevation: number;
+ speaker_distance: number;
+ speaker_orientation: number;
+ enable_blur: boolean;
+ enable_air_absorb: boolean;
+ enableSpatialAudio: boolean;
+}
+
+export default class SpatialAudio
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ enableSpatialAudio: false,
+ targetUid: 0,
+ speaker_azimuth: 0,
+ speaker_elevation: 0,
+ speaker_distance: 1,
+ speaker_orientation: 0,
+ enable_blur: false,
+ enable_air_absorb: true,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ audioScenario: AudioScenarioType.AudioScenarioGameStreaming,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ this.engine.setParameters(
+ JSON.stringify({ 'rtc.audio.force_bluetooth_a2dp': true })
+ );
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: enableSpatialAudio
+ */
+ enableSpatialAudio = () => {
+ this.engine?.enableSpatialAudio(true);
+ this.setState({ enableSpatialAudio: true });
+ };
+
+ /**
+ * Step 3-2: setRemoteUserSpatialAudioParams
+ */
+ setRemoteUserSpatialAudioParams = () => {
+ const {
+ targetUid,
+ speaker_azimuth,
+ speaker_elevation,
+ speaker_distance,
+ speaker_orientation,
+ enable_blur,
+ enable_air_absorb,
+ } = this.state;
+
+ this.engine?.setRemoteUserSpatialAudioParams(targetUid, {
+ speaker_azimuth,
+ speaker_elevation,
+ speaker_distance,
+ speaker_orientation,
+ enable_blur,
+ enable_air_absorb,
+ });
+ };
+
+ /**
+ * Step 3-3: disableSpatialAudio
+ */
+ disableSpatialAudio = () => {
+ this.engine?.enableSpatialAudio(false);
+ this.setState({ enableSpatialAudio: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ remoteUsers,
+ targetUid,
+ speaker_azimuth,
+ speaker_elevation,
+ speaker_distance,
+ speaker_orientation,
+ enable_blur,
+ enable_air_absorb,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ targetUid: value });
+ }}
+ />
+
+ {
+ this.setState({ speaker_azimuth: value });
+ }}
+ />
+
+ {
+ this.setState({ speaker_elevation: value });
+ }}
+ />
+
+ {
+ this.setState({ speaker_distance: value });
+ }}
+ />
+
+ {
+ this.setState({ speaker_orientation: value });
+ }}
+ />
+
+ {
+ this.setState({
+ enable_blur: value,
+ });
+ }}
+ />
+
+ {
+ this.setState({
+ enable_air_absorb: value,
+ });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess, enableSpatialAudio } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/StreamMessage/StreamMessage.tsx b/examples/expo/app/examples/advanced/StreamMessage/StreamMessage.tsx
new file mode 100644
index 000000000..983bd5c68
--- /dev/null
+++ b/examples/expo/app/examples/advanced/StreamMessage/StreamMessage.tsx
@@ -0,0 +1,255 @@
+import { Buffer } from 'buffer';
+
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ RtcConnection,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraSwitch,
+ AgoraText,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ syncWithAudio: boolean;
+ ordered: boolean;
+ streamId?: number;
+ data: string;
+}
+
+export default class StreamMessage
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ syncWithAudio: false,
+ ordered: false,
+ streamId: undefined,
+ data: '',
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: createDataStream
+ */
+ createDataStream = () => {
+ const { syncWithAudio, ordered, streamId } = this.state;
+ if (streamId === undefined) {
+ this.setState({
+ streamId: this.engine?.createDataStream({
+ syncWithAudio,
+ ordered,
+ }),
+ });
+ }
+ };
+
+ /**
+ * Step 3-2: sendStreamMessage
+ */
+ sendStreamMessage = () => {
+ const { streamId, data } = this.state;
+ if (!data) {
+ this.error('data is invalid');
+ return;
+ }
+
+ const buffer = Buffer.from(data);
+ this.engine?.sendStreamMessage(streamId!, buffer, buffer.length);
+ this.setState({ data: '' });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onStreamMessage(
+ connection: RtcConnection,
+ remoteUid: number,
+ streamId: number,
+ data: Uint8Array,
+ length: number,
+ sentTs: number
+ ) {
+ this.info(
+ 'onStreamMessage',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'streamId',
+ streamId,
+ 'data',
+ data,
+ 'length',
+ length,
+ 'sentTs',
+ sentTs
+ );
+ this.alert(
+ `Receive from uid:${remoteUid}`,
+ `StreamId ${streamId}: ${data.toString()}`
+ );
+ }
+
+ onStreamMessageError(
+ connection: RtcConnection,
+ remoteUid: number,
+ streamId: number,
+ code: number,
+ missed: number,
+ cached: number
+ ) {
+ this.error(
+ 'onStreamMessageError',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'streamId',
+ streamId,
+ 'code',
+ code,
+ 'missed',
+ missed,
+ 'cached',
+ cached
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { syncWithAudio, ordered, streamId, data } = this.state;
+ return (
+ <>
+ {
+ this.setState({ syncWithAudio: value });
+ }}
+ />
+
+ {
+ this.setState({ ordered: value });
+ }}
+ />
+
+ {`streamId: ${streamId}`}
+
+ {
+ this.setState({ data: text });
+ }}
+ placeholder={`data`}
+ value={data}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess, streamId } = this.state;
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/TakeSnapshot/TakeSnapshot.tsx b/examples/expo/app/examples/advanced/TakeSnapshot/TakeSnapshot.tsx
new file mode 100644
index 000000000..c53c8694d
--- /dev/null
+++ b/examples/expo/app/examples/advanced/TakeSnapshot/TakeSnapshot.tsx
@@ -0,0 +1,218 @@
+import React, { ReactElement } from 'react';
+import { Platform } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ ErrorCodeType,
+ IRtcEngineEventHandler,
+ RtcConnection,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+import RNFS from 'react-native-fs';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraImage,
+ AgoraStyle,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { arrayToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ targetUid: number;
+ filePath: string;
+ takeSnapshot: boolean;
+}
+
+export default class TakeSnapshot
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ _timestamp: number = 0;
+
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ targetUid: 0,
+ filePath: `${
+ Platform.OS === 'android'
+ ? RNFS.ExternalCachesDirectoryPath
+ : RNFS.DocumentDirectoryPath
+ }`,
+ takeSnapshot: false,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3: takeSnapshot
+ */
+ takeSnapshot = () => {
+ const { targetUid, filePath } = this.state;
+ if (!filePath) {
+ this.error('filePath is invalid');
+ return;
+ }
+
+ this._timestamp = new Date().getTime();
+ this.engine?.takeSnapshot(
+ targetUid,
+ `${filePath}/${targetUid}-${this._timestamp}.jpg`
+ );
+ this.setState({ takeSnapshot: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onSnapshotTaken(
+ connection: RtcConnection,
+ uid: number,
+ filePath: string,
+ width: number,
+ height: number,
+ errCode: number
+ ) {
+ this.info(
+ 'onSnapshotTaken',
+ 'connection',
+ connection,
+ 'uid',
+ uid,
+ 'filePath',
+ filePath,
+ 'width',
+ width,
+ 'height',
+ height,
+ 'errCode',
+ errCode
+ );
+ const { targetUid, filePath: path } = this.state;
+ if (filePath === `${path}/${targetUid}-${this._timestamp}.jpg`) {
+ this.setState({ takeSnapshot: errCode === ErrorCodeType.ErrOk });
+ }
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { remoteUsers, targetUid, filePath, takeSnapshot } = this.state;
+ return (
+ <>
+ {
+ this.setState({ targetUid: value, takeSnapshot: false });
+ }}
+ />
+ {takeSnapshot ? (
+ <>
+
+
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/VideoEncoderConfiguration/VideoEncoderConfiguration.tsx b/examples/expo/app/examples/advanced/VideoEncoderConfiguration/VideoEncoderConfiguration.tsx
new file mode 100644
index 000000000..518f511c0
--- /dev/null
+++ b/examples/expo/app/examples/advanced/VideoEncoderConfiguration/VideoEncoderConfiguration.tsx
@@ -0,0 +1,281 @@
+import React, { ReactElement } from 'react';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ DegradationPreference,
+ IRtcEngineEventHandler,
+ OrientationMode,
+ VideoCodecType,
+ VideoMirrorModeType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraTextInput,
+ AgoraView,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ codecType: VideoCodecType;
+ width: number;
+ height: number;
+ frameRate: number;
+ bitrate: number;
+ minBitrate: number;
+ orientationMode: OrientationMode;
+ degradationPreference: DegradationPreference;
+ mirrorMode: VideoMirrorModeType;
+}
+
+export default class VideoEncoderConfiguration
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ codecType: VideoCodecType.VideoCodecH264,
+ width: 640,
+ height: 360,
+ frameRate: 15,
+ bitrate: 0,
+ minBitrate: -1,
+ orientationMode: OrientationMode.OrientationModeAdaptive,
+ degradationPreference: DegradationPreference.MaintainQuality,
+ mirrorMode: VideoMirrorModeType.VideoMirrorModeDisabled,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // This case works if startPreview without joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3: setVideoEncoderConfiguration
+ */
+ setVideoEncoderConfiguration = () => {
+ const {
+ codecType,
+ width,
+ height,
+ frameRate,
+ bitrate,
+ minBitrate,
+ orientationMode,
+ degradationPreference,
+ mirrorMode,
+ } = this.state;
+ this.engine?.setVideoEncoderConfiguration({
+ codecType,
+ dimensions: {
+ width: width,
+ height: height,
+ },
+ frameRate,
+ bitrate,
+ minBitrate,
+ orientationMode,
+ degradationPreference,
+ mirrorMode,
+ });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { codecType, orientationMode, degradationPreference, mirrorMode } =
+ this.state;
+ return (
+ <>
+ {
+ this.setState({ codecType: value });
+ }}
+ />
+
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ width: text === '' ? this.createState().width : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`width (defaults: ${this.createState().width})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ height: text === '' ? this.createState().height : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`height (defaults: ${this.createState().height})`}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ frameRate: text === '' ? this.createState().frameRate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`frameRate (defaults: ${this.createState().frameRate})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ bitrate: text === '' ? this.createState().bitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`bitrate (defaults: ${this.createState().bitrate})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ this.setState({
+ minBitrate: text === '' ? this.createState().minBitrate : +text,
+ });
+ }}
+ numberKeyboard={true}
+ placeholder={`minBitrate (defaults: ${
+ this.createState().minBitrate
+ })`}
+ />
+ {
+ this.setState({ orientationMode: value });
+ }}
+ />
+
+ {
+ this.setState({ degradationPreference: value });
+ }}
+ />
+
+ {
+ this.setState({ mirrorMode: value });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/VirtualBackground/VirtualBackground.tsx b/examples/expo/app/examples/advanced/VirtualBackground/VirtualBackground.tsx
new file mode 100644
index 000000000..77c69fcc0
--- /dev/null
+++ b/examples/expo/app/examples/advanced/VirtualBackground/VirtualBackground.tsx
@@ -0,0 +1,240 @@
+import React, { ReactElement } from 'react';
+import {
+ BackgroundBlurDegree,
+ BackgroundSourceType,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+import ColorPicker, { Panel1 } from 'reanimated-color-picker';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import {
+ enumToItems,
+ getAbsolutePath,
+ getResourcePath,
+} from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ background_source_type: BackgroundSourceType;
+ color: string;
+ source: string;
+ blur_degree: BackgroundBlurDegree;
+ enableVirtualBackground?: boolean;
+}
+
+export default class VirtualBackground
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ startPreview: false,
+ background_source_type: BackgroundSourceType.BackgroundColor,
+ color: '#ffffff',
+ source: getResourcePath('agora-logo.png'),
+ blur_degree: BackgroundBlurDegree.BlurDegreeMedium,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // This case works if startPreview without joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1: enableVirtualBackground
+ */
+ enableVirtualBackground = async () => {
+ const { background_source_type, color, source, blur_degree } = this.state;
+ if (
+ background_source_type === BackgroundSourceType.BackgroundImg &&
+ !source
+ ) {
+ this.error('source is invalid');
+ return;
+ }
+
+ this.engine?.enableVirtualBackground(
+ true,
+ {
+ background_source_type,
+ color: +color.replace('#', '0x'),
+ source: await getAbsolutePath(source),
+ blur_degree,
+ },
+ {}
+ );
+ this.setState({ enableVirtualBackground: true });
+ };
+
+ /**
+ * Step 3-2: disableVirtualBackground
+ */
+ disableVirtualBackground = () => {
+ this.engine?.enableVirtualBackground(false, {}, {});
+ this.setState({ enableVirtualBackground: false });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onSelectColor = ({ hex }) => {
+ this.setState({
+ color: hex,
+ });
+ };
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { background_source_type, color, source, blur_degree } = this.state;
+ return (
+ <>
+ {
+ this.setState({ background_source_type: value });
+ }}
+ />
+ {background_source_type === BackgroundSourceType.BackgroundColor ? (
+
+
+
+ ) : undefined}
+ {
+ this.setState({
+ source: text,
+ });
+ }}
+ placeholder={'source'}
+ value={source}
+ />
+ {
+ this.setState({ blur_degree: value });
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess, enableVirtualBackground } =
+ this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/advanced/VoiceChanger/VoiceChanger.tsx b/examples/expo/app/examples/advanced/VoiceChanger/VoiceChanger.tsx
new file mode 100644
index 000000000..95dd3b19c
--- /dev/null
+++ b/examples/expo/app/examples/advanced/VoiceChanger/VoiceChanger.tsx
@@ -0,0 +1,440 @@
+import React, { ReactElement } from 'react';
+import {
+ AudioEffectPreset,
+ AudioEqualizationBandFrequency,
+ AudioReverbType,
+ ChannelProfileType,
+ ClientRoleType,
+ IRtcEngineEventHandler,
+ VoiceBeautifierPreset,
+ VoiceConversionPreset,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+} from '../../../../src/components/ui';
+import {
+ AudioEffectPresetParam1Limit,
+ AudioEffectPresetParam2Limit,
+ AudioReverbTypeValueLimit,
+ VoiceBeautifierPresetParam1Limit,
+ VoiceBeautifierPresetParam2Limit,
+} from '../../../../src/config/VoiceChangerConfig';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ voiceBeautifierPreset: VoiceBeautifierPreset;
+ audioEffectPreset: AudioEffectPreset;
+ param1: number;
+ param2: number;
+ reverbKey: AudioReverbType;
+ value: number;
+ bandFrequency: AudioEqualizationBandFrequency;
+ bandGain: number;
+ pitch: number;
+ voiceConversionPreset: VoiceConversionPreset;
+}
+
+export default class VoiceChanger
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ voiceBeautifierPreset: VoiceBeautifierPreset.VoiceBeautifierOff,
+ audioEffectPreset: AudioEffectPreset.AudioEffectOff,
+ param1: 0,
+ param2: 0,
+ reverbKey: AudioReverbType.AudioReverbDryLevel,
+ value: 0,
+ bandFrequency: AudioEqualizationBandFrequency.AudioEqualizationBand31,
+ bandGain: 0,
+ pitch: 1.0,
+ voiceConversionPreset: VoiceConversionPreset.VoiceConversionOff,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1 (Optional): setVoiceBeautifierPreset
+ */
+ setVoiceBeautifierPreset = () => {
+ const { voiceBeautifierPreset } = this.state;
+ this.engine?.setVoiceBeautifierPreset(voiceBeautifierPreset);
+ };
+
+ /**
+ * Step 3-2 (Optional): setVoiceBeautifierParameters
+ */
+ setVoiceBeautifierParameters = () => {
+ const { voiceBeautifierPreset, param1, param2 } = this.state;
+ this.engine?.setVoiceBeautifierParameters(
+ voiceBeautifierPreset,
+ param1,
+ param2
+ );
+ };
+
+ /**
+ * Step 3-3 (Optional): setAudioEffectPreset
+ */
+ setAudioEffectPreset = () => {
+ const { audioEffectPreset } = this.state;
+ this.engine?.setAudioEffectPreset(audioEffectPreset);
+ };
+
+ /**
+ * Step 3-4 (Optional): setAudioEffectParameters
+ */
+ setAudioEffectParameters = () => {
+ const { audioEffectPreset, param1, param2 } = this.state;
+ this.engine?.setAudioEffectParameters(audioEffectPreset, param1, param2);
+ };
+
+ /**
+ * Step 3-5 (Optional): setLocalVoiceReverb
+ */
+ setLocalVoiceReverb = () => {
+ const { reverbKey, value } = this.state;
+ this.engine?.setLocalVoiceReverb(reverbKey, value);
+ };
+
+ /**
+ * Step 3-6 (Optional): setLocalVoiceEqualization
+ */
+ setLocalVoiceEqualization = () => {
+ const { bandFrequency, bandGain } = this.state;
+ this.engine?.setLocalVoiceEqualization(bandFrequency, bandGain);
+ };
+
+ /**
+ * Step 3-7 (Optional): setLocalVoicePitch
+ */
+ setLocalVoicePitch = () => {
+ const { pitch } = this.state;
+ this.engine?.setLocalVoicePitch(pitch);
+ };
+
+ /**
+ * Step 3-8 (Optional): setVoiceConversionPreset
+ */
+ setVoiceConversionPreset = () => {
+ const { voiceConversionPreset } = this.state;
+ this.engine?.setVoiceConversionPreset(voiceConversionPreset);
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {this._renderVoiceBeautifierPreset()}
+
+ {this._renderAudioEffectPreset()}
+
+ {this._renderAudioReverbType()}
+
+ {this._renderAudioEqualizationBandFrequency()}
+
+ {this._renderLocalVoicePitch()}
+
+ {this._renderVoiceConversionPreset()}
+ >
+ );
+ }
+
+ _renderVoiceBeautifierPreset = () => {
+ const { voiceBeautifierPreset, param1, param2 } = this.state;
+ const limit1 = VoiceBeautifierPresetParam1Limit.get(voiceBeautifierPreset);
+ const limit2 = VoiceBeautifierPresetParam2Limit.get(voiceBeautifierPreset);
+ return (
+ <>
+ {
+ this.setState({ voiceBeautifierPreset: value });
+ }}
+ />
+
+ {limit1 !== undefined ? (
+ {
+ this.setState({ param1: value });
+ }}
+ />
+ ) : undefined}
+ {limit2 !== undefined ? (
+ {
+ this.setState({ param2: value });
+ }}
+ />
+ ) : undefined}
+ {limit1 !== undefined && limit2 !== undefined ? (
+
+ ) : undefined}
+ >
+ );
+ };
+
+ _renderAudioEffectPreset = () => {
+ const { audioEffectPreset, param1, param2 } = this.state;
+ const limit1 = AudioEffectPresetParam1Limit.get(audioEffectPreset);
+ const limit2 = AudioEffectPresetParam2Limit.get(audioEffectPreset);
+ return (
+ <>
+ {
+ this.setState({ audioEffectPreset: value });
+ }}
+ />
+
+ {limit1 !== undefined ? (
+ {
+ this.setState({ param1: value });
+ }}
+ />
+ ) : undefined}
+ {limit2 !== undefined ? (
+ {
+ this.setState({ param2: value });
+ }}
+ />
+ ) : undefined}
+ {limit1 !== undefined && limit2 !== undefined ? (
+
+ ) : undefined}
+ >
+ );
+ };
+
+ _renderAudioReverbType = () => {
+ const { reverbKey, value } = this.state;
+ const limit = AudioReverbTypeValueLimit.get(reverbKey);
+ return (
+ <>
+ {
+ this.setState({ reverbKey: v });
+ }}
+ />
+ {limit !== undefined ? (
+ {
+ this.setState({ value: v });
+ }}
+ />
+ ) : undefined}
+ {limit !== undefined ? (
+
+ ) : undefined}
+ >
+ );
+ };
+
+ _renderAudioEqualizationBandFrequency = () => {
+ const { bandFrequency, bandGain } = this.state;
+ const min = -15;
+ const max = 15;
+ return (
+ <>
+ {
+ this.setState({ bandFrequency: value });
+ }}
+ />
+ {
+ this.setState({ bandGain: value });
+ }}
+ />
+
+ >
+ );
+ };
+
+ _renderLocalVoicePitch = () => {
+ const { pitch } = this.state;
+ const min = 0.5;
+ const max = 2.0;
+ return (
+ <>
+ {
+ this.setState({ pitch: value });
+ }}
+ />
+
+ >
+ );
+ };
+
+ _renderVoiceConversionPreset = () => {
+ const { voiceConversionPreset } = this.state;
+ return (
+ <>
+ {
+ this.setState({ voiceConversionPreset: value });
+ }}
+ />
+
+ >
+ );
+ };
+}
diff --git a/examples/expo/app/examples/advanced/index.ts b/examples/expo/app/examples/advanced/index.ts
new file mode 100644
index 000000000..26311394a
--- /dev/null
+++ b/examples/expo/app/examples/advanced/index.ts
@@ -0,0 +1,158 @@
+import AudioCallRoute from './AudioCallRoute/AudioCallRoute';
+import AudioMixing from './AudioMixing/AudioMixing';
+import AudioSpectrum from './AudioSpectrum/AudioSpectrum';
+import BeautyEffect from './BeautyEffect/BeautyEffect';
+import ChannelMediaRelay from './ChannelMediaRelay/ChannelMediaRelay';
+import ContentInspect from './ContentInspect/ContentInspect';
+import DirectCdnStreaming from './DirectCdnStreaming/DirectCdnStreaming';
+import Encryption from './Encryption/Encryption';
+import Extension from './Extension/Extension';
+import JoinMultipleChannel from './JoinMultipleChannel/JoinMultipleChannel';
+import LocalSpatialAudioEngine from './LocalSpatialAudioEngine/LocalSpatialAudioEngine';
+import LocalVideoTranscoder from './LocalVideoTranscoder/LocalVideoTranscoder';
+import MediaPlayer from './MediaPlayer/MediaPlayer';
+import MediaRecorder from './MediaRecorder/MediaRecorder';
+import MusicContentCenter from './MusicContentCenter/MusicContentCenter';
+import PictureInPicture from './PictureInPicture/PictureInPicture';
+import PlayEffect from './PlayEffect/PlayEffect';
+import ProcessVideoRawData from './ProcessVideoRawData/ProcessVideoRawData';
+import PushVideoFrame from './PushVideoFrame/PushVideoFrame';
+import RTMPStreaming from './RTMPStreaming/RTMPStreaming';
+import RhythmPlayer from './RhythmPlayer/RhythmPlayer';
+import ScreenShare from './ScreenShare/ScreenShare';
+import SendMetadata from './SendMetadata/SendMetadata';
+import SendMultiVideoStream from './SendMultiVideoStream/SendMultiVideoStream';
+import SpatialAudio from './SpatialAudio/SpatialAudio';
+import StreamMessage from './StreamMessage/StreamMessage';
+import TakeSnapshot from './TakeSnapshot/TakeSnapshot';
+import VideoEncoderConfiguration from './VideoEncoderConfiguration/VideoEncoderConfiguration';
+import VirtualBackground from './VirtualBackground/VirtualBackground';
+import VoiceChanger from './VoiceChanger/VoiceChanger';
+
+const Advanced = {
+ title: 'advanced',
+ data: [
+ {
+ name: 'PictureInPicture',
+ component: PictureInPicture,
+ },
+ {
+ name: 'AudioCallRoute',
+ component: AudioCallRoute,
+ },
+ {
+ name: 'AudioMixing',
+ component: AudioMixing,
+ },
+ {
+ name: 'AudioSpectrum',
+ component: AudioSpectrum,
+ },
+ {
+ name: 'BeautyEffect',
+ component: BeautyEffect,
+ },
+ {
+ name: 'ChannelMediaRelay',
+ component: ChannelMediaRelay,
+ },
+ {
+ name: 'ContentInspect',
+ component: ContentInspect,
+ },
+ {
+ name: 'DirectCdnStreaming',
+ component: DirectCdnStreaming,
+ },
+ {
+ name: 'Encryption',
+ component: Encryption,
+ },
+ {
+ name: 'Extension',
+ component: Extension,
+ },
+ {
+ name: 'JoinMultipleChannel',
+ component: JoinMultipleChannel,
+ },
+ {
+ name: 'LocalSpatialAudioEngine',
+ component: LocalSpatialAudioEngine,
+ },
+ {
+ name: 'LocalVideoTranscoder',
+ component: LocalVideoTranscoder,
+ },
+ {
+ name: 'MediaPlayer',
+ component: MediaPlayer,
+ },
+ {
+ name: 'MediaRecorder',
+ component: MediaRecorder,
+ },
+ {
+ name: 'MusicContentCenter',
+ component: MusicContentCenter,
+ },
+ {
+ name: 'PlayEffect',
+ component: PlayEffect,
+ },
+ {
+ name: 'ProcessVideoRawData',
+ component: ProcessVideoRawData,
+ },
+ {
+ name: 'PushVideoFrame',
+ component: PushVideoFrame,
+ },
+ {
+ name: 'RhythmPlayer',
+ component: RhythmPlayer,
+ },
+ {
+ name: 'RTMPStreaming',
+ component: RTMPStreaming,
+ },
+ {
+ name: 'ScreenShare',
+ component: ScreenShare,
+ },
+ {
+ name: 'SendMetadata',
+ component: SendMetadata,
+ },
+ {
+ name: 'SendMultiVideoStream',
+ component: SendMultiVideoStream,
+ },
+ {
+ name: 'SpatialAudio',
+ component: SpatialAudio,
+ },
+ {
+ name: 'StreamMessage',
+ component: StreamMessage,
+ },
+ {
+ name: 'TakeSnapshot',
+ component: TakeSnapshot,
+ },
+ {
+ name: 'VideoEncoderConfiguration',
+ component: VideoEncoderConfiguration,
+ },
+ {
+ name: 'VirtualBackground',
+ component: VirtualBackground,
+ },
+ {
+ name: 'VoiceChanger',
+ component: VoiceChanger,
+ },
+ ],
+};
+
+export default Advanced;
diff --git a/examples/expo/app/examples/basic/JoinChannelAudio/JoinChannelAudio.tsx b/examples/expo/app/examples/basic/JoinChannelAudio/JoinChannelAudio.tsx
new file mode 100644
index 000000000..26b3de5fd
--- /dev/null
+++ b/examples/expo/app/examples/basic/JoinChannelAudio/JoinChannelAudio.tsx
@@ -0,0 +1,569 @@
+import { Text } from '@rneui/base';
+import React, { ReactElement } from 'react';
+import { View } from 'react-native';
+import {
+ AudioVolumeInfo,
+ ChannelProfileType,
+ ClientRoleType,
+ EarMonitoringFilterType,
+ ErrorCodeType,
+ IRtcEngineEventHandler,
+ LocalAudioStats,
+ LocalAudioStreamReason,
+ LocalAudioStreamState,
+ MediaDeviceType,
+ QualityType,
+ RemoteAudioStats,
+ RtcConnection,
+ RtcStats,
+ UserOfflineReasonType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraCard,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraList,
+ AgoraSlider,
+ AgoraStyle,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ enableLocalAudio: boolean;
+ muteLocalAudioStream: boolean;
+ enableSpeakerphone: boolean;
+ recordingSignalVolume: number;
+ playbackSignalVolume: number;
+ localVolume?: number;
+ lastmileDelay?: number;
+ audioSentBitrate?: number;
+ cpuAppUsage?: number;
+ cpuTotalUsage?: number;
+ txPacketLossRate?: number;
+ remoteUserStatsList: Map<
+ number,
+ { volume: number; remoteAudioStats: RemoteAudioStats }
+ >;
+ includeAudioFilters: EarMonitoringFilterType;
+ enableInEarMonitoring: boolean;
+ inEarMonitoringVolume: number;
+}
+
+export default class JoinChannelAudio
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ enableLocalAudio: true,
+ muteLocalAudioStream: false,
+ enableSpeakerphone: true,
+ recordingSignalVolume: 100,
+ playbackSignalVolume: 100,
+ includeAudioFilters: EarMonitoringFilterType.EarMonitoringFilterNone,
+ enableInEarMonitoring: false,
+ inEarMonitoringVolume: 100,
+ remoteUserStatsList: new Map(),
+ localVolume: 0,
+ lastmileDelay: 0,
+ audioSentBitrate: 0,
+ cpuAppUsage: 0,
+ cpuTotalUsage: 0,
+ txPacketLossRate: 0,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine.enableAudio();
+ this.engine.enableAudioVolumeIndication(200, 3, true);
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3-1-1 (Optional): enableLocalAudio
+ */
+ enableLocalAudio = () => {
+ this.engine?.enableLocalAudio(true);
+ this.setState({ enableLocalAudio: true });
+ };
+
+ /**
+ * Step 3-1-2 (Optional): disableLocalAudio
+ */
+ disableLocalAudio = () => {
+ this.engine?.enableLocalAudio(false);
+ this.setState({ enableLocalAudio: false });
+ };
+
+ /**
+ * Step 3-2-1 (Optional): muteLocalAudioStream
+ */
+ muteLocalAudioStream = () => {
+ this.engine?.muteLocalAudioStream(true);
+ this.setState({ muteLocalAudioStream: true });
+ };
+
+ /**
+ * Step 3-2-2 (Optional): unmuteLocalAudioStream
+ */
+ unmuteLocalAudioStream = () => {
+ this.engine?.muteLocalAudioStream(false);
+ this.setState({ muteLocalAudioStream: false });
+ };
+
+ /**
+ * Step 3-3-1 (Optional): enableSpeakerphone
+ */
+ enableSpeakerphone = () => {
+ this.engine?.setEnableSpeakerphone(true);
+ this.setState({ enableSpeakerphone: true });
+ };
+
+ /**
+ * Step 3-3-2 (Optional): disableSpeakerphone
+ */
+ disableSpeakerphone = () => {
+ this.engine?.setEnableSpeakerphone(false);
+ this.setState({ enableSpeakerphone: false });
+ };
+
+ /**
+ * Step 3-4 (Optional): adjustRecordingSignalVolume
+ */
+ adjustRecordingSignalVolume = () => {
+ const { recordingSignalVolume } = this.state;
+ this.engine?.adjustRecordingSignalVolume(recordingSignalVolume);
+ };
+
+ /**
+ * Step 3-5 (Optional): adjustPlaybackSignalVolume
+ */
+ adjustPlaybackSignalVolume = () => {
+ const { playbackSignalVolume } = this.state;
+ this.engine?.adjustPlaybackSignalVolume(playbackSignalVolume);
+ };
+
+ /**
+ * Step 3-6-1 (Optional): enableInEarMonitoring
+ */
+ enableInEarMonitoring = () => {
+ const { includeAudioFilters } = this.state;
+ if (
+ this.engine?.enableInEarMonitoring(true, includeAudioFilters) ===
+ ErrorCodeType.ErrOk
+ ) {
+ this.setState({ enableInEarMonitoring: true });
+ }
+ };
+
+ /**
+ * Step 3-6-2 (Optional): setInEarMonitoringVolume
+ */
+ setInEarMonitoringVolume = () => {
+ const { inEarMonitoringVolume } = this.state;
+ this.engine?.setInEarMonitoringVolume(inEarMonitoringVolume);
+ };
+
+ /**
+ * Step 3-6-3 (Optional): disableInEarMonitoring
+ */
+ disableInEarMonitoring = () => {
+ const { includeAudioFilters } = this.state;
+ if (
+ this.engine?.enableInEarMonitoring(false, includeAudioFilters) ===
+ ErrorCodeType.ErrOk
+ ) {
+ this.setState({ enableInEarMonitoring: false });
+ }
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onError(err: ErrorCodeType, msg: string) {
+ super.onError(err, msg);
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ super.onLeaveChannel(connection, stats);
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ super.onUserJoined(connection, remoteUid, elapsed);
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ super.onUserOffline(connection, remoteUid, reason);
+ }
+
+ onAudioDeviceStateChanged(
+ deviceId: string,
+ deviceType: number,
+ deviceState: number
+ ) {
+ this.info(
+ 'onAudioDeviceStateChanged',
+ 'deviceId',
+ deviceId,
+ 'deviceType',
+ deviceType,
+ 'deviceState',
+ deviceState
+ );
+ }
+
+ onAudioDeviceVolumeChanged(
+ deviceType: MediaDeviceType,
+ volume: number,
+ muted: boolean
+ ) {
+ this.info(
+ 'onAudioDeviceVolumeChanged',
+ 'deviceType',
+ deviceType,
+ 'volume',
+ volume,
+ 'muted',
+ muted
+ );
+ }
+
+ onLocalAudioStateChanged(
+ connection: RtcConnection,
+ state: LocalAudioStreamState,
+ error: LocalAudioStreamReason
+ ) {
+ this.info(
+ 'onLocalAudioStateChanged',
+ 'connection',
+ connection,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ }
+
+ onAudioRoutingChanged(routing: number) {
+ this.info('onAudioRoutingChanged', 'routing', routing);
+ }
+
+ onAudioVolumeIndication(
+ connection: RtcConnection,
+ speakers: AudioVolumeInfo[],
+ speakerNumber: number,
+ totalVolume: number
+ ): void {
+ speakers.map((speaker) => {
+ if (speaker.uid === 0) {
+ this.setState({ localVolume: speaker.volume });
+ } else {
+ if (!speaker.uid) return;
+ const { remoteUserStatsList } = this.state;
+ remoteUserStatsList.set(speaker.uid, {
+ volume: speaker.volume!,
+ remoteAudioStats:
+ remoteUserStatsList.get(speaker.uid)?.remoteAudioStats || {},
+ });
+ }
+ });
+ }
+
+ onRtcStats(connection: RtcConnection, stats: RtcStats): void {
+ this.setState({
+ lastmileDelay: stats.lastmileDelay,
+ cpuAppUsage: stats.cpuAppUsage,
+ cpuTotalUsage: stats.cpuTotalUsage,
+ txPacketLossRate: stats.txPacketLossRate,
+ });
+ }
+
+ onLocalAudioStats(connection: RtcConnection, stats: LocalAudioStats): void {
+ this.setState({
+ audioSentBitrate: stats.sentBitrate,
+ });
+ }
+
+ onRemoteAudioStats(connection: RtcConnection, stats: RemoteAudioStats): void {
+ const { remoteUserStatsList } = this.state;
+ if (stats.uid) {
+ remoteUserStatsList.set(stats.uid, {
+ volume: remoteUserStatsList.get(stats.uid)?.volume || 0,
+ remoteAudioStats: stats,
+ });
+ }
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ const {
+ joinChannelSuccess,
+ remoteUsers,
+ localVolume,
+ lastmileDelay,
+ audioSentBitrate,
+ cpuAppUsage,
+ cpuTotalUsage,
+ txPacketLossRate,
+ remoteUserStatsList,
+ } = this.state;
+ return (
+ <>
+ {joinChannelSuccess ? (
+ <>
+
+ <>
+ Volume: {localVolume}
+ LM Delay: {lastmileDelay}ms
+ ASend: {audioSentBitrate}kbps
+
+ CPU: {cpuAppUsage}%/{cpuTotalUsage}%
+
+ Send Loss: {txPacketLossRate}%
+ >
+
+ (
+
+ {joinChannelSuccess ? (
+
+
+ Volume: {remoteUserStatsList.get(item)?.volume}
+
+
+ ARecv:{' '}
+ {
+ remoteUserStatsList.get(item)?.remoteAudioStats
+ .receivedBitrate
+ }
+ kbps
+
+
+ ALoss:{' '}
+ {
+ remoteUserStatsList.get(item)?.remoteAudioStats
+ .audioLossRate
+ }
+ %
+
+
+ AQuality:{' '}
+ {
+ QualityType[
+ remoteUserStatsList.get(item)?.remoteAudioStats
+ .quality!
+ ]
+ }
+
+
+ ) : undefined}
+
+ )}
+ />
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const {
+ recordingSignalVolume,
+ playbackSignalVolume,
+ includeAudioFilters,
+ enableInEarMonitoring,
+ inEarMonitoringVolume,
+ } = this.state;
+ return (
+ <>
+ {
+ this.setState({ recordingSignalVolume: value });
+ }}
+ />
+
+
+ {
+ this.setState({ playbackSignalVolume: value });
+ }}
+ />
+
+
+ {
+ this.setState({ includeAudioFilters: value });
+ }}
+ />
+
+ {
+ this.setState({ inEarMonitoringVolume: value });
+ }}
+ />
+
+
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const {
+ enableLocalAudio,
+ muteLocalAudioStream,
+ enableSpeakerphone,
+ enableInEarMonitoring,
+ } = this.state;
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/basic/JoinChannelVideo/JoinChannelVideo.tsx b/examples/expo/app/examples/basic/JoinChannelVideo/JoinChannelVideo.tsx
new file mode 100644
index 000000000..c98bdd26b
--- /dev/null
+++ b/examples/expo/app/examples/basic/JoinChannelVideo/JoinChannelVideo.tsx
@@ -0,0 +1,443 @@
+import { Text } from '@rneui/base';
+import React, { ReactElement } from 'react';
+import { Platform, View } from 'react-native';
+import {
+ ChannelProfileType,
+ ClientRoleType,
+ ErrorCodeType,
+ IRtcEngineEventHandler,
+ LocalAudioStats,
+ LocalVideoStats,
+ LocalVideoStreamReason,
+ LocalVideoStreamState,
+ QualityType,
+ RemoteAudioStats,
+ RemoteVideoStats,
+ RtcConnection,
+ RtcStats,
+ RtcSurfaceView,
+ RtcTextureView,
+ UserOfflineReasonType,
+ VideoCanvas,
+ VideoSourceType,
+ VideoViewSetupMode,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseComponent,
+ BaseVideoComponentState,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraSwitch,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseVideoComponentState {
+ switchCamera: boolean;
+ renderByTextureView: boolean;
+ setupMode: VideoViewSetupMode;
+ lastmileDelay?: number;
+ videoSentBitrate?: number;
+ encodedFrameWidth?: number;
+ encodedFrameHeight?: number;
+ encoderOutputFrameRate?: number;
+ audioSentBitrate?: number;
+ cpuAppUsage?: number;
+ cpuTotalUsage?: number;
+ txPacketLossRate?: number;
+ remoteUserStatsList: Map<
+ number,
+ { remoteVideoStats: RemoteVideoStats; remoteAudioStats: RemoteAudioStats }
+ >;
+}
+
+export default class JoinChannelVideo
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: true,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ remoteUserStatsList: new Map(),
+ encodedFrameWidth: 0,
+ encodedFrameHeight: 0,
+ encoderOutputFrameRate: 0,
+ lastmileDelay: 0,
+ videoSentBitrate: 0,
+ audioSentBitrate: 0,
+ cpuAppUsage: 0,
+ cpuTotalUsage: 0,
+ txPacketLossRate: 0,
+ startPreview: false,
+ switchCamera: false,
+ renderByTextureView: false,
+ setupMode: VideoViewSetupMode.VideoViewSetupReplace,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+
+ this.engine = createAgoraRtcEngine();
+ this.engine.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+ this.engine.registerEventHandler(this);
+
+ // Need granted the microphone and camera permission
+ await askMediaAccess([
+ 'android.permission.RECORD_AUDIO',
+ 'android.permission.CAMERA',
+ ]);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ this.engine.enableVideo();
+
+ // Start preview before joinChannel
+ this.engine.startPreview();
+ this.setState({ startPreview: true });
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, uid } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ this.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3 (Optional): switchCamera
+ */
+ switchCamera = () => {
+ this.engine?.switchCamera();
+ this.setState((preState) => {
+ return { switchCamera: !preState.switchCamera };
+ });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ }
+
+ onError(err: ErrorCodeType, msg: string) {
+ super.onError(err, msg);
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ super.onLeaveChannel(connection, stats);
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ super.onUserJoined(connection, remoteUid, elapsed);
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ super.onUserOffline(connection, remoteUid, reason);
+ }
+
+ onVideoDeviceStateChanged(
+ deviceId: string,
+ deviceType: number,
+ deviceState: number
+ ) {
+ this.info(
+ 'onVideoDeviceStateChanged',
+ 'deviceId',
+ deviceId,
+ 'deviceType',
+ deviceType,
+ 'deviceState',
+ deviceState
+ );
+ }
+
+ onLocalVideoStateChanged(
+ source: VideoSourceType,
+ state: LocalVideoStreamState,
+ error: LocalVideoStreamReason
+ ) {
+ this.info(
+ 'onLocalVideoStateChanged',
+ 'source',
+ source,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ }
+
+ onRtcStats(connection: RtcConnection, stats: RtcStats): void {
+ this.setState({
+ lastmileDelay: stats.lastmileDelay,
+ cpuAppUsage: stats.cpuAppUsage,
+ cpuTotalUsage: stats.cpuTotalUsage,
+ txPacketLossRate: stats.txPacketLossRate,
+ });
+ }
+
+ onLocalVideoStats(connection: RtcConnection, stats: LocalVideoStats): void {
+ this.setState({
+ videoSentBitrate: stats.sentBitrate,
+ encodedFrameWidth: stats.encodedFrameWidth,
+ encodedFrameHeight: stats.encodedFrameHeight,
+ encoderOutputFrameRate: stats.encoderOutputFrameRate,
+ });
+ }
+
+ onLocalAudioStats(connection: RtcConnection, stats: LocalAudioStats): void {
+ this.setState({
+ audioSentBitrate: stats.sentBitrate,
+ });
+ }
+
+ onRemoteVideoStats(connection: RtcConnection, stats: RemoteVideoStats): void {
+ const { remoteUserStatsList } = this.state;
+ if (stats.uid) {
+ remoteUserStatsList.set(stats.uid, {
+ remoteVideoStats: stats,
+ remoteAudioStats:
+ remoteUserStatsList.get(stats.uid)?.remoteAudioStats || {},
+ });
+ }
+ }
+
+ onRemoteAudioStats(connection: RtcConnection, stats: RemoteAudioStats): void {
+ const { remoteUserStatsList } = this.state;
+ if (stats.uid) {
+ remoteUserStatsList.set(stats.uid, {
+ remoteVideoStats:
+ remoteUserStatsList.get(stats.uid)?.remoteVideoStats || {},
+ remoteAudioStats: stats,
+ });
+ }
+ }
+
+ protected renderUsers(): ReactElement | undefined {
+ return super.renderUsers();
+ }
+
+ protected renderVideo(user: VideoCanvas): ReactElement | undefined {
+ const {
+ renderByTextureView,
+ setupMode,
+ joinChannelSuccess,
+ encodedFrameWidth,
+ encodedFrameHeight,
+ encoderOutputFrameRate,
+ remoteUserStatsList,
+ lastmileDelay,
+ videoSentBitrate,
+ audioSentBitrate,
+ cpuAppUsage,
+ cpuTotalUsage,
+ txPacketLossRate,
+ } = this.state;
+ return (
+ <>
+ {renderByTextureView ? (
+
+ ) : (
+
+ )}
+ {joinChannelSuccess && user.sourceType === 0 && (
+
+
+ {encodedFrameWidth}x{encodedFrameHeight},{encoderOutputFrameRate}
+ fps
+
+
+ LM Delay: {lastmileDelay}ms
+
+
+ VSend: {videoSentBitrate}kbps
+
+
+ ASend: {audioSentBitrate}kbps
+
+
+ CPU: {cpuAppUsage}%/{cpuTotalUsage}%
+
+
+ Send Loss: {txPacketLossRate}%
+
+
+ )}
+ {joinChannelSuccess && user.sourceType !== 0 && user.uid && (
+
+
+ VRecv:{' '}
+ {
+ remoteUserStatsList.get(user.uid)?.remoteVideoStats
+ .receivedBitrate
+ }
+ kbps
+
+
+ ARecv:{' '}
+ {
+ remoteUserStatsList.get(user.uid)?.remoteAudioStats
+ .receivedBitrate
+ }
+ kbps
+
+
+ VLoss:{' '}
+ {
+ remoteUserStatsList.get(user.uid)?.remoteVideoStats
+ .packetLossRate
+ }
+ %
+
+
+ ALoss:{' '}
+ {
+ remoteUserStatsList.get(user.uid)?.remoteAudioStats
+ .audioLossRate
+ }
+ %
+
+
+ AQuality:{' '}
+ {
+ QualityType[
+ remoteUserStatsList.get(user.uid)?.remoteAudioStats.quality!
+ ]
+ }
+
+
+ )}
+ >
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess, renderByTextureView, setupMode } =
+ this.state;
+ return (
+ <>
+ {Platform.OS === 'android' && (
+ {
+ this.setState({ renderByTextureView: value });
+ }}
+ />
+ )}
+
+ {
+ this.setState({ setupMode: value });
+ }}
+ />
+ {setupMode === VideoViewSetupMode.VideoViewSetupAdd ? (
+ <>
+
+ {renderByTextureView ? (
+
+ ) : (
+
+ )}
+ >
+ ) : undefined}
+
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { startPreview, joinChannelSuccess } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/basic/StringUid/StringUid.tsx b/examples/expo/app/examples/basic/StringUid/StringUid.tsx
new file mode 100644
index 000000000..1ed3169a2
--- /dev/null
+++ b/examples/expo/app/examples/basic/StringUid/StringUid.tsx
@@ -0,0 +1,231 @@
+import React, { ReactElement } from 'react';
+import {
+ AreaCode,
+ ChannelProfileType,
+ ClientRoleType,
+ ErrorCodeType,
+ IRtcEngineEventHandler,
+ RtcConnection,
+ RtcStats,
+ UserOfflineReasonType,
+ createAgoraRtcEngine,
+} from 'react-native-agora';
+
+import {
+ BaseAudioComponentState,
+ BaseComponent,
+} from '../../../../src/components/BaseComponent';
+import {
+ AgoraButton,
+ AgoraDropdown,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import Config from '../../../../src/config/agora.config';
+import { enumToItems } from '../../../../src/utils';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+interface State extends BaseAudioComponentState {
+ userAccount: string;
+ isInitialized: boolean;
+ selectedAreaCode: number;
+}
+
+export default class StringUid
+ extends BaseComponent<{}, State>
+ implements IRtcEngineEventHandler
+{
+ protected createState(): State {
+ return {
+ appId: Config.appId,
+ enableVideo: false,
+ isInitialized: false,
+ channelId: Config.channelId,
+ token: Config.token,
+ uid: Config.uid,
+ joinChannelSuccess: false,
+ remoteUsers: [],
+ userAccount: '',
+ selectedAreaCode: AreaCode.AreaCodeGlob,
+ };
+ }
+
+ /**
+ * Step 1: initRtcEngine
+ */
+ protected async initRtcEngine() {
+ this.engine = createAgoraRtcEngine();
+ }
+
+ /**
+ * Step 2: joinChannel
+ */
+ protected joinChannel() {
+ const { channelId, token, userAccount } = this.state;
+ if (!channelId) {
+ this.error('channelId is invalid');
+ return;
+ }
+ if (!userAccount) {
+ this.error('userAccount is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ this.engine?.joinChannelWithUserAccount(token, channelId, userAccount, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ }
+
+ /**
+ * Step 3 (Optional): getUserInfoByUserAccount
+ */
+ getUserInfoByUserAccount = () => {
+ const { userAccount } = this.state;
+ const userInfo = this.engine?.getUserInfoByUserAccount(userAccount);
+ if (userInfo) {
+ this.debug('getUserInfoByUserAccount', 'userInfo', userInfo);
+ } else {
+ this.error('getUserInfoByUserAccount');
+ }
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ protected leaveChannel() {
+ this.engine?.leaveChannel();
+ }
+
+ /**
+ * Step 5: releaseRtcEngine
+ */
+ protected releaseRtcEngine() {
+ this.engine?.unregisterEventHandler(this);
+ this.engine?.release();
+ this.setState({ isInitialized: false });
+ }
+
+ onError(err: ErrorCodeType, msg: string) {
+ super.onError(err, msg);
+ }
+
+ onJoinChannelSuccess(connection: RtcConnection, elapsed: number) {
+ super.onJoinChannelSuccess(connection, elapsed);
+ }
+
+ onLeaveChannel(connection: RtcConnection, stats: RtcStats) {
+ this.releaseRtcEngine();
+ super.onLeaveChannel(connection, stats);
+ }
+
+ onUserJoined(connection: RtcConnection, remoteUid: number, elapsed: number) {
+ super.onUserJoined(connection, remoteUid, elapsed);
+ }
+
+ onUserOffline(
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) {
+ super.onUserOffline(connection, remoteUid, reason);
+ }
+
+ onLocalUserRegistered(uid: number, userAccount: string) {
+ this.info('LocalUserRegistered', uid, userAccount);
+ }
+
+ protected async initializeEngine() {
+ const { appId } = this.state;
+ if (!appId) {
+ this.error(`appId is invalid`);
+ }
+ this.engine!.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ areaCode: this.state.selectedAreaCode,
+ });
+ this.engine!.registerEventHandler(this);
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ this.engine!.enableAudio();
+ this.setState({ isInitialized: true });
+ }
+
+ protected renderChannel(): ReactElement | undefined {
+ const { channelId, joinChannelSuccess, isInitialized, selectedAreaCode } =
+ this.state;
+ return (
+ <>
+ {
+ this.setState({ selectedAreaCode: value });
+ }}
+ />
+ {
+ isInitialized ? this.releaseRtcEngine() : this.initializeEngine();
+ }}
+ />
+ {
+ this.setState({ channelId: text });
+ }}
+ placeholder={`channelId`}
+ value={channelId}
+ />
+ {
+ joinChannelSuccess ? this.leaveChannel() : this.joinChannel();
+ }}
+ />
+ >
+ );
+ }
+
+ protected renderConfiguration(): ReactElement | undefined {
+ const { userAccount, joinChannelSuccess } = this.state;
+ return (
+ <>
+ {
+ this.setState({ userAccount: text });
+ }}
+ placeholder={`userAccount`}
+ value={userAccount}
+ />
+ >
+ );
+ }
+
+ protected renderAction(): ReactElement | undefined {
+ const { joinChannelSuccess } = this.state;
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/basic/index.ts b/examples/expo/app/examples/basic/index.ts
new file mode 100644
index 000000000..7776e968e
--- /dev/null
+++ b/examples/expo/app/examples/basic/index.ts
@@ -0,0 +1,23 @@
+import JoinChannelAudio from './JoinChannelAudio/JoinChannelAudio';
+import JoinChannelVideo from './JoinChannelVideo/JoinChannelVideo';
+import StringUid from './StringUid/StringUid';
+
+const Basic = {
+ title: 'basic',
+ data: [
+ {
+ name: 'JoinChannelAudio',
+ component: JoinChannelAudio,
+ },
+ {
+ name: 'JoinChannelVideo',
+ component: JoinChannelVideo,
+ },
+ {
+ name: 'StringUid',
+ component: StringUid,
+ },
+ ],
+};
+
+export default Basic;
diff --git a/examples/expo/app/examples/hook/AudioMixing/AudioMixing.tsx b/examples/expo/app/examples/hook/AudioMixing/AudioMixing.tsx
new file mode 100644
index 000000000..986941cec
--- /dev/null
+++ b/examples/expo/app/examples/hook/AudioMixing/AudioMixing.tsx
@@ -0,0 +1,252 @@
+import React, { ReactElement, useCallback, useEffect, useState } from 'react';
+import {
+ AudioMixingReasonType,
+ AudioMixingStateType,
+ ClientRoleType,
+} from 'react-native-agora';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraSwitch,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import { getResourcePath } from '../../../../src/utils';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function AudioMixing() {
+ const [enableVideo] = useState(false);
+ const { channelId, setChannelId, token, uid, joinChannelSuccess, engine } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+
+ const [filePath, setFilePath] = useState(
+ getResourcePath('effect.mp3')
+ );
+ const [loopback, setLoopback] = useState(false);
+ const [cycle, setCycle] = useState(-1);
+ const [startPos, setStartPos] = useState(0);
+ const [startAudioMixing, setStartAudioMixing] = useState(false);
+ const [pauseAudioMixing, setPauseAudioMixing] = useState(false);
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ log.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3-1: startAudioMixing
+ */
+ const _startAudioMixing = () => {
+ if (!filePath) {
+ log.error('filePath is invalid');
+ return;
+ }
+ if (cycle < -1) {
+ log.error('cycle is invalid');
+ return;
+ }
+ if (startPos < 0) {
+ log.error('startPos is invalid');
+ return;
+ }
+
+ engine.current.startAudioMixing(filePath, loopback, cycle, startPos);
+ };
+
+ /**
+ * Step 3-2 (Optional): pauseAudioMixing
+ */
+ const _pauseAudioMixing = () => {
+ engine.current.pauseAudioMixing();
+ };
+
+ /**
+ * Step 3-3 (Optional): resumeAudioMixing
+ */
+ const resumeAudioMixing = () => {
+ engine.current.resumeAudioMixing();
+ };
+
+ /**
+ * Step 3-4 (Optional): getAudioMixingCurrentPosition
+ */
+ const getAudioMixingCurrentPosition = () => {
+ const position = engine.current.getAudioMixingCurrentPosition();
+ const duration = engine.current.getAudioMixingDuration();
+ log.debug(
+ 'getAudioMixingCurrentPosition',
+ 'position',
+ position,
+ 'duration',
+ duration
+ );
+ };
+
+ /**
+ * Step 3-5: stopAudioMixing
+ */
+ const stopAudioMixing = () => {
+ engine.current.stopAudioMixing();
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ const onAudioMixingStateChanged = useCallback(
+ (state: AudioMixingStateType, reason: AudioMixingReasonType) => {
+ log.info('onAudioMixingStateChanged', 'state', state, 'reason', reason);
+ switch (state) {
+ case AudioMixingStateType.AudioMixingStatePlaying:
+ setStartAudioMixing(true);
+ setPauseAudioMixing(false);
+ break;
+ case AudioMixingStateType.AudioMixingStatePaused:
+ setPauseAudioMixing(true);
+ break;
+ case AudioMixingStateType.AudioMixingStateStopped:
+ case AudioMixingStateType.AudioMixingStateFailed:
+ setStartAudioMixing(false);
+ break;
+ }
+ },
+ []
+ );
+
+ const onAudioMixingFinished = useCallback(() => {
+ log.info('AudioMixingFinished');
+ }, []);
+
+ const onAudioRoutingChanged = useCallback((routing: number) => {
+ log.info('onAudioRoutingChanged', 'routing', routing);
+ }, []);
+
+ useEffect(() => {
+ engine.current.addListener(
+ 'onAudioMixingStateChanged',
+ onAudioMixingStateChanged
+ );
+ engine.current.addListener('onAudioMixingFinished', onAudioMixingFinished);
+ engine.current.addListener('onAudioRoutingChanged', onAudioRoutingChanged);
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener(
+ 'onAudioMixingStateChanged',
+ onAudioMixingStateChanged
+ );
+ engineCopy.removeListener('onAudioMixingFinished', onAudioMixingFinished);
+ engineCopy.removeListener('onAudioRoutingChanged', onAudioRoutingChanged);
+ };
+ }, [
+ engine,
+ onAudioMixingFinished,
+ onAudioMixingStateChanged,
+ onAudioRoutingChanged,
+ ]);
+
+ return (
+ (
+
+ )}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {
+ setFilePath(text);
+ }}
+ placeholder={'filePath'}
+ value={filePath}
+ />
+ {
+ setLoopback(value);
+ }}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ setCycle((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`cycle (defaults: ${cycle})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ setStartPos((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`startPos (defaults: ${startPos})`}
+ />
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/JoinChannelAudio/JoinChannelAudio.tsx b/examples/expo/app/examples/hook/JoinChannelAudio/JoinChannelAudio.tsx
new file mode 100644
index 000000000..2e7cbd0a4
--- /dev/null
+++ b/examples/expo/app/examples/hook/JoinChannelAudio/JoinChannelAudio.tsx
@@ -0,0 +1,391 @@
+import React, { ReactElement, useCallback, useEffect, useState } from 'react';
+import {
+ ClientRoleType,
+ EarMonitoringFilterType,
+ ErrorCodeType,
+ LocalAudioStreamReason,
+ LocalAudioStreamState,
+ MediaDeviceType,
+ RtcConnection,
+} from 'react-native-agora';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import BaseRenderUsers from '../../../../src/components/hook/BaseRenderUsers';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+} from '../../../../src/components/ui';
+import { enumToItems } from '../../../../src/utils';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function JoinChannelAudio() {
+ const [enableVideo] = useState(false);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ joinChannelSuccess,
+ remoteUsers,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+
+ const [enableLocalAudio, setEnableLocalAudio] = useState(true);
+ const [muteLocalAudioStream, setMuteLocalAudioStream] = useState(false);
+ const [enableSpeakerphone, setEnableSpeakerphone] = useState(false);
+ const [recordingSignalVolume, setRecordingSignalVolume] = useState(100);
+ const [playbackSignalVolume, setPlaybackSignalVolume] = useState(100);
+ const [includeAudioFilters, setIncludeAudioFilters] = useState(
+ EarMonitoringFilterType.EarMonitoringFilterNone
+ );
+ const [enableInEarMonitoring, setEnableInEarMonitoring] = useState(false);
+ const [inEarMonitoringVolume, setInEarMonitoringVolume] = useState(100);
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ log.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3-1-1 (Optional): enableLocalAudio
+ */
+ const _enableLocalAudio = () => {
+ engine.current.enableLocalAudio(true);
+ setEnableLocalAudio(true);
+ };
+
+ /**
+ * Step 3-1-2 (Optional): disableLocalAudio
+ */
+ const disableLocalAudio = () => {
+ engine.current.enableLocalAudio(false);
+ setEnableLocalAudio(false);
+ };
+
+ /**
+ * Step 3-2-1 (Optional): muteLocalAudioStream
+ */
+ const _muteLocalAudioStream = () => {
+ engine.current.muteLocalAudioStream(true);
+ setMuteLocalAudioStream(true);
+ };
+
+ /**
+ * Step 3-2-2 (Optional): unmuteLocalAudioStream
+ */
+ const unmuteLocalAudioStream = () => {
+ engine.current.muteLocalAudioStream(false);
+ setMuteLocalAudioStream(false);
+ };
+
+ /**
+ * Step 3-3-1 (Optional): enableSpeakerphone
+ */
+ const _enableSpeakerphone = () => {
+ engine.current.setEnableSpeakerphone(true);
+ setEnableSpeakerphone(true);
+ };
+
+ /**
+ * Step 3-3-2 (Optional): disableSpeakerphone
+ */
+ const disableSpeakerphone = () => {
+ engine.current.setEnableSpeakerphone(false);
+ setEnableSpeakerphone(false);
+ };
+
+ /**
+ * Step 3-4 (Optional): adjustRecordingSignalVolume
+ */
+ const adjustRecordingSignalVolume = () => {
+ engine.current.adjustRecordingSignalVolume(recordingSignalVolume);
+ };
+
+ /**
+ * Step 3-5 (Optional): adjustPlaybackSignalVolume
+ */
+ const adjustPlaybackSignalVolume = () => {
+ engine.current.adjustPlaybackSignalVolume(playbackSignalVolume);
+ };
+
+ /**
+ * Step 3-6-1 (Optional): enableInEarMonitoring
+ */
+ const _enableInEarMonitoring = () => {
+ if (
+ engine.current.enableInEarMonitoring(true, includeAudioFilters) ===
+ ErrorCodeType.ErrOk
+ ) {
+ setEnableInEarMonitoring(true);
+ }
+ };
+
+ /**
+ * Step 3-6-2 (Optional): setInEarMonitoringVolume
+ */
+ const _setInEarMonitoringVolume = () => {
+ engine.current.setInEarMonitoringVolume(inEarMonitoringVolume);
+ };
+
+ /**
+ * Step 3-6-3 (Optional): disableInEarMonitoring
+ */
+ const disableInEarMonitoring = () => {
+ if (
+ engine.current.enableInEarMonitoring(false, includeAudioFilters) ===
+ ErrorCodeType.ErrOk
+ ) {
+ setEnableInEarMonitoring(false);
+ }
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ const onAudioDeviceStateChanged = useCallback(
+ (deviceId: string, deviceType: number, deviceState: number) => {
+ log.info(
+ 'onAudioDeviceStateChanged',
+ 'deviceId',
+ deviceId,
+ 'deviceType',
+ deviceType,
+ 'deviceState',
+ deviceState
+ );
+ },
+ []
+ );
+
+ const onAudioDeviceVolumeChanged = useCallback(
+ (deviceType: MediaDeviceType, volume: number, muted: boolean) => {
+ log.info(
+ 'onAudioDeviceVolumeChanged',
+ 'deviceType',
+ deviceType,
+ 'volume',
+ volume,
+ 'muted',
+ muted
+ );
+ },
+ []
+ );
+
+ const onLocalAudioStateChanged = useCallback(
+ (
+ connection: RtcConnection,
+ state: LocalAudioStreamState,
+ error: LocalAudioStreamReason
+ ) => {
+ log.info(
+ 'onLocalAudioStateChanged',
+ 'connection',
+ connection,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ },
+ []
+ );
+
+ const onAudioRoutingChanged = useCallback((routing: number) => {
+ log.info('onAudioRoutingChanged', 'routing', routing);
+ }, []);
+
+ useEffect(() => {
+ engine.current.addListener(
+ 'onAudioDeviceStateChanged',
+ onAudioDeviceStateChanged
+ );
+ engine.current.addListener(
+ 'onAudioDeviceVolumeChanged',
+ onAudioDeviceVolumeChanged
+ );
+ engine.current.addListener(
+ 'onLocalAudioStateChanged',
+ onLocalAudioStateChanged
+ );
+ engine.current.addListener('onAudioRoutingChanged', onAudioRoutingChanged);
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener(
+ 'onAudioDeviceStateChanged',
+ onAudioDeviceStateChanged
+ );
+ engineCopy.removeListener(
+ 'onAudioDeviceVolumeChanged',
+ onAudioDeviceVolumeChanged
+ );
+ engineCopy.removeListener(
+ 'onLocalAudioStateChanged',
+ onLocalAudioStateChanged
+ );
+ engineCopy.removeListener('onAudioRoutingChanged', onAudioRoutingChanged);
+ };
+ }, [
+ engine,
+ onAudioDeviceStateChanged,
+ onAudioDeviceVolumeChanged,
+ onAudioRoutingChanged,
+ onLocalAudioStateChanged,
+ ]);
+
+ return (
+ (
+
+ )}
+ renderUsers={() => (
+
+ )}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {
+ setRecordingSignalVolume(value);
+ }}
+ />
+
+
+ {
+ setPlaybackSignalVolume(value);
+ }}
+ />
+
+
+ {
+ setIncludeAudioFilters(value);
+ }}
+ />
+
+ {
+ setInEarMonitoringVolume(value);
+ }}
+ />
+
+
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/JoinChannelVideo/JoinChannelVideo.tsx b/examples/expo/app/examples/hook/JoinChannelVideo/JoinChannelVideo.tsx
new file mode 100644
index 000000000..1f2b78a3f
--- /dev/null
+++ b/examples/expo/app/examples/hook/JoinChannelVideo/JoinChannelVideo.tsx
@@ -0,0 +1,226 @@
+import React, { ReactElement, useEffect, useState } from 'react';
+import { Platform } from 'react-native';
+import {
+ ClientRoleType,
+ LocalVideoStreamReason,
+ LocalVideoStreamState,
+ RtcSurfaceView,
+ RtcTextureView,
+ VideoCanvas,
+ VideoSourceType,
+ VideoViewSetupMode,
+} from 'react-native-agora';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import BaseRenderUsers from '../../../../src/components/hook/BaseRenderUsers';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraSwitch,
+} from '../../../../src/components/ui';
+import { enumToItems } from '../../../../src/utils';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function JoinChannelVideo() {
+ const [enableVideo] = useState(true);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ joinChannelSuccess,
+ remoteUsers,
+ startPreview,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+ const [_, setSwitchCamera] = useState(false);
+ const [renderByTextureView, setRenderByTextureView] = useState(false);
+ const [setupMode, setSetupMode] = useState(
+ VideoViewSetupMode.VideoViewSetupReplace
+ );
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ log.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3 (Optional): switchCamera
+ */
+ const _switchCamera = () => {
+ engine.current.switchCamera();
+ setSwitchCamera((prev) => !prev);
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ useEffect(() => {
+ engine.current.addListener(
+ 'onVideoDeviceStateChanged',
+ (deviceId: string, deviceType: number, deviceState: number) => {
+ log.info(
+ 'onVideoDeviceStateChanged',
+ 'deviceId',
+ deviceId,
+ 'deviceType',
+ deviceType,
+ 'deviceState',
+ deviceState
+ );
+ }
+ );
+
+ engine.current.addListener(
+ 'onLocalVideoStateChanged',
+ (
+ source: VideoSourceType,
+ state: LocalVideoStreamState,
+ error: LocalVideoStreamReason
+ ) => {
+ log.info(
+ 'onLocalVideoStateChanged',
+ 'source',
+ source,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ }
+ );
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeAllListeners();
+ };
+ }, [engine]);
+
+ return (
+ (
+
+ )}
+ renderUsers={() => (
+
+ )}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderVideo(user: VideoCanvas): ReactElement | undefined {
+ return renderByTextureView ? (
+
+ ) : (
+
+ );
+ }
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {Platform.OS === 'android' && (
+ {
+ setRenderByTextureView(value);
+ }}
+ />
+ )}
+
+ {
+ setSetupMode(value);
+ }}
+ />
+ {setupMode === VideoViewSetupMode.VideoViewSetupAdd ? (
+ <>
+
+ {renderByTextureView ? (
+
+ ) : (
+
+ )}
+ >
+ ) : undefined}
+
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/JoinMultipleChannel/JoinMultipleChannel.tsx b/examples/expo/app/examples/hook/JoinMultipleChannel/JoinMultipleChannel.tsx
new file mode 100644
index 000000000..edd2c6127
--- /dev/null
+++ b/examples/expo/app/examples/hook/JoinMultipleChannel/JoinMultipleChannel.tsx
@@ -0,0 +1,382 @@
+import React, { ReactElement, useCallback, useEffect, useState } from 'react';
+import {
+ ClientRoleType,
+ RemoteVideoState,
+ RemoteVideoStateReason,
+ RtcConnection,
+ RtcStats,
+ VideoCanvas,
+} from 'react-native-agora';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import {
+ AgoraButton,
+ AgoraCard,
+ AgoraList,
+ AgoraStyle,
+ AgoraTextInput,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function JoinMultipleChannel() {
+ const [enableVideo] = useState(true);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ setUid,
+ joinChannelSuccess,
+ remoteUsers,
+ setRemoteUsers,
+ startPreview,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo, false);
+
+ const [channelId2, setChannelId2] = useState('');
+ const [token2] = useState('');
+ const [uid2, setUid2] = useState(0);
+ const [joinChannelSuccess2, setJoinChannelSuccess2] =
+ useState(false);
+ const [remoteUsers2, setRemoteUsers2] = useState([]);
+
+ /**
+ * Step 2-1: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid <= 0) {
+ log.error('uid is invalid');
+ return;
+ }
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannelEx(
+ token,
+ {
+ channelId,
+ localUid: uid,
+ },
+ {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ }
+ );
+ };
+
+ /**
+ * Step 2-2: joinChannel2
+ */
+ const joinChannel2 = () => {
+ if (!channelId2) {
+ log.error('channelId2 is invalid');
+ return;
+ }
+ if (uid2 < 0) {
+ log.error('uid2 is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannelEx(
+ token2,
+ {
+ channelId: channelId2,
+ localUid: uid2,
+ },
+ {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ }
+ );
+ };
+
+ /**
+ * Step 3-1: publishStreamToChannel
+ */
+ const publishStreamToChannel = () => {
+ engine.current.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: false, publishCameraTrack: false },
+ {
+ channelId: channelId2,
+ localUid: uid2,
+ }
+ );
+ engine.current.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: true, publishCameraTrack: true },
+ {
+ channelId,
+ localUid: uid,
+ }
+ );
+ };
+
+ /**
+ * Step 3-2: publishStreamToChannel2
+ */
+ const publishStreamToChannel2 = () => {
+ engine.current.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: false, publishCameraTrack: false },
+ {
+ channelId,
+ localUid: uid,
+ }
+ );
+ engine.current.updateChannelMediaOptionsEx(
+ { publishMicrophoneTrack: true, publishCameraTrack: true },
+ {
+ channelId: channelId2,
+ localUid: uid2,
+ }
+ );
+ };
+
+ /**
+ * Step 4-1: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannelEx({
+ channelId,
+ localUid: uid,
+ });
+ };
+
+ /**
+ * Step 4-2: leaveChannel2
+ */
+ const leaveChannel2 = () => {
+ engine.current.leaveChannelEx({
+ channelId: channelId2,
+ localUid: uid2,
+ });
+ };
+
+ const onJoinChannelSuccess = useCallback(
+ (connection: RtcConnection, elapsed: number) => {
+ if (connection.channelId === channelId2 && connection.localUid === uid2) {
+ setJoinChannelSuccess2(true);
+ }
+ },
+ [channelId2, uid2]
+ );
+
+ const onLeaveChannel = useCallback(
+ (connection: RtcConnection, stats: RtcStats) => {
+ if (connection.channelId === channelId2 && connection.localUid === uid2) {
+ setJoinChannelSuccess2(false);
+ setRemoteUsers2([]);
+ }
+ // Keep preview after leave channel
+ engine.current.startPreview();
+ },
+ [channelId2, engine, uid2]
+ );
+
+ const onRemoteVideoStateChanged = useCallback(
+ (
+ connection: RtcConnection,
+ remoteUid: number,
+ state: RemoteVideoState,
+ reason: RemoteVideoStateReason,
+ elapsed: number
+ ) => {
+ log.info(
+ 'onRemoteVideoStateChanged',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'state',
+ state,
+ 'reason',
+ reason,
+ 'elapsed',
+ elapsed
+ );
+ if (state === RemoteVideoState.RemoteVideoStateStarting) {
+ if (connection.channelId === channelId && connection.localUid === uid) {
+ setRemoteUsers((prev) => {
+ return [...prev, remoteUid];
+ });
+ } else if (
+ connection.channelId === channelId2 &&
+ connection.localUid === uid2
+ ) {
+ setRemoteUsers2((prev) => {
+ return [...prev, remoteUid];
+ });
+ }
+ } else if (state === RemoteVideoState.RemoteVideoStateStopped) {
+ if (connection.channelId === channelId && connection.localUid === uid) {
+ setRemoteUsers((prev) => {
+ return prev.filter((value) => value !== remoteUid);
+ });
+ } else if (
+ connection.channelId === channelId2 &&
+ connection.localUid === uid2
+ ) {
+ setRemoteUsers2((prev) => {
+ return prev.filter((value) => value !== remoteUid);
+ });
+ }
+ }
+ },
+ [channelId, channelId2, setRemoteUsers, uid, uid2]
+ );
+
+ useEffect(() => {
+ engine.current.addListener('onJoinChannelSuccess', onJoinChannelSuccess);
+ engine.current.addListener('onLeaveChannel', onLeaveChannel);
+ engine.current.addListener(
+ 'onRemoteVideoStateChanged',
+ onRemoteVideoStateChanged
+ );
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener('onJoinChannelSuccess', onJoinChannelSuccess);
+ engineCopy.removeListener('onLeaveChannel', onLeaveChannel);
+ engineCopy.removeListener(
+ 'onRemoteVideoStateChanged',
+ onRemoteVideoStateChanged
+ );
+ };
+ }, [engine, onJoinChannelSuccess, onLeaveChannel, onRemoteVideoStateChanged]);
+
+ return (
+
+ );
+
+ function renderChannel(): ReactElement | undefined {
+ return (
+ <>
+ {
+ setChannelId(text);
+ }}
+ placeholder={`channelId`}
+ value={channelId}
+ />
+ {
+ if (isNaN(+text)) return;
+ setUid(+text);
+ }}
+ numberKeyboard={true}
+ placeholder={`uid (must > 0)`}
+ value={uid > 0 ? uid.toString() : ''}
+ />
+ {
+ joinChannelSuccess ? leaveChannel() : joinChannel();
+ }}
+ />
+ {
+ setChannelId2(text);
+ }}
+ placeholder={`channelId2`}
+ value={channelId2}
+ />
+ {
+ if (isNaN(+text)) return;
+ setUid2(+text);
+ }}
+ numberKeyboard={true}
+ placeholder={`uid2 (must > 0)`}
+ value={uid2 > 0 ? uid2.toString() : ''}
+ />
+ {
+ joinChannelSuccess2 ? leaveChannel2() : joinChannel2();
+ }}
+ />
+ >
+ );
+ }
+
+ function renderUsers(): ReactElement | undefined {
+ return (
+ <>
+ {startPreview || joinChannelSuccess || joinChannelSuccess2 ? (
+
+ renderVideo(
+ { uid: item },
+ remoteUsers2.indexOf(item) === -1 ? channelId : channelId2,
+ remoteUsers2.indexOf(item) === -1 ? uid : uid2
+ )!
+ }
+ />
+ ) : undefined}
+ >
+ );
+ }
+
+ function renderVideo(
+ user: VideoCanvas,
+ channelId?: string,
+ localUid?: number
+ ): ReactElement | undefined {
+ return (
+
+
+
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/ScreenShare/ScreenShare.tsx b/examples/expo/app/examples/hook/ScreenShare/ScreenShare.tsx
new file mode 100644
index 000000000..56af32972
--- /dev/null
+++ b/examples/expo/app/examples/hook/ScreenShare/ScreenShare.tsx
@@ -0,0 +1,531 @@
+import React, { ReactElement, useCallback, useEffect, useState } from 'react';
+import { Platform } from 'react-native';
+import {
+ ClientRoleType,
+ LocalVideoStreamReason,
+ LocalVideoStreamState,
+ PermissionType,
+ RenderModeType,
+ RtcConnection,
+ RtcStats,
+ UserOfflineReasonType,
+ VideoCanvas,
+ VideoContentHint,
+ VideoSourceType,
+ showRPSystemBroadcastPickerView,
+} from 'react-native-agora';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import BaseRenderUsers from '../../../../src/components/hook/BaseRenderUsers';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraSlider,
+ AgoraStyle,
+ AgoraSwitch,
+ AgoraTextInput,
+ AgoraView,
+ RtcSurfaceView,
+} from '../../../../src/components/ui';
+import { enumToItems } from '../../../../src/utils';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function ScreenShare() {
+ const [enableVideo] = useState(true);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ joinChannelSuccess,
+ remoteUsers,
+ startPreview,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+ const [token2] = useState('');
+ const [uid2, setUid2] = useState(0);
+ const [captureAudio, setCaptureAudio] = useState(false);
+ const [sampleRate, setSampleRate] = useState(16000);
+ const [channels, setChannels] = useState(2);
+ const [captureSignalVolume, setCaptureSignalVolume] = useState(100);
+ const [captureVideo, setCaptureVideo] = useState(true);
+
+ const [width, setWidth] = useState(1280);
+ const [height, setHeight] = useState(720);
+ const [frameRate, setFrameRate] = useState(15);
+ const [bitrate, setBitrate] = useState(0);
+ const [contentHint, setContentHint] = useState(
+ VideoContentHint.ContentHintMotion
+ );
+ const [startScreenCapture, setStartScreenCapture] = useState(false);
+ const [publishScreenCapture, setPublishScreenCapture] =
+ useState(false);
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ log.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3-1: startScreenCapture
+ */
+ const _startScreenCapture = async () => {
+ engine.current.startScreenCapture({
+ captureAudio,
+ audioParams: {
+ sampleRate,
+ channels,
+ captureSignalVolume,
+ },
+ captureVideo,
+ videoParams: {
+ dimensions: { width, height },
+ frameRate,
+ bitrate,
+ contentHint,
+ },
+ });
+ engine.current.startPreview(VideoSourceType.VideoSourceScreen);
+
+ if (Platform.OS === 'ios') {
+ // Show the picker view for screen share, ⚠️ only support for iOS 12+
+ await showRPSystemBroadcastPickerView(true);
+ }
+
+ if (captureAudio && !captureVideo) {
+ setStartScreenCapture(true);
+ }
+ };
+
+ /**
+ * Step 3-2 (Optional): updateScreenCaptureParameters
+ */
+ const updateScreenCaptureParameters = () => {
+ engine.current.updateScreenCapture({
+ captureAudio,
+ audioParams: {
+ sampleRate,
+ channels,
+ captureSignalVolume,
+ },
+ captureVideo,
+ videoParams: {
+ dimensions: { width, height },
+ frameRate,
+ bitrate,
+ contentHint,
+ },
+ });
+
+ if (!captureAudio && !captureVideo) {
+ setStartScreenCapture(false);
+ } else {
+ // ⚠️ You should updateChannelMediaOptionsEx if you change captureAudio or captureVideo
+ if (publishScreenCapture) {
+ engine.current.updateChannelMediaOptionsEx(
+ {
+ publishScreenCaptureAudio: captureAudio,
+ publishScreenCaptureVideo: captureVideo,
+ },
+ { channelId, localUid: uid2 }
+ );
+ }
+ }
+ };
+
+ /**
+ * Step 3-3: publishScreenCapture
+ */
+ const _publishScreenCapture = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid2 <= 0) {
+ log.error('uid2 is invalid');
+ return;
+ }
+
+ // publish screen share stream
+ engine.current.joinChannelEx(
+ token2,
+ { channelId, localUid: uid2 },
+ {
+ autoSubscribeAudio: false,
+ autoSubscribeVideo: false,
+ publishMicrophoneTrack: false,
+ publishCameraTrack: false,
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ publishScreenCaptureAudio: true,
+ publishScreenCaptureVideo: true,
+ }
+ );
+ };
+
+ /**
+ * Step 3-4: stopScreenCapture
+ */
+ const stopScreenCapture = useCallback(() => {
+ engine.current.stopScreenCapture();
+ setStartScreenCapture(false);
+ }, [engine]);
+
+ /**
+ * Step 3-5: unpublishScreenCapture
+ */
+ const unpublishScreenCapture = () => {
+ engine.current.leaveChannelEx({ channelId, localUid: uid2 });
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ const onJoinChannelSuccess = useCallback(
+ (connection: RtcConnection, elapsed: number) => {
+ if (connection.localUid === uid2) {
+ log.info(
+ 'onJoinChannelSuccess',
+ 'connection',
+ connection,
+ 'elapsed',
+ elapsed
+ );
+ setPublishScreenCapture(true);
+ return;
+ }
+ },
+ [uid2]
+ );
+
+ const onLeaveChannel = useCallback(
+ (connection: RtcConnection, stats: RtcStats) => {
+ log.info('onLeaveChannel', 'connection', connection, 'stats', stats);
+ if (connection.localUid === uid2) {
+ setPublishScreenCapture(false);
+ return;
+ }
+ },
+ [uid2]
+ );
+
+ const onUserJoined = useCallback(
+ (connection: RtcConnection, remoteUid: number, elapsed: number) => {
+ if (connection.localUid === uid2 || remoteUid === uid2) {
+ // ⚠️ mute the streams from screen sharing
+ engine.current.muteRemoteAudioStream(uid2, true);
+ engine.current.muteRemoteVideoStream(uid2, true);
+ return;
+ }
+ },
+ [engine, uid2]
+ );
+
+ const onUserOffline = useCallback(
+ (
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) => {
+ if (connection.localUid === uid2 || remoteUid === uid2) return;
+ },
+ [uid2]
+ );
+
+ const onLocalVideoStateChanged = useCallback(
+ (
+ source: VideoSourceType,
+ state: LocalVideoStreamState,
+ error: LocalVideoStreamReason
+ ) => {
+ log.info(
+ 'onLocalVideoStateChanged',
+ 'source',
+ source,
+ 'state',
+ state,
+ 'error',
+ error
+ );
+ if (source === VideoSourceType.VideoSourceScreen) {
+ switch (state) {
+ case LocalVideoStreamState.LocalVideoStreamStateStopped:
+ case LocalVideoStreamState.LocalVideoStreamStateFailed:
+ break;
+ case LocalVideoStreamState.LocalVideoStreamStateCapturing:
+ case LocalVideoStreamState.LocalVideoStreamStateEncoding:
+ setStartScreenCapture(true);
+ break;
+ }
+ }
+ },
+ []
+ );
+
+ const onPermissionError = useCallback(
+ (permissionType: PermissionType) => {
+ log.info('onPermissionError', 'permissionType', permissionType);
+ // ⚠️ You should call stopScreenCapture if received the event with permissionType ScreenCapture,
+ // otherwise you can not startScreenCapture again
+ stopScreenCapture();
+ setStartScreenCapture(false);
+ },
+ [stopScreenCapture]
+ );
+
+ useEffect(() => {
+ engine.current.addListener('onJoinChannelSuccess', onJoinChannelSuccess);
+ engine.current.addListener('onLeaveChannel', onLeaveChannel);
+ engine.current.addListener('onUserJoined', onUserJoined);
+ engine.current.addListener('onUserOffline', onUserOffline);
+ engine.current.addListener(
+ 'onLocalVideoStateChanged',
+ onLocalVideoStateChanged
+ );
+ engine.current.addListener('onPermissionError', onPermissionError);
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener('onJoinChannelSuccess', onJoinChannelSuccess);
+ engineCopy.removeListener('onLeaveChannel', onLeaveChannel);
+ engineCopy.removeListener('onUserJoined', onUserJoined);
+ engineCopy.removeListener('onUserOffline', onUserOffline);
+ engineCopy.removeListener(
+ 'onLocalVideoStateChanged',
+ onLocalVideoStateChanged
+ );
+ engineCopy.removeListener('onPermissionError', onPermissionError);
+ };
+ }, [
+ engine,
+ onJoinChannelSuccess,
+ onLeaveChannel,
+ onLocalVideoStateChanged,
+ onUserJoined,
+ onUserOffline,
+ onPermissionError,
+ ]);
+
+ return (
+ (
+
+ )}
+ renderUsers={renderUsers}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderUsers(): ReactElement | undefined {
+ return (
+ <>
+
+ {startScreenCapture ? (
+
+ ) : undefined}
+ >
+ );
+ }
+
+ function renderVideo(user: VideoCanvas): ReactElement | undefined {
+ return (
+
+ );
+ }
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {
+ if (isNaN(+text)) return;
+ setUid2((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`uid2 (must > 0)`}
+ value={uid2 > 0 ? uid2.toString() : ''}
+ />
+ {
+ setCaptureAudio(value);
+ }}
+ />
+
+ {captureAudio ? (
+ <>
+ {Platform.OS === 'android' ? (
+ <>
+ {
+ if (isNaN(+text)) return;
+ setSampleRate((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`sampleRate (defaults: ${sampleRate})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ setChannels((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`channels (defaults: ${channels})`}
+ />
+ >
+ ) : undefined}
+ {
+ setCaptureSignalVolume(value);
+ }}
+ />
+
+ >
+ ) : undefined}
+ {
+ setCaptureVideo(value);
+ }}
+ />
+
+ {captureVideo ? (
+ <>
+
+ {
+ if (isNaN(+text)) return;
+ setWidth((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`width (defaults: ${width})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ setHeight((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`height (defaults: ${height})`}
+ />
+
+ {
+ if (isNaN(+text)) return;
+ setFrameRate((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`frameRate (defaults: ${frameRate})`}
+ />
+ {
+ if (isNaN(+text)) return;
+ setBitrate((prev) => (text === '' ? prev : +text));
+ }}
+ numberKeyboard={true}
+ placeholder={`bitrate (defaults: ${bitrate})`}
+ />
+ {
+ setContentHint(value);
+ }}
+ />
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/StringUid/StringUid.tsx b/examples/expo/app/examples/hook/StringUid/StringUid.tsx
new file mode 100644
index 000000000..f72ea58c9
--- /dev/null
+++ b/examples/expo/app/examples/hook/StringUid/StringUid.tsx
@@ -0,0 +1,140 @@
+import React, { ReactElement, useCallback, useEffect, useState } from 'react';
+import { ClientRoleType } from 'react-native-agora';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import BaseRenderUsers from '../../../../src/components/hook/BaseRenderUsers';
+import { AgoraButton, AgoraTextInput } from '../../../../src/components/ui';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function StringUid() {
+ const [enableVideo] = useState(false);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ setUid,
+ joinChannelSuccess,
+ remoteUsers,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+
+ const [userAccount, setUserAccount] = useState('');
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (!userAccount) {
+ log.error('userAccount is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannelWithUserAccount(token, channelId, userAccount, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3 (Optional): getUserInfoByUserAccount
+ */
+ const getUserInfoByUserAccount = () => {
+ const userInfo = engine.current.getUserInfoByUserAccount(userAccount);
+ if (userInfo) {
+ log.debug('getUserInfoByUserAccount', userInfo);
+ } else {
+ log.error('getUserInfoByUserAccount');
+ }
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ const onLocalUserRegistered = useCallback(
+ (uid: number, userAccount: string) => {
+ log.info('LocalUserRegistered', 'uid', uid, 'userAccount', userAccount);
+ setUid(uid);
+ },
+ [setUid]
+ );
+
+ useEffect(() => {
+ engine.current.addListener('onLocalUserRegistered', onLocalUserRegistered);
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener('onLocalUserRegistered', onLocalUserRegistered);
+ };
+ }, [engine, onLocalUserRegistered]);
+
+ return (
+ (
+
+ )}
+ renderUsers={() => (
+
+ )}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {
+ setUserAccount(text);
+ }}
+ placeholder={`userAccount`}
+ value={userAccount}
+ />
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/TakeSnapshot/TakeSnapshot.tsx b/examples/expo/app/examples/hook/TakeSnapshot/TakeSnapshot.tsx
new file mode 100644
index 000000000..4f0980dc1
--- /dev/null
+++ b/examples/expo/app/examples/hook/TakeSnapshot/TakeSnapshot.tsx
@@ -0,0 +1,207 @@
+import React, {
+ ReactElement,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from 'react';
+import { Platform } from 'react-native';
+import {
+ ClientRoleType,
+ ErrorCodeType,
+ RtcConnection,
+} from 'react-native-agora';
+import RNFS from 'react-native-fs';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import BaseRenderUsers from '../../../../src/components/hook/BaseRenderUsers';
+import {
+ AgoraButton,
+ AgoraDivider,
+ AgoraDropdown,
+ AgoraImage,
+ AgoraStyle,
+} from '../../../../src/components/ui';
+import { arrayToItems } from '../../../../src/utils';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function TakeSnapshot() {
+ const [enableVideo] = useState(true);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ joinChannelSuccess,
+ remoteUsers,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+
+ const [targetUid, setTargetUid] = useState(0);
+ const [osFilePath] = useState(
+ `${
+ Platform.OS === 'android'
+ ? RNFS.ExternalCachesDirectoryPath
+ : RNFS.DocumentDirectoryPath
+ }`
+ );
+ const timestamp = useRef(0);
+ const [takeSnapshot, setTakeSnapshot] = useState(false);
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ log.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3: takeSnapshot
+ */
+ const _takeSnapshot = () => {
+ if (!osFilePath) {
+ log.error('filePath is invalid');
+ return;
+ }
+ timestamp.current = new Date().getTime();
+ engine.current.takeSnapshot(
+ targetUid,
+ `${osFilePath}/${targetUid}-${timestamp.current}.jpg`
+ );
+ setTakeSnapshot(false);
+ };
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ const onSnapshotTaken = useCallback(
+ (
+ connection: RtcConnection,
+ uid: number,
+ filePath: string,
+ width: number,
+ height: number,
+ errCode: number
+ ) => {
+ log.info(
+ 'onSnapshotTaken',
+ 'connection',
+ connection,
+ 'uid',
+ uid,
+ 'filePath',
+ filePath,
+ 'width',
+ width,
+ 'height',
+ height,
+ 'errCode',
+ errCode
+ );
+ if (filePath === `${osFilePath}/${targetUid}-${timestamp.current}.jpg`) {
+ setTakeSnapshot(errCode === ErrorCodeType.ErrOk);
+ }
+ },
+ [osFilePath, targetUid]
+ );
+
+ useEffect(() => {
+ engine.current.addListener('onSnapshotTaken', onSnapshotTaken);
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener('onSnapshotTaken', onSnapshotTaken);
+ };
+ }, [engine, onSnapshotTaken]);
+
+ return (
+ (
+
+ )}
+ renderUsers={() => (
+
+ )}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {
+ setTargetUid(value);
+ }}
+ />
+ {takeSnapshot ? (
+ <>
+
+
+ >
+ ) : undefined}
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/VirtualBackground/VirtualBackground.tsx b/examples/expo/app/examples/hook/VirtualBackground/VirtualBackground.tsx
new file mode 100644
index 000000000..607bab6cd
--- /dev/null
+++ b/examples/expo/app/examples/hook/VirtualBackground/VirtualBackground.tsx
@@ -0,0 +1,209 @@
+import React, { ReactElement, useState } from 'react';
+import {
+ BackgroundBlurDegree,
+ BackgroundSourceType,
+ ClientRoleType,
+} from 'react-native-agora';
+import ColorPicker, { Panel1 } from 'reanimated-color-picker';
+
+import { BaseComponent } from '../../../../src/components/hook/BaseComponent';
+import BaseRenderChannel from '../../../../src/components/hook/BaseRenderChannel';
+import BaseRenderUsers from '../../../../src/components/hook/BaseRenderUsers';
+import {
+ AgoraButton,
+ AgoraDropdown,
+ AgoraStyle,
+ AgoraTextInput,
+} from '../../../../src/components/ui';
+import {
+ enumToItems,
+ getAbsolutePath,
+ getResourcePath,
+} from '../../../../src/utils';
+import * as log from '../../../../src/utils/log';
+import useInitRtcEngine from '../hooks/useInitRtcEngine';
+
+export default function VirtualBackground() {
+ const [enableVideo] = useState(true);
+ const {
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ joinChannelSuccess,
+ remoteUsers,
+ startPreview,
+ engine,
+ } =
+ /**
+ * Step 1: initRtcEngine
+ */
+ useInitRtcEngine(enableVideo);
+
+ const [background_source_type, setBackground_source_type] = useState(
+ BackgroundSourceType.BackgroundColor
+ );
+ const [color, setColor] = useState('#ffffff');
+ const [source, setSource] = useState(getResourcePath('agora-logo.png'));
+ const [blur_degree, setBlur_degree] = useState(
+ BackgroundBlurDegree.BlurDegreeMedium
+ );
+ const [enableVirtualBackground, setEnableVirtualBackground] = useState(false);
+
+ /**
+ * Step 2: joinChannel
+ */
+ const joinChannel = () => {
+ if (!channelId) {
+ log.error('channelId is invalid');
+ return;
+ }
+ if (uid < 0) {
+ log.error('uid is invalid');
+ return;
+ }
+
+ // start joining channel
+ // 1. Users can only see each other after they join the
+ // same channel successfully using the same app id.
+ // 2. If app certificate is turned on at dashboard, token is needed
+ // when joining channel. The channel name and uid used to calculate
+ // the token has to match the ones used for channel join
+ engine.current.joinChannel(token, channelId, uid, {
+ // Make myself as the broadcaster to send stream to remote
+ clientRoleType: ClientRoleType.ClientRoleBroadcaster,
+ });
+ };
+
+ /**
+ * Step 3-1: enableVirtualBackground
+ */
+ const _enableVirtualBackground = async () => {
+ if (
+ background_source_type === BackgroundSourceType.BackgroundImg &&
+ !source
+ ) {
+ log.error('source is invalid');
+ return;
+ }
+
+ engine.current.enableVirtualBackground(
+ true,
+ {
+ background_source_type,
+ color: +color.replace('#', '0x'),
+ source: await getAbsolutePath(source),
+ blur_degree,
+ },
+ {}
+ );
+ setEnableVirtualBackground(true);
+ };
+
+ /**
+ * Step 3-2: disableVirtualBackground
+ */
+ const disableVirtualBackground = () => {
+ engine.current.enableVirtualBackground(false, {}, {});
+ setEnableVirtualBackground(false);
+ };
+
+ /**
+ * Step 4: leaveChannel
+ */
+ const leaveChannel = () => {
+ engine.current.leaveChannel();
+ };
+
+ const onSelectColor = ({ hex }) => {
+ setColor(hex);
+ };
+
+ return (
+ (
+
+ )}
+ renderUsers={() => (
+
+ )}
+ renderAction={renderAction}
+ />
+ );
+
+ function renderConfiguration(): ReactElement | undefined {
+ return (
+ <>
+ {
+ setBackground_source_type(value);
+ }}
+ />
+ {background_source_type === BackgroundSourceType.BackgroundColor ? (
+
+
+
+ ) : undefined}
+ {
+ setSource(text);
+ }}
+ placeholder={'source'}
+ value={source}
+ />
+ {
+ setBlur_degree(value);
+ }}
+ />
+ >
+ );
+ }
+
+ function renderAction(): ReactElement | undefined {
+ return (
+ <>
+
+ >
+ );
+ }
+}
diff --git a/examples/expo/app/examples/hook/hooks/useInitRtcEngine.tsx b/examples/expo/app/examples/hook/hooks/useInitRtcEngine.tsx
new file mode 100644
index 000000000..f9377b342
--- /dev/null
+++ b/examples/expo/app/examples/hook/hooks/useInitRtcEngine.tsx
@@ -0,0 +1,206 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import createAgoraRtcEngine, {
+ ChannelProfileType,
+ ErrorCodeType,
+ IRtcEngineEx,
+ RtcConnection,
+ RtcStats,
+ UserOfflineReasonType,
+} from 'react-native-agora';
+
+import Config from '../../../../src/config/agora.config';
+import * as log from '../../../../src/utils/log';
+import { askMediaAccess } from '../../../../src/utils/permissions';
+
+const useInitRtcEngine = (
+ enableVideo: boolean,
+ listenUserJoinOrLeave: boolean = true
+) => {
+ const [appId] = useState(Config.appId);
+ const [channelId, setChannelId] = useState(Config.channelId);
+ const [token] = useState(Config.token);
+ const [uid, setUid] = useState(Config.uid);
+ const [joinChannelSuccess, setJoinChannelSuccess] = useState(false);
+ const [remoteUsers, setRemoteUsers] = useState([]);
+ const [startPreview, setStartPreview] = useState(false);
+
+ const engine = useRef(createAgoraRtcEngine() as IRtcEngineEx);
+
+ const initRtcEngine = useCallback(async () => {
+ if (!appId) {
+ log.error(`appId is invalid`);
+ }
+
+ engine.current.initialize({
+ appId,
+ logConfig: { filePath: Config.logFilePath },
+ // Should use ChannelProfileLiveBroadcasting on most of cases
+ channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
+ });
+
+ // Need granted the microphone permission
+ await askMediaAccess(['android.permission.RECORD_AUDIO']);
+
+ // Only need to enable audio on this case
+ engine.current.enableAudio();
+
+ if (enableVideo) {
+ // Need granted the camera permission
+ await askMediaAccess(['android.permission.CAMERA']);
+
+ // Need to enable video on this case
+ // If you only call `enableAudio`, only relay the audio stream to the target channel
+ engine.current.enableVideo();
+
+ // Start preview before joinChannel
+ engine.current.startPreview();
+ setStartPreview(true);
+ }
+ }, [appId, enableVideo]);
+
+ const onError = useCallback((err: ErrorCodeType, msg: string) => {
+ log.info('onError', 'err', err, 'msg', msg);
+ }, []);
+
+ const onJoinChannelSuccess = useCallback(
+ (connection: RtcConnection, elapsed: number) => {
+ log.info(
+ 'onJoinChannelSuccess',
+ 'connection',
+ connection,
+ 'elapsed',
+ elapsed
+ );
+ if (
+ connection.channelId === channelId &&
+ (connection.localUid === uid || uid === 0)
+ ) {
+ setJoinChannelSuccess(true);
+ }
+ },
+ [channelId, uid]
+ );
+
+ const onLeaveChannel = useCallback(
+ (connection: RtcConnection, stats: RtcStats) => {
+ log.info('onLeaveChannel', 'connection', connection, 'stats', stats);
+ if (
+ connection.channelId === channelId &&
+ (connection.localUid === uid || uid === 0)
+ ) {
+ setJoinChannelSuccess(false);
+ setRemoteUsers([]);
+ }
+ },
+ [channelId, uid]
+ );
+
+ const onUserJoined = useCallback(
+ (connection: RtcConnection, remoteUid: number, elapsed: number) => {
+ log.info(
+ 'onUserJoined',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'elapsed',
+ elapsed
+ );
+ if (
+ connection.channelId === channelId &&
+ (connection.localUid === uid || uid === 0)
+ ) {
+ setRemoteUsers((prev) => {
+ if (prev === undefined) return [];
+ return [...prev, remoteUid];
+ });
+ }
+ },
+ [channelId, uid]
+ );
+
+ const onUserOffline = useCallback(
+ (
+ connection: RtcConnection,
+ remoteUid: number,
+ reason: UserOfflineReasonType
+ ) => {
+ log.info(
+ 'onUserOffline',
+ 'connection',
+ connection,
+ 'remoteUid',
+ remoteUid,
+ 'reason',
+ reason
+ );
+ if (
+ connection.channelId === channelId &&
+ (connection.localUid === uid || uid === 0)
+ ) {
+ setRemoteUsers((prev) => {
+ if (prev === undefined) return [];
+ return prev!.filter((value) => value !== remoteUid);
+ });
+ }
+ },
+ [channelId, uid]
+ );
+
+ useEffect(() => {
+ (async () => {
+ await initRtcEngine();
+ })();
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.release();
+ };
+ }, [engine, initRtcEngine]);
+
+ useEffect(() => {
+ engine.current.addListener('onError', onError);
+ engine.current.addListener('onJoinChannelSuccess', onJoinChannelSuccess);
+ engine.current.addListener('onLeaveChannel', onLeaveChannel);
+ if (listenUserJoinOrLeave) {
+ engine.current.addListener('onUserJoined', onUserJoined);
+ engine.current.addListener('onUserOffline', onUserOffline);
+ }
+
+ const engineCopy = engine.current;
+ return () => {
+ engineCopy.removeListener('onError', onError);
+ engineCopy.removeListener('onJoinChannelSuccess', onJoinChannelSuccess);
+ engineCopy.removeListener('onLeaveChannel', onLeaveChannel);
+ if (listenUserJoinOrLeave) {
+ engineCopy.removeListener('onUserJoined', onUserJoined);
+ engineCopy.removeListener('onUserOffline', onUserOffline);
+ }
+ };
+ }, [
+ engine,
+ initRtcEngine,
+ onError,
+ onJoinChannelSuccess,
+ onLeaveChannel,
+ onUserJoined,
+ onUserOffline,
+ listenUserJoinOrLeave,
+ ]);
+
+ return {
+ appId,
+ channelId,
+ setChannelId,
+ token,
+ uid,
+ setUid,
+ joinChannelSuccess,
+ setJoinChannelSuccess,
+ remoteUsers,
+ setRemoteUsers,
+ startPreview,
+ engine,
+ };
+};
+export default useInitRtcEngine;
diff --git a/example/src/examples/hook/hooks/useResetState.tsx b/examples/expo/app/examples/hook/hooks/useResetState.tsx
similarity index 100%
rename from example/src/examples/hook/hooks/useResetState.tsx
rename to examples/expo/app/examples/hook/hooks/useResetState.tsx
diff --git a/examples/expo/app/examples/hook/index.ts b/examples/expo/app/examples/hook/index.ts
new file mode 100644
index 000000000..ce98c4534
--- /dev/null
+++ b/examples/expo/app/examples/hook/index.ts
@@ -0,0 +1,47 @@
+import AudioMixing from './AudioMixing/AudioMixing';
+import JoinChannelAudio from './JoinChannelAudio/JoinChannelAudio';
+import JoinChannelVideo from './JoinChannelVideo/JoinChannelVideo';
+import JoinMultipleChannel from './JoinMultipleChannel/JoinMultipleChannel';
+import ScreenShare from './ScreenShare/ScreenShare';
+import StringUid from './StringUid/StringUid';
+import TakeSnapshot from './TakeSnapshot/TakeSnapshot';
+import VirtualBackground from './VirtualBackground/VirtualBackground';
+
+const Hooks = {
+ title: 'hook',
+ data: [
+ {
+ name: 'JoinChannelVideo',
+ component: JoinChannelVideo,
+ },
+ {
+ name: 'JoinChannelAudio',
+ component: JoinChannelAudio,
+ },
+ {
+ name: 'StringUid',
+ component: StringUid,
+ },
+ {
+ name: 'JoinMultipleChannel',
+ component: JoinMultipleChannel,
+ },
+ {
+ name: 'VirtualBackground',
+ component: VirtualBackground,
+ },
+ {
+ name: 'AudioMixing',
+ component: AudioMixing,
+ },
+ {
+ name: 'TakeSnapshot',
+ component: TakeSnapshot,
+ },
+ {
+ name: 'ScreenShare',
+ component: ScreenShare,
+ },
+ ],
+};
+export default Hooks;
diff --git a/examples/expo/app/index.tsx b/examples/expo/app/index.tsx
new file mode 100644
index 000000000..5409914df
--- /dev/null
+++ b/examples/expo/app/index.tsx
@@ -0,0 +1,100 @@
+import { Link } from 'expo-router';
+import React, { useEffect, useState } from 'react';
+import {
+ SectionList,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+
+import {
+ AgoraPipState,
+ SDKBuildInfo,
+ createAgoraRtcEngine,
+ isDebuggable,
+ setDebuggable,
+} from 'react-native-agora';
+
+import { PipStateConsumer } from '../src/context/pip';
+
+import * as log from '../src/utils/log';
+
+import Advanced from './examples/advanced';
+import Basic from './examples/basic';
+import Hooks from './examples/hook';
+
+const DATA = [Basic, Advanced, Hooks];
+const AppSectionList = SectionList;
+
+export default function Index() {
+ const [version, setVersion] = useState({});
+
+ useEffect(() => {
+ const engine = createAgoraRtcEngine();
+ setVersion(engine.getVersion());
+ }, []);
+
+ return (
+
+ {(context) => (
+ <>
+ item.name + index}
+ renderItem={({ item, section }) => (
+
+ {
+ log.logSink.clearData();
+ }}
+ style={styles.title}
+ href={`examples/${section.title}/${item.name}/${item.name}`}
+ >
+ {item.name}
+
+
+ )}
+ renderSectionHeader={({ section: { title } }) => (
+ {title}
+ )}
+ />
+ {context.pipState !== AgoraPipState.pipStateStarted && (
+ {
+ setDebuggable(!isDebuggable());
+ }}
+ >
+
+ Powered by Agora RTC SDK {version.version} build {version.build}
+
+
+ )}
+ >
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ header: {
+ padding: 10,
+ fontSize: 24,
+ color: 'white',
+ backgroundColor: 'grey',
+ },
+ item: {
+ padding: 15,
+ },
+ title: {
+ fontSize: 24,
+ color: 'black',
+ },
+ version: {
+ backgroundColor: '#ffffffdd',
+ textAlign: 'center',
+ },
+});
diff --git a/examples/expo/assets/adaptive-icon.png b/examples/expo/assets/adaptive-icon.png
new file mode 100644
index 000000000..03d6f6b6c
Binary files /dev/null and b/examples/expo/assets/adaptive-icon.png differ
diff --git a/examples/expo/assets/favicon.png b/examples/expo/assets/favicon.png
new file mode 100644
index 000000000..e75f697b1
Binary files /dev/null and b/examples/expo/assets/favicon.png differ
diff --git a/examples/expo/assets/icon.png b/examples/expo/assets/icon.png
new file mode 100644
index 000000000..a0b1526fc
Binary files /dev/null and b/examples/expo/assets/icon.png differ
diff --git a/examples/expo/assets/splash-icon.png b/examples/expo/assets/splash-icon.png
new file mode 100644
index 000000000..03d6f6b6c
Binary files /dev/null and b/examples/expo/assets/splash-icon.png differ
diff --git a/examples/expo/babel.config.js b/examples/expo/babel.config.js
new file mode 100644
index 000000000..16be9adce
--- /dev/null
+++ b/examples/expo/babel.config.js
@@ -0,0 +1,21 @@
+const path = require('path');
+
+const pak = require('../../package.json');
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: ['babel-preset-expo'],
+ plugins: [
+ [
+ 'module-resolver',
+ {
+ extensions: ['.tsx', '.ts', '.js', '.json'],
+ alias: {
+ [pak.name]: path.join(__dirname, '../..', pak.source),
+ },
+ },
+ ],
+ 'react-native-reanimated/plugin',
+ ],
+ };
+};
diff --git a/example/e2e/jest.config.js b/examples/expo/e2e/jest.config.js
similarity index 100%
rename from example/e2e/jest.config.js
rename to examples/expo/e2e/jest.config.js
diff --git a/examples/expo/e2e/starter.test.js b/examples/expo/e2e/starter.test.js
new file mode 100644
index 000000000..fceff8fd4
--- /dev/null
+++ b/examples/expo/e2e/starter.test.js
@@ -0,0 +1,31 @@
+import { by, device, element, expect } from 'detox';
+
+describe('Example', () => {
+ beforeAll(async () => {
+ const permissions = { camera: 'YES', microphone: 'YES' };
+ await device.launchApp({ permissions });
+ });
+
+ beforeEach(async () => {
+ if (device.getPlatform() === 'android') {
+ await device.reloadReactNative();
+ }
+ });
+
+ it('should have APIExample screen', async () => {
+ await expect(element(by.text('API Example'))).toBeVisible();
+ });
+
+ it('should show JoinChannelAudio screen after tap', async () => {
+ await element(by.text('JoinChannelAudio')).atIndex(0).tap();
+ await expect(element(by.text('JoinChannelAudio'))).toBeVisible();
+ if (device.getPlatform() === 'ios') {
+ await element(by.text('API Example')).atIndex(0).tap();
+ }
+ });
+
+ it('should show JoinChannelVideo screen after tap', async () => {
+ await element(by.text('JoinChannelVideo')).atIndex(0).tap();
+ await expect(element(by.text('JoinChannelVideo'))).toBeVisible();
+ });
+});
diff --git a/examples/expo/ios/.gitignore b/examples/expo/ios/.gitignore
new file mode 100644
index 000000000..8beb34430
--- /dev/null
+++ b/examples/expo/ios/.gitignore
@@ -0,0 +1,30 @@
+# OSX
+#
+.DS_Store
+
+# Xcode
+#
+build/
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+xcuserdata
+*.xccheckout
+*.moved-aside
+DerivedData
+*.hmap
+*.ipa
+*.xcuserstate
+project.xcworkspace
+.xcode.env.local
+
+# Bundle artifacts
+*.jsbundle
+
+# CocoaPods
+/Pods/
diff --git a/example/ios/.xcode.env b/examples/expo/ios/.xcode.env
similarity index 100%
rename from example/ios/.xcode.env
rename to examples/expo/ios/.xcode.env
diff --git a/example/ios/NativeModules/VideoRawDataNativeModule.h b/examples/expo/ios/NativeModules/VideoRawDataNativeModule.h
similarity index 100%
rename from example/ios/NativeModules/VideoRawDataNativeModule.h
rename to examples/expo/ios/NativeModules/VideoRawDataNativeModule.h
diff --git a/example/ios/NativeModules/VideoRawDataNativeModule.m b/examples/expo/ios/NativeModules/VideoRawDataNativeModule.m
similarity index 100%
rename from example/ios/NativeModules/VideoRawDataNativeModule.m
rename to examples/expo/ios/NativeModules/VideoRawDataNativeModule.m
diff --git a/examples/expo/ios/Podfile b/examples/expo/ios/Podfile
new file mode 100644
index 000000000..08c076214
--- /dev/null
+++ b/examples/expo/ios/Podfile
@@ -0,0 +1,70 @@
+require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
+require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
+
+require 'json'
+podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
+
+ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false'
+ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
+
+platform :ios, min_ios_version_supported
+install! 'cocoapods',
+ :deterministic_uuids => false
+
+prepare_react_native_project!
+
+target 'reactnativeagoraexampleexpo' do
+ use_expo_modules!
+
+ if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
+ config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
+ else
+ config_command = [
+ 'npx',
+ 'expo-modules-autolinking',
+ 'react-native-config',
+ '--json',
+ '--platform',
+ 'ios'
+ ]
+ end
+
+ config = use_native_modules!(config_command)
+
+ use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
+ use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
+
+ use_react_native!(
+ :path => config[:reactNativePath],
+ :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
+ # An absolute path to your application root.
+ :app_path => "#{Pod::Config.instance.installation_root}/..",
+ :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
+ )
+
+ post_install do |installer|
+ react_native_post_install(
+ installer,
+ config[:reactNativePath],
+ :mac_catalyst_enabled => false,
+ :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
+ )
+
+ # This is necessary for Xcode 14, because it signs resource bundles by default
+ # when building for devices.
+ installer.target_installation_results.pod_target_installation_results
+ .each do |pod_name, target_installation_result|
+ target_installation_result.resource_bundle_targets.each do |resource_bundle_target|
+ resource_bundle_target.build_configurations.each do |config|
+ config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
+ end
+ end
+ end
+ end
+end
+
+target 'ScreenShare' do
+ #dependencies start
+ pod 'AgoraRtcEngine_Special_iOS', '4.5.2.140'
+ #dependencies end
+end
diff --git a/examples/expo/ios/Podfile.lock b/examples/expo/ios/Podfile.lock
new file mode 100644
index 000000000..f58659f3e
--- /dev/null
+++ b/examples/expo/ios/Podfile.lock
@@ -0,0 +1,2805 @@
+PODS:
+ - AgoraIrisRTC_iOS (4.5.2.140-build.6)
+ - AgoraRtcEngine_Special_iOS (4.5.2.140)
+ - boost (1.84.0)
+ - DoubleConversion (1.1.6)
+ - EXConstants (17.1.7):
+ - ExpoModulesCore
+ - EXJSONUtils (0.15.0)
+ - EXManifests (0.16.6):
+ - ExpoModulesCore
+ - Expo (53.0.20):
+ - DoubleConversion
+ - ExpoModulesCore
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTAppDelegate
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactAppDependencyProvider
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-client (5.2.4):
+ - EXManifests
+ - expo-dev-launcher
+ - expo-dev-menu
+ - expo-dev-menu-interface
+ - EXUpdatesInterface
+ - expo-dev-launcher (5.1.16):
+ - DoubleConversion
+ - EXManifests
+ - expo-dev-launcher/Main (= 5.1.16)
+ - expo-dev-menu
+ - expo-dev-menu-interface
+ - ExpoModulesCore
+ - EXUpdatesInterface
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-NativeModulesApple
+ - React-RCTAppDelegate
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactAppDependencyProvider
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-launcher/Main (5.1.16):
+ - DoubleConversion
+ - EXManifests
+ - expo-dev-launcher/Unsafe
+ - expo-dev-menu
+ - expo-dev-menu-interface
+ - ExpoModulesCore
+ - EXUpdatesInterface
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-NativeModulesApple
+ - React-RCTAppDelegate
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactAppDependencyProvider
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-launcher/Unsafe (5.1.16):
+ - DoubleConversion
+ - EXManifests
+ - expo-dev-menu
+ - expo-dev-menu-interface
+ - ExpoModulesCore
+ - EXUpdatesInterface
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-NativeModulesApple
+ - React-RCTAppDelegate
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactAppDependencyProvider
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-menu (6.1.14):
+ - DoubleConversion
+ - expo-dev-menu/Main (= 6.1.14)
+ - expo-dev-menu/ReactNativeCompatibles (= 6.1.14)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-menu-interface (1.10.0)
+ - expo-dev-menu/Main (6.1.14):
+ - DoubleConversion
+ - EXManifests
+ - expo-dev-menu-interface
+ - expo-dev-menu/Vendored
+ - ExpoModulesCore
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactAppDependencyProvider
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-menu/ReactNativeCompatibles (6.1.14):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-menu/SafeAreaView (6.1.14):
+ - DoubleConversion
+ - ExpoModulesCore
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - expo-dev-menu/Vendored (6.1.14):
+ - DoubleConversion
+ - expo-dev-menu/SafeAreaView
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - ExpoAsset (11.1.7):
+ - ExpoModulesCore
+ - ExpoFileSystem (18.1.11):
+ - ExpoModulesCore
+ - ExpoFont (13.3.2):
+ - ExpoModulesCore
+ - ExpoHead (5.1.4):
+ - ExpoModulesCore
+ - ExpoKeepAwake (14.1.4):
+ - ExpoModulesCore
+ - ExpoLinking (7.1.7):
+ - ExpoModulesCore
+ - ExpoModulesCore (2.5.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - EXUpdatesInterface (1.1.0):
+ - ExpoModulesCore
+ - fast_float (6.1.4)
+ - FBLazyVector (0.79.5)
+ - fmt (11.0.2)
+ - glog (0.3.5)
+ - hermes-engine (0.79.5):
+ - hermes-engine/Pre-built (= 0.79.5)
+ - hermes-engine/Pre-built (0.79.5)
+ - RCT-Folly (2024.11.18.00):
+ - boost
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - RCT-Folly/Default (= 2024.11.18.00)
+ - RCT-Folly/Default (2024.11.18.00):
+ - boost
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - RCT-Folly/Fabric (2024.11.18.00):
+ - boost
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - RCTDeprecation (0.79.5)
+ - RCTRequired (0.79.5)
+ - RCTTypeSafety (0.79.5):
+ - FBLazyVector (= 0.79.5)
+ - RCTRequired (= 0.79.5)
+ - React-Core (= 0.79.5)
+ - React (0.79.5):
+ - React-Core (= 0.79.5)
+ - React-Core/DevSupport (= 0.79.5)
+ - React-Core/RCTWebSocket (= 0.79.5)
+ - React-RCTActionSheet (= 0.79.5)
+ - React-RCTAnimation (= 0.79.5)
+ - React-RCTBlob (= 0.79.5)
+ - React-RCTImage (= 0.79.5)
+ - React-RCTLinking (= 0.79.5)
+ - React-RCTNetwork (= 0.79.5)
+ - React-RCTSettings (= 0.79.5)
+ - React-RCTText (= 0.79.5)
+ - React-RCTVibration (= 0.79.5)
+ - React-callinvoker (0.79.5)
+ - React-Core (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default (= 0.79.5)
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/CoreModulesHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/Default (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/DevSupport (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default (= 0.79.5)
+ - React-Core/RCTWebSocket (= 0.79.5)
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTActionSheetHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTAnimationHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTBlobHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTImageHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTLinkingHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTNetworkHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTSettingsHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTTextHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTVibrationHeaders (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-Core/RCTWebSocket (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTDeprecation
+ - React-Core/Default (= 0.79.5)
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-perflogger
+ - React-runtimescheduler
+ - React-utils
+ - SocketRocket (= 0.7.1)
+ - Yoga
+ - React-CoreModules (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTTypeSafety (= 0.79.5)
+ - React-Core/CoreModulesHeaders (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-NativeModulesApple
+ - React-RCTBlob
+ - React-RCTFBReactNativeSpec
+ - React-RCTImage (= 0.79.5)
+ - ReactCommon
+ - SocketRocket (= 0.7.1)
+ - React-cxxreact (0.79.5):
+ - boost
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-callinvoker (= 0.79.5)
+ - React-debug (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-logger (= 0.79.5)
+ - React-perflogger (= 0.79.5)
+ - React-runtimeexecutor (= 0.79.5)
+ - React-timing (= 0.79.5)
+ - React-debug (0.79.5)
+ - React-defaultsnativemodule (0.79.5):
+ - hermes-engine
+ - RCT-Folly
+ - React-domnativemodule
+ - React-featureflagsnativemodule
+ - React-hermes
+ - React-idlecallbacksnativemodule
+ - React-jsi
+ - React-jsiexecutor
+ - React-microtasksnativemodule
+ - React-RCTFBReactNativeSpec
+ - React-domnativemodule (0.79.5):
+ - hermes-engine
+ - RCT-Folly
+ - React-Fabric
+ - React-FabricComponents
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-Fabric (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/animations (= 0.79.5)
+ - React-Fabric/attributedstring (= 0.79.5)
+ - React-Fabric/componentregistry (= 0.79.5)
+ - React-Fabric/componentregistrynative (= 0.79.5)
+ - React-Fabric/components (= 0.79.5)
+ - React-Fabric/consistency (= 0.79.5)
+ - React-Fabric/core (= 0.79.5)
+ - React-Fabric/dom (= 0.79.5)
+ - React-Fabric/imagemanager (= 0.79.5)
+ - React-Fabric/leakchecker (= 0.79.5)
+ - React-Fabric/mounting (= 0.79.5)
+ - React-Fabric/observers (= 0.79.5)
+ - React-Fabric/scheduler (= 0.79.5)
+ - React-Fabric/telemetry (= 0.79.5)
+ - React-Fabric/templateprocessor (= 0.79.5)
+ - React-Fabric/uimanager (= 0.79.5)
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/animations (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/attributedstring (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/componentregistry (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/componentregistrynative (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/components (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/components/legacyviewmanagerinterop (= 0.79.5)
+ - React-Fabric/components/root (= 0.79.5)
+ - React-Fabric/components/scrollview (= 0.79.5)
+ - React-Fabric/components/view (= 0.79.5)
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/components/legacyviewmanagerinterop (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/components/root (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/components/scrollview (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/components/view (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-renderercss
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-Fabric/consistency (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/core (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/dom (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/imagemanager (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/leakchecker (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/mounting (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/observers (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/observers/events (= 0.79.5)
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/observers/events (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/scheduler (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/observers/events
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-performancetimeline
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/telemetry (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/templateprocessor (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/uimanager (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric/uimanager/consistency (= 0.79.5)
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererconsistency
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-Fabric/uimanager/consistency (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererconsistency
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - React-FabricComponents (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-FabricComponents/components (= 0.79.5)
+ - React-FabricComponents/textlayoutmanager (= 0.79.5)
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-FabricComponents/components/inputaccessory (= 0.79.5)
+ - React-FabricComponents/components/iostextinput (= 0.79.5)
+ - React-FabricComponents/components/modal (= 0.79.5)
+ - React-FabricComponents/components/rncore (= 0.79.5)
+ - React-FabricComponents/components/safeareaview (= 0.79.5)
+ - React-FabricComponents/components/scrollview (= 0.79.5)
+ - React-FabricComponents/components/text (= 0.79.5)
+ - React-FabricComponents/components/textinput (= 0.79.5)
+ - React-FabricComponents/components/unimplementedview (= 0.79.5)
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/inputaccessory (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/iostextinput (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/modal (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/rncore (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/safeareaview (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/scrollview (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/text (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/textinput (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/components/unimplementedview (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricComponents/textlayoutmanager (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-cxxreact
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-logger
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-FabricImage (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - RCTRequired (= 0.79.5)
+ - RCTTypeSafety (= 0.79.5)
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsiexecutor (= 0.79.5)
+ - React-logger
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon
+ - Yoga
+ - React-featureflags (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - React-featureflagsnativemodule (0.79.5):
+ - hermes-engine
+ - RCT-Folly
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - ReactCommon/turbomodule/core
+ - React-graphics (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-utils
+ - React-hermes (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-cxxreact (= 0.79.5)
+ - React-jsi
+ - React-jsiexecutor (= 0.79.5)
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-perflogger (= 0.79.5)
+ - React-runtimeexecutor
+ - React-idlecallbacksnativemodule (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - React-runtimescheduler
+ - ReactCommon/turbomodule/core
+ - React-ImageManager (0.79.5):
+ - glog
+ - RCT-Folly/Fabric
+ - React-Core/Default
+ - React-debug
+ - React-Fabric
+ - React-graphics
+ - React-rendererdebug
+ - React-utils
+ - React-jserrorhandler (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-jsi
+ - ReactCommon/turbomodule/bridging
+ - React-jsi (0.79.5):
+ - boost
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-jsiexecutor (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-cxxreact (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-perflogger (= 0.79.5)
+ - React-jsinspector (0.79.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - React-featureflags
+ - React-jsi
+ - React-jsinspectortracing
+ - React-perflogger (= 0.79.5)
+ - React-runtimeexecutor (= 0.79.5)
+ - React-jsinspectortracing (0.79.5):
+ - RCT-Folly
+ - React-oscompat
+ - React-jsitooling (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - RCT-Folly (= 2024.11.18.00)
+ - React-cxxreact (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-jsitracing (0.79.5):
+ - React-jsi
+ - React-logger (0.79.5):
+ - glog
+ - React-Mapbuffer (0.79.5):
+ - glog
+ - React-debug
+ - React-microtasksnativemodule (0.79.5):
+ - hermes-engine
+ - RCT-Folly
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-RCTFBReactNativeSpec
+ - ReactCommon/turbomodule/core
+ - react-native-agora (4.5.3):
+ - AgoraIrisRTC_iOS (= 4.5.2.140-build.6)
+ - AgoraRtcEngine_Special_iOS (= 4.5.2.140)
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - react-native-image-tools (0.8.1):
+ - React
+ - react-native-safe-area-context (5.6.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - react-native-safe-area-context/common (= 5.6.0)
+ - react-native-safe-area-context/fabric (= 5.6.0)
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - react-native-safe-area-context/common (5.6.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - react-native-safe-area-context/fabric (5.6.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - react-native-safe-area-context/common
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - react-native-slider (4.5.7):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - react-native-slider/common (= 4.5.7)
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - react-native-slider/common (4.5.7):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - React-NativeModulesApple (0.79.5):
+ - glog
+ - hermes-engine
+ - React-callinvoker
+ - React-Core
+ - React-cxxreact
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsinspector
+ - React-runtimeexecutor
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - React-oscompat (0.79.5)
+ - React-perflogger (0.79.5):
+ - DoubleConversion
+ - RCT-Folly (= 2024.11.18.00)
+ - React-performancetimeline (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - React-cxxreact
+ - React-featureflags
+ - React-jsinspectortracing
+ - React-perflogger
+ - React-timing
+ - React-RCTActionSheet (0.79.5):
+ - React-Core/RCTActionSheetHeaders (= 0.79.5)
+ - React-RCTAnimation (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTTypeSafety
+ - React-Core/RCTAnimationHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - React-RCTAppDelegate (0.79.5):
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-CoreModules
+ - React-debug
+ - React-defaultsnativemodule
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsitooling
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTFBReactNativeSpec
+ - React-RCTImage
+ - React-RCTNetwork
+ - React-RCTRuntime
+ - React-rendererdebug
+ - React-RuntimeApple
+ - React-RuntimeCore
+ - React-runtimescheduler
+ - React-utils
+ - ReactCommon
+ - React-RCTBlob (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-Core/RCTBlobHeaders
+ - React-Core/RCTWebSocket
+ - React-jsi
+ - React-jsinspector
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - React-RCTNetwork
+ - ReactCommon
+ - React-RCTFabric (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-FabricComponents
+ - React-FabricImage
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-performancetimeline
+ - React-RCTAnimation
+ - React-RCTImage
+ - React-RCTText
+ - React-rendererconsistency
+ - React-renderercss
+ - React-rendererdebug
+ - React-runtimescheduler
+ - React-utils
+ - Yoga
+ - React-RCTFBReactNativeSpec (0.79.5):
+ - hermes-engine
+ - RCT-Folly
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-NativeModulesApple
+ - ReactCommon
+ - React-RCTImage (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTTypeSafety
+ - React-Core/RCTImageHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - React-RCTNetwork
+ - ReactCommon
+ - React-RCTLinking (0.79.5):
+ - React-Core/RCTLinkingHeaders (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - ReactCommon/turbomodule/core (= 0.79.5)
+ - React-RCTNetwork (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTTypeSafety
+ - React-Core/RCTNetworkHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - React-RCTRuntime (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-Core
+ - React-hermes
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-jsitooling
+ - React-RuntimeApple
+ - React-RuntimeCore
+ - React-RuntimeHermes
+ - React-RCTSettings (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTTypeSafety
+ - React-Core/RCTSettingsHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - React-RCTText (0.79.5):
+ - React-Core/RCTTextHeaders (= 0.79.5)
+ - Yoga
+ - React-RCTVibration (0.79.5):
+ - RCT-Folly (= 2024.11.18.00)
+ - React-Core/RCTVibrationHeaders
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFBReactNativeSpec
+ - ReactCommon
+ - React-rendererconsistency (0.79.5)
+ - React-renderercss (0.79.5):
+ - React-debug
+ - React-utils
+ - React-rendererdebug (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - RCT-Folly (= 2024.11.18.00)
+ - React-debug
+ - React-rncore (0.79.5)
+ - React-RuntimeApple (0.79.5):
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-callinvoker
+ - React-Core/Default
+ - React-CoreModules
+ - React-cxxreact
+ - React-featureflags
+ - React-jserrorhandler
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-Mapbuffer
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTFBReactNativeSpec
+ - React-RuntimeCore
+ - React-runtimeexecutor
+ - React-RuntimeHermes
+ - React-runtimescheduler
+ - React-utils
+ - React-RuntimeCore (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-cxxreact
+ - React-Fabric
+ - React-featureflags
+ - React-hermes
+ - React-jserrorhandler
+ - React-jsi
+ - React-jsiexecutor
+ - React-jsinspector
+ - React-jsitooling
+ - React-performancetimeline
+ - React-runtimeexecutor
+ - React-runtimescheduler
+ - React-utils
+ - React-runtimeexecutor (0.79.5):
+ - React-jsi (= 0.79.5)
+ - React-RuntimeHermes (0.79.5):
+ - hermes-engine
+ - RCT-Folly/Fabric (= 2024.11.18.00)
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsinspector
+ - React-jsinspectortracing
+ - React-jsitooling
+ - React-jsitracing
+ - React-RuntimeCore
+ - React-utils
+ - React-runtimescheduler (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-callinvoker
+ - React-cxxreact
+ - React-debug
+ - React-featureflags
+ - React-hermes
+ - React-jsi
+ - React-jsinspectortracing
+ - React-performancetimeline
+ - React-rendererconsistency
+ - React-rendererdebug
+ - React-runtimeexecutor
+ - React-timing
+ - React-utils
+ - React-timing (0.79.5)
+ - React-utils (0.79.5):
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-debug
+ - React-hermes
+ - React-jsi (= 0.79.5)
+ - ReactAppDependencyProvider (0.79.5):
+ - ReactCodegen
+ - ReactCodegen (0.79.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-FabricImage
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-jsi
+ - React-jsiexecutor
+ - React-NativeModulesApple
+ - React-RCTAppDelegate
+ - React-rendererdebug
+ - React-utils
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - ReactCommon (0.79.5):
+ - ReactCommon/turbomodule (= 0.79.5)
+ - ReactCommon/turbomodule (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-callinvoker (= 0.79.5)
+ - React-cxxreact (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-logger (= 0.79.5)
+ - React-perflogger (= 0.79.5)
+ - ReactCommon/turbomodule/bridging (= 0.79.5)
+ - ReactCommon/turbomodule/core (= 0.79.5)
+ - ReactCommon/turbomodule/bridging (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-callinvoker (= 0.79.5)
+ - React-cxxreact (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-logger (= 0.79.5)
+ - React-perflogger (= 0.79.5)
+ - ReactCommon/turbomodule/core (0.79.5):
+ - DoubleConversion
+ - fast_float (= 6.1.4)
+ - fmt (= 11.0.2)
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - React-callinvoker (= 0.79.5)
+ - React-cxxreact (= 0.79.5)
+ - React-debug (= 0.79.5)
+ - React-featureflags (= 0.79.5)
+ - React-jsi (= 0.79.5)
+ - React-logger (= 0.79.5)
+ - React-perflogger (= 0.79.5)
+ - React-utils (= 0.79.5)
+ - RNCPicker (2.11.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - RNFS (2.20.0):
+ - React-Core
+ - RNGestureHandler (2.28.0):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - RNReanimated (3.17.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNReanimated/reanimated (= 3.17.5)
+ - RNReanimated/worklets (= 3.17.5)
+ - Yoga
+ - RNReanimated/reanimated (3.17.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNReanimated/reanimated/apple (= 3.17.5)
+ - Yoga
+ - RNReanimated/reanimated/apple (3.17.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - RNReanimated/worklets (3.17.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNReanimated/worklets/apple (= 3.17.5)
+ - Yoga
+ - RNReanimated/worklets/apple (3.17.5):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - RNScreens (4.11.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTImage
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNScreens/common (= 4.11.1)
+ - Yoga
+ - RNScreens/common (4.11.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-RCTImage
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - RNSVG (15.12.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - RNSVG/common (= 15.12.1)
+ - Yoga
+ - RNSVG/common (15.12.1):
+ - DoubleConversion
+ - glog
+ - hermes-engine
+ - RCT-Folly (= 2024.11.18.00)
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - Yoga
+ - RNVectorIcons (9.2.0):
+ - React-Core
+ - SocketRocket (0.7.1)
+ - Yoga (0.0.0)
+
+DEPENDENCIES:
+ - AgoraRtcEngine_Special_iOS (= 4.5.2.140)
+ - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
+ - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
+ - EXConstants (from `../node_modules/expo-constants/ios`)
+ - EXJSONUtils (from `../node_modules/expo-json-utils/ios`)
+ - EXManifests (from `../node_modules/expo-manifests/ios`)
+ - Expo (from `../node_modules/expo`)
+ - expo-dev-client (from `../node_modules/expo-dev-client/ios`)
+ - expo-dev-launcher (from `../node_modules/expo-dev-launcher`)
+ - expo-dev-menu (from `../node_modules/expo-dev-menu`)
+ - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`)
+ - ExpoAsset (from `../node_modules/expo-asset/ios`)
+ - ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
+ - ExpoFont (from `../node_modules/expo-font/ios`)
+ - ExpoHead (from `../node_modules/expo-router/ios`)
+ - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
+ - ExpoLinking (from `../node_modules/expo-linking/ios`)
+ - ExpoModulesCore (from `../node_modules/expo-modules-core`)
+ - EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`)
+ - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
+ - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
+ - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
+ - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
+ - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
+ - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
+ - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
+ - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
+ - RCTRequired (from `../node_modules/react-native/Libraries/Required`)
+ - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
+ - React (from `../node_modules/react-native/`)
+ - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
+ - React-Core (from `../node_modules/react-native/`)
+ - React-Core/RCTWebSocket (from `../node_modules/react-native/`)
+ - React-CoreModules (from `../node_modules/react-native/React/CoreModules`)
+ - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`)
+ - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`)
+ - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`)
+ - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`)
+ - React-Fabric (from `../node_modules/react-native/ReactCommon`)
+ - React-FabricComponents (from `../node_modules/react-native/ReactCommon`)
+ - React-FabricImage (from `../node_modules/react-native/ReactCommon`)
+ - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`)
+ - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`)
+ - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`)
+ - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`)
+ - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`)
+ - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`)
+ - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`)
+ - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
+ - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
+ - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`)
+ - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`)
+ - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`)
+ - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`)
+ - React-logger (from `../node_modules/react-native/ReactCommon/logger`)
+ - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
+ - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
+ - react-native-agora (from `../../..`)
+ - react-native-image-tools (from `../node_modules/react-native-image-tool`)
+ - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
+ - "react-native-slider (from `../node_modules/@react-native-community/slider`)"
+ - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
+ - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
+ - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
+ - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`)
+ - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
+ - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
+ - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`)
+ - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`)
+ - React-RCTFabric (from `../node_modules/react-native/React`)
+ - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`)
+ - React-RCTImage (from `../node_modules/react-native/Libraries/Image`)
+ - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`)
+ - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`)
+ - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`)
+ - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`)
+ - React-RCTText (from `../node_modules/react-native/Libraries/Text`)
+ - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
+ - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`)
+ - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`)
+ - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`)
+ - React-rncore (from `../node_modules/react-native/ReactCommon`)
+ - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`)
+ - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`)
+ - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
+ - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`)
+ - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
+ - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`)
+ - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
+ - ReactAppDependencyProvider (from `build/generated/ios`)
+ - ReactCodegen (from `build/generated/ios`)
+ - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
+ - "RNCPicker (from `../node_modules/@react-native-picker/picker`)"
+ - RNFS (from `../node_modules/react-native-fs`)
+ - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
+ - RNReanimated (from `../node_modules/react-native-reanimated`)
+ - RNScreens (from `../node_modules/react-native-screens`)
+ - RNSVG (from `../node_modules/react-native-svg`)
+ - RNVectorIcons (from `../node_modules/react-native-vector-icons`)
+ - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
+
+SPEC REPOS:
+ trunk:
+ - AgoraIrisRTC_iOS
+ - AgoraRtcEngine_Special_iOS
+ - SocketRocket
+
+EXTERNAL SOURCES:
+ boost:
+ :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
+ DoubleConversion:
+ :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
+ EXConstants:
+ :path: "../node_modules/expo-constants/ios"
+ EXJSONUtils:
+ :path: "../node_modules/expo-json-utils/ios"
+ EXManifests:
+ :path: "../node_modules/expo-manifests/ios"
+ Expo:
+ :path: "../node_modules/expo"
+ expo-dev-client:
+ :path: "../node_modules/expo-dev-client/ios"
+ expo-dev-launcher:
+ :path: "../node_modules/expo-dev-launcher"
+ expo-dev-menu:
+ :path: "../node_modules/expo-dev-menu"
+ expo-dev-menu-interface:
+ :path: "../node_modules/expo-dev-menu-interface/ios"
+ ExpoAsset:
+ :path: "../node_modules/expo-asset/ios"
+ ExpoFileSystem:
+ :path: "../node_modules/expo-file-system/ios"
+ ExpoFont:
+ :path: "../node_modules/expo-font/ios"
+ ExpoHead:
+ :path: "../node_modules/expo-router/ios"
+ ExpoKeepAwake:
+ :path: "../node_modules/expo-keep-awake/ios"
+ ExpoLinking:
+ :path: "../node_modules/expo-linking/ios"
+ ExpoModulesCore:
+ :path: "../node_modules/expo-modules-core"
+ EXUpdatesInterface:
+ :path: "../node_modules/expo-updates-interface/ios"
+ fast_float:
+ :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
+ FBLazyVector:
+ :path: "../node_modules/react-native/Libraries/FBLazyVector"
+ fmt:
+ :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
+ glog:
+ :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
+ hermes-engine:
+ :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
+ :tag: hermes-2025-06-04-RNv0.79.3-7f9a871eefeb2c3852365ee80f0b6733ec12ac3b
+ RCT-Folly:
+ :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
+ RCTDeprecation:
+ :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation"
+ RCTRequired:
+ :path: "../node_modules/react-native/Libraries/Required"
+ RCTTypeSafety:
+ :path: "../node_modules/react-native/Libraries/TypeSafety"
+ React:
+ :path: "../node_modules/react-native/"
+ React-callinvoker:
+ :path: "../node_modules/react-native/ReactCommon/callinvoker"
+ React-Core:
+ :path: "../node_modules/react-native/"
+ React-CoreModules:
+ :path: "../node_modules/react-native/React/CoreModules"
+ React-cxxreact:
+ :path: "../node_modules/react-native/ReactCommon/cxxreact"
+ React-debug:
+ :path: "../node_modules/react-native/ReactCommon/react/debug"
+ React-defaultsnativemodule:
+ :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults"
+ React-domnativemodule:
+ :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom"
+ React-Fabric:
+ :path: "../node_modules/react-native/ReactCommon"
+ React-FabricComponents:
+ :path: "../node_modules/react-native/ReactCommon"
+ React-FabricImage:
+ :path: "../node_modules/react-native/ReactCommon"
+ React-featureflags:
+ :path: "../node_modules/react-native/ReactCommon/react/featureflags"
+ React-featureflagsnativemodule:
+ :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags"
+ React-graphics:
+ :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics"
+ React-hermes:
+ :path: "../node_modules/react-native/ReactCommon/hermes"
+ React-idlecallbacksnativemodule:
+ :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks"
+ React-ImageManager:
+ :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios"
+ React-jserrorhandler:
+ :path: "../node_modules/react-native/ReactCommon/jserrorhandler"
+ React-jsi:
+ :path: "../node_modules/react-native/ReactCommon/jsi"
+ React-jsiexecutor:
+ :path: "../node_modules/react-native/ReactCommon/jsiexecutor"
+ React-jsinspector:
+ :path: "../node_modules/react-native/ReactCommon/jsinspector-modern"
+ React-jsinspectortracing:
+ :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing"
+ React-jsitooling:
+ :path: "../node_modules/react-native/ReactCommon/jsitooling"
+ React-jsitracing:
+ :path: "../node_modules/react-native/ReactCommon/hermes/executor/"
+ React-logger:
+ :path: "../node_modules/react-native/ReactCommon/logger"
+ React-Mapbuffer:
+ :path: "../node_modules/react-native/ReactCommon"
+ React-microtasksnativemodule:
+ :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
+ react-native-agora:
+ :path: "../../.."
+ react-native-image-tools:
+ :path: "../node_modules/react-native-image-tool"
+ react-native-safe-area-context:
+ :path: "../node_modules/react-native-safe-area-context"
+ react-native-slider:
+ :path: "../node_modules/@react-native-community/slider"
+ React-NativeModulesApple:
+ :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
+ React-oscompat:
+ :path: "../node_modules/react-native/ReactCommon/oscompat"
+ React-perflogger:
+ :path: "../node_modules/react-native/ReactCommon/reactperflogger"
+ React-performancetimeline:
+ :path: "../node_modules/react-native/ReactCommon/react/performance/timeline"
+ React-RCTActionSheet:
+ :path: "../node_modules/react-native/Libraries/ActionSheetIOS"
+ React-RCTAnimation:
+ :path: "../node_modules/react-native/Libraries/NativeAnimation"
+ React-RCTAppDelegate:
+ :path: "../node_modules/react-native/Libraries/AppDelegate"
+ React-RCTBlob:
+ :path: "../node_modules/react-native/Libraries/Blob"
+ React-RCTFabric:
+ :path: "../node_modules/react-native/React"
+ React-RCTFBReactNativeSpec:
+ :path: "../node_modules/react-native/React"
+ React-RCTImage:
+ :path: "../node_modules/react-native/Libraries/Image"
+ React-RCTLinking:
+ :path: "../node_modules/react-native/Libraries/LinkingIOS"
+ React-RCTNetwork:
+ :path: "../node_modules/react-native/Libraries/Network"
+ React-RCTRuntime:
+ :path: "../node_modules/react-native/React/Runtime"
+ React-RCTSettings:
+ :path: "../node_modules/react-native/Libraries/Settings"
+ React-RCTText:
+ :path: "../node_modules/react-native/Libraries/Text"
+ React-RCTVibration:
+ :path: "../node_modules/react-native/Libraries/Vibration"
+ React-rendererconsistency:
+ :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency"
+ React-renderercss:
+ :path: "../node_modules/react-native/ReactCommon/react/renderer/css"
+ React-rendererdebug:
+ :path: "../node_modules/react-native/ReactCommon/react/renderer/debug"
+ React-rncore:
+ :path: "../node_modules/react-native/ReactCommon"
+ React-RuntimeApple:
+ :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios"
+ React-RuntimeCore:
+ :path: "../node_modules/react-native/ReactCommon/react/runtime"
+ React-runtimeexecutor:
+ :path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
+ React-RuntimeHermes:
+ :path: "../node_modules/react-native/ReactCommon/react/runtime"
+ React-runtimescheduler:
+ :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler"
+ React-timing:
+ :path: "../node_modules/react-native/ReactCommon/react/timing"
+ React-utils:
+ :path: "../node_modules/react-native/ReactCommon/react/utils"
+ ReactAppDependencyProvider:
+ :path: build/generated/ios
+ ReactCodegen:
+ :path: build/generated/ios
+ ReactCommon:
+ :path: "../node_modules/react-native/ReactCommon"
+ RNCPicker:
+ :path: "../node_modules/@react-native-picker/picker"
+ RNFS:
+ :path: "../node_modules/react-native-fs"
+ RNGestureHandler:
+ :path: "../node_modules/react-native-gesture-handler"
+ RNReanimated:
+ :path: "../node_modules/react-native-reanimated"
+ RNScreens:
+ :path: "../node_modules/react-native-screens"
+ RNSVG:
+ :path: "../node_modules/react-native-svg"
+ RNVectorIcons:
+ :path: "../node_modules/react-native-vector-icons"
+ Yoga:
+ :path: "../node_modules/react-native/ReactCommon/yoga"
+
+SPEC CHECKSUMS:
+ AgoraIrisRTC_iOS: 41a033327414eeea7a8666e69aa7cd2530b70a76
+ AgoraRtcEngine_Special_iOS: aa5c57a16e9b43734f38da4c6105a15d55c33873
+ boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
+ DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
+ EXConstants: 9d62a46a36eae6d28cb978efcbc68aef354d1704
+ EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd
+ EXManifests: f4cc4a62ee4f1c8a9cf2bb79d325eac6cb9f5684
+ Expo: 666a397fcb608d72b019e16ba139b74e93e0b7d7
+ expo-dev-client: f1b99dfea0c9174d2e4ec96c2c5461587dda1e86
+ expo-dev-launcher: 27e8eba58d52b2f471b0c4001b0535a1c0b5610d
+ expo-dev-menu: 2868212810f6651bc5c30e72c636ef64de31ec6b
+ expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e
+ ExpoAsset: 7bdbbacf4e6752ae6e3cf70555cee076f6229e6e
+ ExpoFileSystem: 9681caebda23fa1b38a12a9c68b2bade7072ce20
+ ExpoFont: 091a47eeaa1b30b0b760aa1d0a2e7814e8bf6fe6
+ ExpoHead: 7c1893efc8dc79570bdcbdadce175723f4c037ec
+ ExpoKeepAwake: e8dedc115d9f6f24b153ccd2d1d8efcdfd68a527
+ ExpoLinking: 343a89ea864a851831fd4495e8aea01cf0f6a36f
+ ExpoModulesCore: 16f74d8df26d7e4b6bf7eb3d0effaf0f15df7b80
+ EXUpdatesInterface: 64f35449b8ef89ce08cdd8952a4d119b5de6821d
+ fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
+ FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52
+ fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
+ glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
+ hermes-engine: f03b0e06d3882d71e67e45b073bb827da1a21aae
+ RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
+ RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5
+ RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8
+ RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab
+ React: 6393ae1807614f017a84805bf2417e3497f518a6
+ React-callinvoker: c34f666f551f05a325b87e7e3e6df0e082fa3d99
+ React-Core: fc07a4b69a963880b25142c51178f4cb75628c7d
+ React-CoreModules: 94d39315cfa791f6c477712fea47c34f8ecb26c6
+ React-cxxreact: 628c28cdb3fdef93ee3bfc2bec8e2d776e81ae49
+ React-debug: a951cdb698321d78ebd955fc8788ebbe51af3519
+ React-defaultsnativemodule: 08779733c4541be5da1f1d3ec8492300dbc3c00a
+ React-domnativemodule: fdd4821b9a0c44e87ed9263231225aa65fe982e0
+ React-Fabric: 8d905d8c41d666bf283a5b09db56bdaccfa07c8d
+ React-FabricComponents: 43aab5c94c7b5bbcabc3a9821b8536a0711a0f01
+ React-FabricImage: 10708fa449d3f1b4a8d6eedb97f0c6476b098bb4
+ React-featureflags: 32d776f9bef34bdab6218ad99db535e75e5c1f4e
+ React-featureflagsnativemodule: 413da7bc0d21aa86315dbea0fb2b2c27cb8b4bab
+ React-graphics: 83c676b633acc5044b5c5dfdb7f95aa3aaf7b7a5
+ React-hermes: af1b3d79491295abc9d1b11f84e77d5dc00095b6
+ React-idlecallbacksnativemodule: b039a595f29d9a87bbad12e731de45879a054b33
+ React-ImageManager: 81dc38602ff1e7a8fd5fe3bf54772cf1a30d49c1
+ React-jserrorhandler: b230f573b63a6a2a5540054d46cfb6087d26c86c
+ React-jsi: e9c3019e00db5d144e0a660616a52a605e12c39a
+ React-jsiexecutor: 3ed70a394b76f33e6c4ec4b382a457df7309d96c
+ React-jsinspector: 977527f0224edb5ae0970e946411f36dd1d70f43
+ React-jsinspectortracing: 64ec4bde979134830c8f937758416f8d50daa8fb
+ React-jsitooling: 9dd45534fd158b508f785b547bf1350933bf465a
+ React-jsitracing: a645b2b3c4f6aa79051d5485c67b188ef49045a0
+ React-logger: e6e6164f1753e46d1b7e2c8f0949cd7937eaf31b
+ React-Mapbuffer: 5b4959cbd91e7e8fae42ab0f4b7c25b86fd139a1
+ React-microtasksnativemodule: 1695ab137281dd03de967b7bbeb4e392601f6432
+ react-native-agora: d7979a8f866a05d36b753146653217af35043a61
+ react-native-image-tools: 88218449791389bbf550a2c475a3b564c8233c8b
+ react-native-safe-area-context: 6863f9e225b541b481514b0f6d51be0867184c2c
+ react-native-slider: 351d1186b07d686b93dad14ce2b474ca62dea0fc
+ React-NativeModulesApple: 3ecc647742d33ad617bd2805902e3f91f2b3008f
+ React-oscompat: 0592889a9fcf0eacb205532028e4a364e22907dd
+ React-perflogger: 634408a9a0f5753faa577dfa81bc009edca01062
+ React-performancetimeline: faa22f963845ae2298c28ef6b84bd8b58d3d8a90
+ React-RCTActionSheet: ce67bdc050cc1d9ef673c7a93e9799288a183f24
+ React-RCTAnimation: 12193c2092a78012c7f77457806dcc822cc40d2c
+ React-RCTAppDelegate: 7225b51d5b6d3ddd3702165d717a1ffd4a90fb71
+ React-RCTBlob: 923cf9b0098b9a641cb1e454c30a444d9d3cda70
+ React-RCTFabric: a280fd9f2697c144b0d835200080a09ab15b2e07
+ React-RCTFBReactNativeSpec: 50eabdca1efbf6ce1d774b816a68e6cc4b2a5598
+ React-RCTImage: 580a5d0a6fdf9b69629d0582e5fb5a173e152099
+ React-RCTLinking: 4ed7c5667709099bfd6b2b6246b1dfd79c89f7cb
+ React-RCTNetwork: 06a22dd0088392694df4fd098634811aa0b3e166
+ React-RCTRuntime: 17c77bab5d39bc354c9983f8f11c7d3597fa8344
+ React-RCTSettings: 9dbf433f302c8ebe43b280453e74624098fbc706
+ React-RCTText: 92fcd78d6c44dbe64d147bb63f53698bcba7c971
+ React-RCTVibration: 513659394c92491e6c749e981424f6e1e0abdb3c
+ React-rendererconsistency: aedf87f8509bc0936ae5475d4ea1e26cb5e8def6
+ React-renderercss: 71727bedda678e0918506749f94f745e1050a080
+ React-rendererdebug: 81a6b97bd089b49a8e7f4f5c7fd1de588c0e8a11
+ React-rncore: 3eb6f7bdfd181bc26f9f3edc87f70eb1a68a2f3c
+ React-RuntimeApple: 368e8e7b0018f9e9ca4294a6a8167e6aebc6eb87
+ React-RuntimeCore: 0f9a8bb41e043f3adaea111e5128801af0dfbc34
+ React-runtimeexecutor: ebfd71307b3166c73ac0c441c1ea42e0f17f821d
+ React-RuntimeHermes: 7f55a7285794023ccb3cfe3e89c66c632ed566b1
+ React-runtimescheduler: 316243b204bb6a5fd80cea7a97df9b1614ee1b0e
+ React-timing: acc3fa92c72dcc1de6300d752ebb84a1d55dc809
+ React-utils: 4efa98c1c602f5eacac3cece396c0b7c7d70c1d3
+ ReactAppDependencyProvider: c42e7abdd2228ae583bdabc3dcd8e5cda6bef944
+ ReactCodegen: 4d001cd4fa72b876bbff500bbb3811e458bb3c72
+ ReactCommon: 41137f7e87cf7fd1c041a7124dfa3d0d48aa43f3
+ RNCPicker: 620d3d6cad22e5279fbcb365f58b3f5da1935198
+ RNFS: 4ac0f0ea233904cb798630b3c077808c06931688
+ RNGestureHandler: f8d0f8c032ba1209eb370bcc29e1138788208673
+ RNReanimated: 8b24b49fc13fce9a6e1729ccff645a63d2b7a6d1
+ RNScreens: c2e3cc506212228c607b4785b315205e28acbf0f
+ RNSVG: ab2249cc665e5d0b2d30657a766a86c99a649a65
+ RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
+ SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
+ Yoga: 357d74ecf46ef5be0234a0a2b08a9f728b37d93f
+
+PODFILE CHECKSUM: 851ca2817530080bcee3fa81d8aaa6a2e1694a55
+
+COCOAPODS: 1.16.2
diff --git a/examples/expo/ios/Podfile.properties.json b/examples/expo/ios/Podfile.properties.json
new file mode 100644
index 000000000..5aa6ae183
--- /dev/null
+++ b/examples/expo/ios/Podfile.properties.json
@@ -0,0 +1,6 @@
+{
+ "expo.jsEngine": "hermes",
+ "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true",
+ "newArchEnabled": "true",
+ "ios.deploymentTarget": "15.1"
+}
diff --git a/example/ios/Resources/agora-logo.png b/examples/expo/ios/Resources/agora-logo.png
similarity index 100%
rename from example/ios/Resources/agora-logo.png
rename to examples/expo/ios/Resources/agora-logo.png
diff --git a/example/ios/Resources/dang.mp3 b/examples/expo/ios/Resources/dang.mp3
similarity index 100%
rename from example/ios/Resources/dang.mp3
rename to examples/expo/ios/Resources/dang.mp3
diff --git a/example/ios/Resources/ding.mp3 b/examples/expo/ios/Resources/ding.mp3
similarity index 100%
rename from example/ios/Resources/ding.mp3
rename to examples/expo/ios/Resources/ding.mp3
diff --git a/example/ios/Resources/effect.mp3 b/examples/expo/ios/Resources/effect.mp3
similarity index 100%
rename from example/ios/Resources/effect.mp3
rename to examples/expo/ios/Resources/effect.mp3
diff --git a/example/ios/ScreenShare/Info.plist b/examples/expo/ios/ScreenShare/Info.plist
similarity index 100%
rename from example/ios/ScreenShare/Info.plist
rename to examples/expo/ios/ScreenShare/Info.plist
diff --git a/examples/expo/ios/ScreenShare/SampleHandler.h b/examples/expo/ios/ScreenShare/SampleHandler.h
new file mode 100644
index 000000000..0e533c957
--- /dev/null
+++ b/examples/expo/ios/ScreenShare/SampleHandler.h
@@ -0,0 +1,12 @@
+//
+// SampleHandler.h
+// ScreenShare
+//
+// Created by guoxianzhe on 2025/8/18.
+//
+
+#import
+
+@interface SampleHandler : RPBroadcastSampleHandler
+
+@end
diff --git a/examples/expo/ios/ScreenShare/SampleHandler.m b/examples/expo/ios/ScreenShare/SampleHandler.m
new file mode 100644
index 000000000..87cf1d325
--- /dev/null
+++ b/examples/expo/ios/ScreenShare/SampleHandler.m
@@ -0,0 +1,47 @@
+//
+// SampleHandler.m
+// ScreenShare
+//
+// Created by guoxianzhe on 2025/8/18.
+//
+
+
+#import "SampleHandler.h"
+
+@implementation SampleHandler
+
+- (void)broadcastStartedWithSetupInfo:(NSDictionary *)setupInfo {
+ // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
+}
+
+- (void)broadcastPaused {
+ // User has requested to pause the broadcast. Samples will stop being delivered.
+}
+
+- (void)broadcastResumed {
+ // User has requested to resume the broadcast. Samples delivery will resume.
+}
+
+- (void)broadcastFinished {
+ // User has requested to finish the broadcast.
+}
+
+- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
+
+ switch (sampleBufferType) {
+ case RPSampleBufferTypeVideo:
+ // Handle video sample buffer
+ break;
+ case RPSampleBufferTypeAudioApp:
+ // Handle audio sample buffer for app audio
+ break;
+ case RPSampleBufferTypeAudioMic:
+ // Handle audio sample buffer for mic audio
+ break;
+
+ default:
+ break;
+ }
+}
+
+@end
diff --git a/examples/expo/ios/reactnativeagoraexampleexpo.xcodeproj/project.pbxproj b/examples/expo/ios/reactnativeagoraexampleexpo.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..e54225456
--- /dev/null
+++ b/examples/expo/ios/reactnativeagoraexampleexpo.xcodeproj/project.pbxproj
@@ -0,0 +1,895 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
+ 33AD1F429730C5A564D044C1 /* libPods-ScreenShare.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 99654AB28D272EBD2C97BA21 /* libPods-ScreenShare.a */; };
+ 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
+ 62B8C0AB79D11D7A26F5E0A4 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 845E3E35A7D6C63406176706 /* PrivacyInfo.xcprivacy */; };
+ 78477FA152C1B174C3EA039C /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A90F64417D32ABC2ED788171 /* ExpoModulesProvider.swift */; };
+ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
+ BB64E1FB2E55B6F8005341DF /* SampleHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = BB64E1F92E55B6F8005341DF /* SampleHandler.m */; };
+ BBDB174B2E533AF300733525 /* dang.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = BBDB17432E533AF300733525 /* dang.mp3 */; };
+ BBDB174D2E533AF300733525 /* agora-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = BBDB17422E533AF300733525 /* agora-logo.png */; };
+ BBDB174E2E533AF300733525 /* effect.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = BBDB17452E533AF300733525 /* effect.mp3 */; };
+ BBDB174F2E533AF300733525 /* ding.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = BBDB17442E533AF300733525 /* ding.mp3 */; };
+ BBDB17512E533AF300733525 /* VideoRawDataNativeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = BBDB17402E533AF300733525 /* VideoRawDataNativeModule.m */; };
+ BBDB17582E533B6200733525 /* ReplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBDB17572E533B6200733525 /* ReplayKit.framework */; };
+ BBDB17602E533B6200733525 /* ScreenShare.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = BBDB17562E533B6200733525 /* ScreenShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ BBDB17672E533BDB00733525 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBDB17662E533BDB00733525 /* WebKit.framework */; };
+ C288BB9EBCAE3B80D447F97A /* libPods-reactnativeagoraexampleexpo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7014BD0ABABEF39F5BF1E57D /* libPods-reactnativeagoraexampleexpo.a */; };
+ F11748422D0307B40044C1D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11748412D0307B40044C1D9 /* AppDelegate.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ BBDB175E2E533B6200733525 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = BBDB17552E533B6200733525;
+ remoteInfo = ScreenShare;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ BBDB17652E533B6300733525 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ BBDB17602E533B6200733525 /* ScreenShare.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0F906FCF67ACACD83EE5D713 /* Pods-reactnativeagoraexampleexpo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-reactnativeagoraexampleexpo.release.xcconfig"; path = "Target Support Files/Pods-reactnativeagoraexampleexpo/Pods-reactnativeagoraexampleexpo.release.xcconfig"; sourceTree = ""; };
+ 13B07F961A680F5B00A75B9A /* reactnativeagoraexampleexpo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = reactnativeagoraexampleexpo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = reactnativeagoraexampleexpo/Images.xcassets; sourceTree = ""; };
+ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = reactnativeagoraexampleexpo/Info.plist; sourceTree = ""; };
+ 3C9B9AAF9A1997FD91FF51C7 /* Pods-ScreenShare.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ScreenShare.release.xcconfig"; path = "Target Support Files/Pods-ScreenShare/Pods-ScreenShare.release.xcconfig"; sourceTree = ""; };
+ 7014BD0ABABEF39F5BF1E57D /* libPods-reactnativeagoraexampleexpo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-reactnativeagoraexampleexpo.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 845E3E35A7D6C63406176706 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = reactnativeagoraexampleexpo/PrivacyInfo.xcprivacy; sourceTree = ""; };
+ 9877CFF4C2E145046AB20B52 /* Pods-ScreenShare.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ScreenShare.debug.xcconfig"; path = "Target Support Files/Pods-ScreenShare/Pods-ScreenShare.debug.xcconfig"; sourceTree = ""; };
+ 99654AB28D272EBD2C97BA21 /* libPods-ScreenShare.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ScreenShare.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+ A90F64417D32ABC2ED788171 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-reactnativeagoraexampleexpo/ExpoModulesProvider.swift"; sourceTree = ""; };
+ AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = reactnativeagoraexampleexpo/SplashScreen.storyboard; sourceTree = ""; };
+ BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "