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 ? ( +