From c9f8ae2dbc0f5ab3d0fd81067d836d1ac8abdaa0 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Mon, 16 Mar 2026 21:20:21 +0800 Subject: [PATCH 1/5] docs: sync README App ID config and fix ARCHITECTURE annotation - Update App ID configuration in all README files from string-configs.xml to local.properties (AGORA_APP_ID/AGORA_APP_CERT), aligned with overseas repo - Remove ByteDance beauty section from APIExample README (no longer in use) - Fix ARCHITECTURE.md: audio/ examples group annotation from BASIC to ADVANCED - Fix PictureInPicture.kt indentation in APIExample-Compose --- Android/AGENTS.md | 30 + .../.agent/skills/query-cases/SKILL.md | 114 ++++ .../.agent/skills/review-case/SKILL.md | 57 ++ .../.agent/skills/upsert-case/SKILL.md | 231 +++++++ .../references/fragment-template.java | 209 ++++++ Android/APIExample-Audio/AGENTS.md | 39 ++ Android/APIExample-Audio/ARCHITECTURE.md | 140 +++++ Android/APIExample-Audio/CLAUDE.md | 5 + Android/APIExample-Audio/README.md | 15 +- Android/APIExample-Audio/README.zh.md | 16 +- Android/APIExample-Audio/app/build.gradle | 14 + .../api/example/common/BaseFragment.java | 9 + .../examples/advanced/PlayAudioFiles.java | 4 +- .../examples/advanced/PreCallTest.java | 4 +- .../advanced/ProcessAudioRawData.java | 4 +- .../examples/advanced/RhythmPlayer.java | 4 +- .../examples/advanced/SpatialSound.java | 4 +- .../examples/advanced/VoiceEffects.java | 4 +- .../customaudio/CustomAudioRender.java | 4 +- .../customaudio/CustomAudioSource.java | 4 +- .../example/examples/audio/AudioWaveform.java | 4 +- .../examples/basic/JoinChannelAudio.java | 4 +- .../agora/api/example/utils/AgoraConfig.java | 22 + .../agora/api/example/utils/TokenUtils.java | 7 +- .../src/main/res/values/string_configs.xml | 41 -- Android/APIExample-Audio/ci.env.py | 37 +- Android/APIExample-Audio/cloud_build.sh | 20 +- .../.agent/skills/query-cases/SKILL.md | 117 ++++ .../.agent/skills/review-case/SKILL.md | 48 ++ .../.agent/skills/upsert-case/SKILL.md | 185 ++++++ .../references/composable-template.kt | 161 +++++ Android/APIExample-Compose/AGENTS.md | 38 ++ Android/APIExample-Compose/ARCHITECTURE.md | 197 ++++++ Android/APIExample-Compose/CLAUDE.md | 5 + Android/APIExample-Compose/README.md | 7 +- Android/APIExample-Compose/README.zh.md | 7 +- .../compose/samples/ChannelEncryption.kt | 4 +- .../compose/samples/CustomAudioRender.kt | 4 +- .../compose/samples/CustomAudioSource.kt | 4 +- .../compose/samples/CustomVideoRender.kt | 4 +- .../compose/samples/CustomVideoSource.kt | 4 +- .../compose/samples/HostAcrossChannel.kt | 4 +- .../compose/samples/JoinChannelAudio.kt | 4 +- .../compose/samples/JoinChannelVideo.kt | 4 +- .../compose/samples/JoinChannelVideoToken.kt | 4 +- .../compose/samples/JoinMultiChannel.kt | 4 +- .../example/compose/samples/LiveStreaming.kt | 4 +- .../compose/samples/LocalVideoTranscoding.kt | 4 +- .../example/compose/samples/MediaMetadata.kt | 4 +- .../example/compose/samples/MediaPlayer.kt | 4 +- .../example/compose/samples/MediaRecorder.kt | 4 +- .../compose/samples/OriginAudioData.kt | 4 +- .../compose/samples/OriginVideoData.kt | 4 +- .../compose/samples/PictureInPicture.kt | 116 ++-- .../example/compose/samples/PlayAudioFiles.kt | 4 +- .../example/compose/samples/PreCallTest.kt | 4 +- .../example/compose/samples/RTMPStreaming.kt | 4 +- .../example/compose/samples/RhythmPlayer.kt | 4 +- .../example/compose/samples/ScreenSharing.kt | 4 +- .../example/compose/samples/SendDataStream.kt | 4 +- .../example/compose/samples/SpatialSound.kt | 4 +- .../compose/samples/VideoProcessExtension.kt | 4 +- .../example/compose/samples/VoiceEffects.kt | 4 +- .../example/compose/utils/AgoraConfig.java | 22 + .../api/example/compose/utils/TokenUtils.java | 7 +- .../.agent/skills/query-cases/SKILL.md | 110 ++++ .../.agent/skills/review-case/SKILL.md | 52 ++ .../.agent/skills/upsert-case/SKILL.md | 342 ++++++++++ .../references/fragment-template.java | 207 ++++++ Android/APIExample/AGENTS.md | 49 ++ Android/APIExample/ARCHITECTURE.md | 215 +++++++ Android/APIExample/CLAUDE.md | 5 + Android/APIExample/README.md | 28 +- Android/APIExample/README.zh.md | 28 +- .../agora-simple-filter/src/main/agoraLibs | 1 + .../agora-stream-encrypt/src/main/agoraLibs | 1 + Android/APIExample/app/build.gradle | 14 + .../api/example/common/BaseFragment.java | 9 + .../api/example/common/BaseVbFragment.java | 2 +- .../examples/advanced/AgoraBeauty.java | 4 +- .../CDNStreaming/AudienceFragment.java | 593 ------------------ .../advanced/CDNStreaming/EntryFragment.java | 116 ---- .../advanced/CDNStreaming/HostFragment.java | 569 ----------------- .../examples/advanced/ChannelEncryption.java | 4 +- .../examples/advanced/ContentInspect.java | 4 +- .../advanced/CustomRemoteVideoRender.java | 4 +- .../examples/advanced/FaceCapture.java | 4 +- .../examples/advanced/HostAcrossChannel.java | 4 +- .../examples/advanced/InCallReport.java | 2 +- .../advanced/JoinMultipleChannel.java | 4 +- .../examples/advanced/KtvCopyrightMusic.java | 2 +- .../examples/advanced/LiveStreaming.java | 4 +- .../advanced/LocalVideoTranscoding.java | 4 +- .../examples/advanced/MediaMetadata.java | 4 +- .../examples/advanced/MediaPlayer.java | 4 +- .../examples/advanced/MediaRecorder.java | 4 +- .../advanced/MultiVideoSourceTracks.java | 4 +- .../example/examples/advanced/Multipath.java | 4 +- .../examples/advanced/PictureInPicture.java | 4 +- .../examples/advanced/PlayAudioFiles.java | 4 +- .../examples/advanced/PreCallTest.java | 4 +- .../advanced/ProcessAudioRawData.java | 4 +- .../examples/advanced/ProcessRawData.java | 4 +- .../examples/advanced/PushExternalVideo.java | 2 +- .../advanced/PushExternalVideoYUV.java | 4 +- .../examples/advanced/RTMPStreaming.java | 4 +- .../examples/advanced/RhythmPlayer.java | 4 +- .../examples/advanced/ScreenSharing.java | 4 +- .../examples/advanced/SendDataStream.java | 4 +- .../examples/advanced/SimpleExtension.java | 4 +- .../example/examples/advanced/Simulcast.java | 4 +- .../examples/advanced/SpatialSound.java | 4 +- .../advanced/SwitchCameraScreenShare.java | 2 +- .../examples/advanced/ThirdPartyBeauty.java | 2 +- .../advanced/TransparentRendering.java | 4 +- .../examples/advanced/UrlLiveStream.java | 6 +- .../advanced/VideoProcessExtension.java | 4 +- .../examples/advanced/VideoQuickSwitch.java | 4 +- .../examples/advanced/VoiceEffects.java | 4 +- .../advanced/beauty/FaceUnityBeauty.java | 2 +- .../advanced/beauty/SenseTimeBeauty.java | 2 +- .../customaudio/CustomAudioRender.java | 4 +- .../customaudio/CustomAudioSource.java | 4 +- .../examples/audio/AudioRouterPlayer.java | 2 +- .../examples/audio/AudioRouterPlayerExo.java | 2 +- .../examples/audio/AudioRouterPlayerIjk.java | 2 +- .../audio/AudioRouterPlayerNative.java | 2 +- .../example/examples/audio/AudioWaveform.java | 4 +- .../examples/basic/JoinChannelAudio.java | 2 +- .../examples/basic/JoinChannelVideo.java | 2 +- .../agora/api/example/utils/AgoraConfig.java | 22 + .../agora/api/example/utils/TokenUtils.java | 7 +- .../main/res/layout/fragment_cdn_audience.xml | 148 ----- .../main/res/layout/fragment_cdn_entry.xml | 68 -- .../src/main/res/layout/fragment_cdn_host.xml | 126 ---- .../app/src/main/res/navigation/nav_graph.xml | 27 - .../src/main/res/values/string_configs.xml | 41 -- Android/APIExample/ci.env.py | 41 +- Android/APIExample/cloud_build.sh | 20 +- Android/ARCHITECTURE.md | 38 ++ Android/CLAUDE.md | 5 + 141 files changed, 3046 insertions(+), 2066 deletions(-) create mode 100644 Android/AGENTS.md create mode 100644 Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md create mode 100644 Android/APIExample-Audio/.agent/skills/review-case/SKILL.md create mode 100644 Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md create mode 100644 Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java create mode 100644 Android/APIExample-Audio/AGENTS.md create mode 100644 Android/APIExample-Audio/ARCHITECTURE.md create mode 100644 Android/APIExample-Audio/CLAUDE.md create mode 100644 Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java delete mode 100644 Android/APIExample-Audio/app/src/main/res/values/string_configs.xml create mode 100644 Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md create mode 100644 Android/APIExample-Compose/.agent/skills/review-case/SKILL.md create mode 100644 Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md create mode 100644 Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt create mode 100644 Android/APIExample-Compose/AGENTS.md create mode 100644 Android/APIExample-Compose/ARCHITECTURE.md create mode 100644 Android/APIExample-Compose/CLAUDE.md create mode 100644 Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AgoraConfig.java create mode 100644 Android/APIExample/.agent/skills/query-cases/SKILL.md create mode 100644 Android/APIExample/.agent/skills/review-case/SKILL.md create mode 100644 Android/APIExample/.agent/skills/upsert-case/SKILL.md create mode 100644 Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java create mode 100644 Android/APIExample/AGENTS.md create mode 100644 Android/APIExample/ARCHITECTURE.md create mode 100644 Android/APIExample/CLAUDE.md create mode 120000 Android/APIExample/agora-simple-filter/src/main/agoraLibs create mode 120000 Android/APIExample/agora-stream-encrypt/src/main/agoraLibs delete mode 100644 Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/AudienceFragment.java delete mode 100644 Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/EntryFragment.java delete mode 100644 Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/HostFragment.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java delete mode 100644 Android/APIExample/app/src/main/res/layout/fragment_cdn_audience.xml delete mode 100644 Android/APIExample/app/src/main/res/layout/fragment_cdn_entry.xml delete mode 100644 Android/APIExample/app/src/main/res/layout/fragment_cdn_host.xml delete mode 100644 Android/APIExample/app/src/main/res/values/string_configs.xml create mode 100644 Android/ARCHITECTURE.md create mode 100644 Android/CLAUDE.md diff --git a/Android/AGENTS.md b/Android/AGENTS.md new file mode 100644 index 000000000..77d601ec8 --- /dev/null +++ b/Android/AGENTS.md @@ -0,0 +1,30 @@ +# AGENTS.md — Android + +Android platform entry point. Read this first, then go to the relevant project's `AGENTS.md`. + +## Projects + +| Project | SDK | Language | Purpose | +|---------|-----|----------|---------| +| `APIExample/` | full-sdk | Java / Kotlin + XML | All APIs — default choice | +| `APIExample-Audio/` | voice-sdk | Java + XML | Audio-only — no video APIs | +| `APIExample-Compose/` | full-sdk | Kotlin + Compose | Compose UI, mirrors APIExample cases | + +SDK version: each project's `gradle.properties` → `rtc_sdk_version` (currently `4.6.3`) + +## Project Selection + +- Video / screen sharing / beauty / extensions → `APIExample/` +- Audio-only → `APIExample-Audio/` +- Compose UI or porting an existing case → `APIExample-Compose/` +- No project specified → default to `APIExample/` + +Never share source files between projects. + +## Navigation + +| Project | Entry Point | +|---------|-------------| +| `APIExample/` | `APIExample/AGENTS.md` | +| `APIExample-Audio/` | `APIExample-Audio/AGENTS.md` | +| `APIExample-Compose/` | `APIExample-Compose/AGENTS.md` | diff --git a/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md b/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md new file mode 100644 index 000000000..4636e2b71 --- /dev/null +++ b/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md @@ -0,0 +1,114 @@ +--- +name: query-cases +description: > + Query and browse existing API example cases in the APIExample-Audio Android demo — + lists cases by group, finds which case demonstrates a specific Agora audio API, + checks sort index availability, and resolves display names from string resources. + Use when: someone asks what cases exist, which audio APIs are demonstrated, wants + to find a case by name or API (e.g. setVoiceBeautifierPreset, enableSpatialAudio), + needs a free sort index before adding a new case, or wants to know if an audio + feature is already implemented. This project uses voice-sdk — no video APIs. + Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED, + available cases, existing cases, which case, is there a case, audio case. +--- + +# Query Cases — APIExample-Audio + +## How cases are registered + +Every case is a Fragment under `app/src/main/java/io/agora/api/example/examples/{basic|advanced|audio}/` with an `@Example` annotation: + +```java +@Example( + index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+ + group = ADVANCED, + name = R.string.item_xxx, + actionId = R.id.action_mainFragment_to_xxx, + tipsId = R.string.xxx_tips +) +``` + +A commented-out `@Example` (`//@Example`) means the case is disabled and won't appear in the app. + +This project uses `voice-sdk` — all cases are audio-only, no video APIs exist. + +--- + +## Query procedure + +### Step 1: Decide scope before scanning + +Before listing files, ask: +- **Looking for a specific API?** — scan Javadoc comments for the API name; no need to read all files +- **Need a free sort index?** — collect all `index` values for the target group, then find the gap +- **Listing all cases?** — scan all three directories and collect annotations + +### Step 2: Read ARCHITECTURE.md first + +Read `ARCHITECTURE.md` (the `examples/` section of the Directory Layout). It contains a pre-built index of all cases with group, index, display name, and key API — no file scanning needed for most queries. + +Use ARCHITECTURE.md as the primary source. Fall back to scanning the source directories only when: +- The query requires data not in ARCHITECTURE.md (e.g. full `@Example` field values, `tipsId`) +- ARCHITECTURE.md appears stale (a case exists in source but not in the doc) +- The output involves free-index claims, index collisions, or "is index X available?" decisions — these must be validated from source immediately before final output + +### Step 3: Scan case directories (fallback only) + +| Directory | Group | Contents | +|-----------|-------|----------| +| `examples/basic/` | BASIC | Core audio join/leave patterns | +| `examples/advanced/` | ADVANCED | Feature-specific audio APIs | +| `examples/audio/` | ADVANCED | Audio visualization (still grouped ADVANCED) | + +Each `.java` file is a case. Subdirectories (e.g. `customaudio/`) contain multi-file cases — the main class is the file whose name matches the directory name; if no name match, look for the file containing `@Example`. + +### Step 4: Extract `@Example` fields + +For each file, read the annotation for `group`, `index`, `name` (string resource ID), and `tipsId`. If the annotation is commented out, the case is disabled. + +Resolve display names from `app/src/main/res/values/strings.xml`: +`R.string.item_voice_effects` → `Voice Effects` + +### Step 5: Read class Javadoc for API mapping + +The Javadoc above each class lists the key APIs demonstrated: + +```java +/** + * This demo demonstrates how to apply voice beautifier effects. + * + * Key APIs used: + * - RtcEngine.setVoiceBeautifierPreset() + */ +``` + +Use this to answer "which case uses X?" queries without reading the full implementation. + +If no Javadoc is present, scan the method body for the API name as a method call. If still not found, note "API mapping unavailable" in the results table. + +### Step 6: Present results + +Full listing — table format: + +| Group | Index | Case Name | File | Key APIs | +|-------|-------|-----------|------|----------| +| BASIC | 0 | Join Channel Audio | JoinChannelAudio.java | joinChannel() | +| ADVANCED | 4 | Voice Effects | VoiceEffects.java | setVoiceBeautifierPreset() | + +For a specific query (e.g. "which case uses enableSpatialAudio?"), return only matching rows. + +For a free-index query, list all used indices in the target group and identify the next available slot: +> BASIC range: 0–9. ADVANCED range: 10+. +> ADVANCED indices in use: 10, 11, 12, 15, 20 → next free: 13 + +Before returning any free-index/collision result, re-scan source registration points (`@Example` across `basic/`, `advanced/`, `audio/`) and recompute once from source-of-truth data. + +--- + +## NEVER + +- **NEVER** count a commented-out `@Example` (`//@Example`) as an active case — it is disabled and won't appear in the app. +- **NEVER** mix index spaces across groups — `audio/` cases use `group=ADVANCED` but share the same index namespace as `advanced/`; always scan both directories together when finding a free index. +- **NEVER** use filename alone to identify a subdirectory case — the main class is the file whose name matches the directory name; if no match, look for the file with `@Example`. +- **NEVER** report a free index without scanning all three directories (`basic/`, `advanced/`, `audio/`) for the target group — missing one causes index collisions. +- **NEVER** suggest video APIs — this project uses voice-sdk only; video APIs do not exist. diff --git a/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md b/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md new file mode 100644 index 000000000..ba517c3ea --- /dev/null +++ b/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md @@ -0,0 +1,57 @@ +--- +name: review-case +description: > + Review an existing case implementation against project-specific red lines + and coding standards. Use after implementing or modifying a case. + Use when: reviewing a case for correctness, checking red-line compliance, + verifying lifecycle and threading patterns, auditing an existing Fragment. + Keywords: review, audit, check, red lines, lifecycle, threading, compliance. +--- + +# Review Case — APIExample-Audio + +Run through every item below before considering a case implementation complete. +Open the case's Fragment source file and verify each point against the actual code. + +## Checklist + +### Teardown & Lifecycle + +- [ ] **leaveChannel before destroy** — `engine.leaveChannel()` is called before `RtcEngine.destroy()` in the teardown path (typically `onDestroy()`). Destroying without leaving first leaks the channel session on the server side. + +- [ ] **handler.post for destroy** — `RtcEngine.destroy()` is invoked via `handler.post(RtcEngine::destroy)` and **not** called directly on the main thread. A direct call blocks the UI thread and causes ANR. + +### Threading + +- [ ] **runOnUIThread for callbacks** — All `IRtcEngineEventHandler` callbacks that update UI are wrapped with `runOnUIThread()`. SDK callbacks arrive on a background thread; touching Views without dispatching to the main thread causes crashes or silent rendering corruption. + +### Permissions + +- [ ] **Permission check before join** — `checkOrRequestPermission()` is called before `joinChannel()`. Joining without the required permissions (RECORD_AUDIO) causes a silent failure — no error callback, just no audio. + +### Backend Reporting + +- [ ] **setParameters present** — `setParameters(...)` is called during engine initialisation. This is required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally. + +### Private Cloud + +- [ ] **getPrivateCloudConfig null-check** — `getPrivateCloudConfig()` is null-checked before `setLocalAccessPoint()` is called. The method returns `null` on standard (non-private-cloud) builds, so calling `setLocalAccessPoint()` without the guard causes a NullPointerException. + +### Audio-Only Constraint + +- [ ] **No video APIs** — The case must not call `enableVideo()`, `setupLocalVideo()`, or reference `VideoCanvas`. APIExample-Audio uses the voice-SDK which has no video module; calling video APIs causes a compile error or runtime crash. + +## If a Check Fails + +- Teardown order wrong (`destroy` before `leaveChannel`) — fix teardown to `leaveChannel()` first, then `handler.post(RtcEngine::destroy)`, and re-test back navigation. +- UI touched in SDK callback without main-thread dispatch — wrap UI updates in `runOnUIThread()` and re-run to verify no thread exceptions. +- Permission flow missing before `joinChannel()` — add `checkOrRequestPermission()` gate and verify join only after `RECORD_AUDIO` is granted. +- Any video API appears in code — remove all video API calls/usages immediately and replace with audio-only equivalents. +- Missing `setParameters(...)` or private-cloud null-check — add both safeguards in engine init and re-run initialization. + +## NEVER + +- **NEVER** approve a case review if any video API (`enableVideo`, `setupLocalVideo`, `VideoCanvas`) exists in APIExample-Audio. +- **NEVER** approve a case review with direct `RtcEngine.destroy()` on main thread. +- **NEVER** approve a case review when `leaveChannel()` is missing before destroy. +- **NEVER** ignore background-thread UI updates inside `IRtcEngineEventHandler` callbacks. diff --git a/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md b/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..9cb3d65e2 --- /dev/null +++ b/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md @@ -0,0 +1,231 @@ +--- +name: upsert-case +description: > + Add a new audio API example case or modify an existing one in the APIExample-Audio Android demo — + creates or updates Fragment class, XML layout, string resources, and nav_graph registration. + Use when: adding a new Agora audio API demo screen, modifying an existing case's implementation + or registration, implementing a new audio feature example in Java + XML layouts, registering a new + case via @Example annotation, subclassing BaseFragment for a new audio demo screen, or updating + an existing case's strings, layout, or nav entry. + This project uses voice-sdk — no video APIs available. + Keywords: add case, modify case, update case, new fragment, nav_graph, @Example, BaseFragment, + APIExample-Audio, audio case, voice-sdk, new screen, audio demo, upsert case. +--- + +# Upsert Case — APIExample-Audio + +## Adding a New Case + +Touch exactly 4 files (all paths relative to `app/src/main/`): + +| File | What to add | +|---|---| +| `java/.../examples/{basic\|advanced\|audio}/YourCaseName.java` | Fragment class | +| `res/layout/fragment_your_case_name.xml` | XML layout | +| `res/values/strings.xml` | 2 strings | +| `res/navigation/nav_graph.xml` | 1 action + 1 destination | + +Registration is automatic via reflection — no other files needed. + +**voice-sdk constraint**: Do NOT call `enableVideo()`, `setupLocalVideo()`, `VideoCanvas`, or any video API — the module does not exist and will crash at runtime. + +--- + +### Step 1: Clarify before coding + +Before writing a single line, ask: +- **What audio API am I demonstrating?** — determines which existing case is the closest reference to copy patterns from +- **BASIC or ADVANCED group?** — BASIC for fundamental join/leave audio patterns; ADVANCED for feature-specific audio APIs +- **What's the sort index?** — index must be unique within the group. BASIC uses 0–9, ADVANCED starts from 10. Run `query-cases` skill first; a collision causes silent ordering bugs at runtime +- **Any special permissions beyond `RECORD_AUDIO`?** — most audio cases only need `RECORD_AUDIO`; check if the API requires anything else + +--- + +### Step 2: Create the Fragment + +**MANDATORY — READ ENTIRE FILE before writing any code**: +[`references/fragment-template.java`](references/fragment-template.java) + +Do NOT skip — the `setParameters`, `handler.post`, `getPrivateCloudConfig()` null-check, `AudioSeatManager` wiring, and voice-sdk constraints are only fully shown there and are required in every case. + +**Do NOT load** any other reference files for this task. + +Non-obvious points the template highlights: + +- `setParameters(...)` for app scenario reporting — **required in every case**, do not remove +- `handler.post(RtcEngine::destroy)` — NOT `RtcEngine.destroy()` directly; direct call blocks UI thread (ANR) +- `getPrivateCloudConfig()` null-check before `setLocalAccessPoint()` — returns null on non-private-cloud builds (NPE) +- All `IRtcEngineEventHandler` callbacks run on a **background thread** — always `runOnUIThread()` for UI +- `onActivityCreated` → create engine; `onDestroy` → `leaveChannel()` then `handler.post(RtcEngine::destroy)` +- `ChannelMediaOptions` must NOT set `publishCameraTrack` or `autoSubscribeVideo` — voice-sdk has no video module +- Use `AudioSeatManager` (not `VideoReportLayout`) to visualize remote participants + +--- + +### Step 3: Create the XML layout + +Typical audio layout — channel input + join button + audio controls: + +```xml + + + + + + + + + + + + +``` + +For waveform visualization, copy the `WaveformView` pattern from `fragment_join_channel_audio.xml`. + +--- + +### Step 4: Add nav entries + +File: `res/navigation/nav_graph.xml` + +**Action** — inside `` (NOT mainFragment — mainFragment only has one action, to Ready): + +```xml + +``` + +**Destination** — at root `` level: + +```xml + +``` + +`action android:id` must exactly match `actionId` in `@Example`. + +--- + +### Step 5: Update ARCHITECTURE.md + +Add one line to the case list in `ARCHITECTURE.md` under the correct directory section (`basic/`, `advanced/`, or `audio/`): + +``` +├── YourCaseName.java # [index] "Display Name" — key API description +``` + +Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans. + +--- + +## Modifying an Existing Case + +When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating: + +| What changed | Files to touch | +|---|---| +| Implementation logic (API calls, event handling) | `java/.../examples/{basic\|advanced\|audio}/CaseName.java` | +| UI layout (views, controls) | `res/layout/fragment_case_name.xml` | +| Display name or tips text | `res/values/strings.xml` | +| Sort index or group (BASIC ↔ ADVANCED) | `@Example` annotation in the Fragment class | +| Navigation label | `res/navigation/nav_graph.xml` (fragment label attribute) | +| Class rename or package move | Fragment class, `nav_graph.xml` (android:name + destination id), `@Example` annotation (actionId), layout file name, `ARCHITECTURE.md` | + +After making changes: + +1. **Verify `@Example` annotation consistency** — ensure `index`, `group`, `name`, `actionId`, and `tipsId` still match the actual string resources, nav action ID, and intended group/position. A mismatch causes the case to silently disappear from the list or navigate to the wrong screen. +2. **Update `res/values/strings.xml`** if the display name or tips text changed. +3. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed. +4. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description. + +--- + +## Verify + +```bash +./gradlew assembleDebug +``` + +- [ ] Case appears in correct group at expected sort position +- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment) +- [ ] `onJoinChannelSuccess` fires in Logcat +- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy` +- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description +- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries + +--- + +## When to Use a Spec Instead + +If the case meets any of the following criteria, create a Spec rather than using this skill directly: + +1. Involves coordinated calls across two or more Agora API modules +2. Requires a custom UI layout (not one of the standard templates above) +3. Manages multiple channels or multiple engine instances +4. Requires a foreground Service or background thread coordination +5. Involves developing new shared components (widget/utils, etc.) +6. Requires optional module integration (e.g. streamEncrypt) + +If none apply → use this skill directly; no Spec needed. + +### Spec Requirements Document Must Include + +- List of APIs the case demonstrates (audio APIs only) +- User interaction flow description +- Expected RtcEngine lifecycle behavior +- Required permissions (typically only `RECORD_AUDIO`) + +### Spec Design Document Must Include + +- Target project identifier: `APIExample-Audio` +- Class/file structure design +- API call sequence (Mermaid sequence diagram recommended) +- State management approach +- UI layout plan +- Integration points with existing shared components +- Case registration info: class name, display name, group (BASIC/ADVANCED), sort index — finalize during design to avoid conflicts +- Generate `@Example` annotation parameters, `nav_graph.xml` action + destination, `strings.xml` key names (`item_` prefix) +- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing indices +- voice-sdk checks: no video APIs (`enableVideo`, `setupLocalVideo`, `setupRemoteVideo`, `VideoCanvas`, `startScreenCapture`) — violations must be eliminated at design time +- Risk identification and mitigation (API availability, permissions, thread safety, performance) + +### Spec Task List Integration + +- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters +- Mark which sub-tasks require manual coding, and provide target file paths and change summaries +- New shared component creation tasks must come before case implementation tasks + +--- + +## NEVER + +- **NEVER** call any video API (`enableVideo`, `setupLocalVideo`, `VideoCanvas`) — voice-sdk has no video module; crash is immediate. +- **NEVER** put the nav action inside `` — it belongs in ``. mainFragment only routes to Ready; all case actions live in Ready. Wrong placement causes silent navigation failure at runtime. +- **NEVER** call `RtcEngine.destroy()` directly on the main thread — always `handler.post(RtcEngine::destroy)`. Direct call blocks the UI thread and causes ANR. +- **NEVER** call `setLocalAccessPoint()` without null-checking `getPrivateCloudConfig()` first — it returns null on standard builds, causing NPE. +- **NEVER** update UI directly inside `IRtcEngineEventHandler` callbacks — they run on a background thread. Always wrap with `runOnUIThread()`. +- **NEVER** omit `setParameters(...)` — it's required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally. diff --git a/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java b/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java new file mode 100644 index 000000000..b4e9214c7 --- /dev/null +++ b/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java @@ -0,0 +1,209 @@ +package io.agora.api.example.examples.advanced; + +import static io.agora.api.example.common.model.Examples.ADVANCED; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Random; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.widget.AudioSeatManager; +import io.agora.api.example.utils.PermissonUtils; +import io.agora.api.example.utils.TokenUtils; +import io.agora.rtc2.ChannelMediaOptions; +import io.agora.rtc2.Constants; +import io.agora.rtc2.IRtcEngineEventHandler; +import io.agora.rtc2.RtcEngine; +import io.agora.rtc2.RtcEngineConfig; +import io.agora.rtc2.proxy.LocalAccessPointConfiguration; + +// NOTE: This project uses voice-sdk. +// Do NOT import or call: enableVideo(), setupLocalVideo(), VideoCanvas, VideoReportLayout, +// setVideoEncoderConfiguration(), or any other video API — the module does not exist. + +/** + * This demo demonstrates how to use [describe the audio feature here]. + * + * Key APIs used: + * - RtcEngine.yourAudioApi() + */ +@Example( + index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+ + group = ADVANCED, // BASIC or ADVANCED + name = R.string.item_your_case_name, + actionId = R.id.action_mainFragment_to_yourCaseName, + tipsId = R.string.your_case_name_tips +) +public class YourCaseName extends BaseFragment implements View.OnClickListener { + private static final String TAG = YourCaseName.class.getSimpleName(); + + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + + // AudioSeatManager visualizes remote audio participants — add seat views in your XML layout + private AudioSeatManager audioSeatManager; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_your_case_name, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + join.setOnClickListener(this); + // bind additional feature-specific views here + + // Wire AudioSeatManager to seat views defined in your XML layout + // audioSeatManager = new AudioSeatManager( + // view.findViewById(R.id.audio_place_01), + // view.findViewById(R.id.audio_place_02) + // ); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Context context = getContext(); + if (context == null) return; + try { + RtcEngineConfig config = new RtcEngineConfig(); + config.mContext = context.getApplicationContext(); + config.mAppId = getAgoraAppId(); + config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; + config.mEventHandler = iRtcEngineEventHandler; + config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAreaCode = ((MainApplication) getActivity().getApplication()) + .getGlobalSettings().getAreaCode(); + engine = RtcEngine.create(config); + // REQUIRED in every case — do not remove + engine.setParameters("{" + + "\"rtc.report_app_scenario\":" + + "{" + + "\"appScenario\":" + 100 + "," + + "\"serviceType\":" + 11 + "," + + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" + + "}" + + "}"); + // null-check is mandatory — returns null on non-private-cloud builds + LocalAccessPointConfiguration localAccessPointConfiguration = + ((MainApplication) getActivity().getApplication()) + .getGlobalSettings().getPrivateCloudConfig(); + if (localAccessPointConfiguration != null) { + engine.setLocalAccessPoint(localAccessPointConfiguration); + } + } catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (engine != null) { + engine.leaveChannel(); + } + // MUST use handler.post — do NOT call RtcEngine.destroy() directly on main thread + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + String channelId = et_channel.getText().toString(); + checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { + @Override + public void onPermissionsResult(boolean allPermissionsGranted, + String[] permissions, int[] grantResults) { + if (allPermissionsGranted) { + joinChannel(channelId); + } + } + }); + } else { + joined = false; + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) { + engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); + // --- feature-specific audio setup goes here --- + // e.g. engine.setAudioProfile(...); engine.setVoiceBeautifierPreset(...); + + ChannelMediaOptions options = new ChannelMediaOptions(); + options.autoSubscribeAudio = true; + options.publishMicrophoneTrack = true; + // Do NOT set publishCameraTrack or autoSubscribeVideo — voice-sdk has no video module + + int uid = new Random().nextInt(1000) + 100000; + TokenUtils.gen(requireContext(), channelId, uid, token -> { + int res = engine.joinChannel(token, channelId, uid, options); + if (res != 0) { + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + join.setEnabled(false); + }); + } + + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + // ALL UI updates must go through runOnUIThread — callbacks run on background thread + runOnUIThread(() -> { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + }); + } + + @Override + public void onUserJoined(int uid, int elapsed) { + Log.i(TAG, "onUserJoined -> " + uid); + runOnUIThread(() -> { + // audioSeatManager.addUser(uid); + }); + } + + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline, reason %d", uid, reason)); + runOnUIThread(() -> { + // audioSeatManager.removeUser(uid); + }); + } + + @Override + public void onError(int err) { + showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err)); + } + }; +} diff --git a/Android/APIExample-Audio/AGENTS.md b/Android/APIExample-Audio/AGENTS.md new file mode 100644 index 000000000..169794535 --- /dev/null +++ b/Android/APIExample-Audio/AGENTS.md @@ -0,0 +1,39 @@ +# AGENTS.md — APIExample-Audio + +Audio-only demo project. Uses `voice-sdk` — the video module is not available. +Use this project for audio-only features. + +## Build Commands + +```bash +./gradlew assembleDebug # build debug APK +./gradlew installDebug # build + install to connected device +./gradlew test # unit tests +./gradlew connectedAndroidTest # instrumented tests (device required) +``` + +## App ID Configuration + +See [README.md — Obtain an App Id](README.md#obtain-an-app-id). + +## Architecture Red Lines + +- Do NOT call `enableVideo()`, `setupLocalVideo()`, or `VideoCanvas` — `voice-sdk` has no video module and will crash at runtime. +- Do NOT add video, screen sharing, or beauty cases — use `APIExample/` instead. +- Each case Fragment must create and destroy its own `RtcEngine` instance. +- Always call `engine.leaveChannel()` before `RtcEngine.destroy()` in `onDestroy()`. +- All `IRtcEngineEventHandler` callbacks run on a background thread — use `handler.post {}` for UI updates. +- Always call `checkOrRequestPermission()` before `joinChannel()`. Audio cases only need `RECORD_AUDIO`. + +## Skills + +| Skill | Path | Description | +|-------|------|-------------| +| upsert-case | `.agent/skills/upsert-case/` | Add a new audio case or modify an existing one | +| query-cases | `.agent/skills/query-cases/` | Query and browse existing audio cases | +| review-case | `.agent/skills/review-case/` | Review a case against project red lines + + +## Further Reading + +- `ARCHITECTURE.md` — full directory layout and case registration details diff --git a/Android/APIExample-Audio/ARCHITECTURE.md b/Android/APIExample-Audio/ARCHITECTURE.md new file mode 100644 index 000000000..1aab8f934 --- /dev/null +++ b/Android/APIExample-Audio/ARCHITECTURE.md @@ -0,0 +1,140 @@ +# ARCHITECTURE.md — APIExample-Audio + +## Directory Layout + +``` +APIExample-Audio/ +├── gradle.properties # rtc_sdk_version +└── app/src/main/ + ├── AndroidManifest.xml + ├── assets/ # Audio sample files + ├── res/ + │ ├── navigation/nav_graph.xml # Single nav graph — all case destinations live here + │ ├── values/strings.xml # All display names and tips strings + │ └── layout/ # XML layouts for each case Fragment + └── java/io/agora/api/example/ + ├── MainApplication.java # Scans DEX and registers all @Example cases at startup + ├── MainActivity.java # Single-Activity host, owns NavController + ├── MainFragment.java # Home screen — renders BASIC / ADVANCED section list + ├── ReadyFragment.java # Splash / config check screen + ├── SettingActivity.java # Global settings (area code, audio profile) + │ + ├── annotation/ + │ └── Example.java # @Example annotation — identical to APIExample + │ + ├── common/ + │ ├── BaseFragment.java # Base class ALL case Fragments must extend + │ ├── Constant.java # App-wide constants + │ ├── adapter/ + │ │ └── SectionAdapter.java # RecyclerView adapter for the grouped case list + │ ├── model/ + │ │ ├── Examples.java # Static registry: ITEM_MAP keyed by group name + │ │ ├── GlobalSettings.java # Audio config shared across cases + │ │ ├── ExampleBean.java + │ │ ├── Peer.java + │ │ └── StatisticsInfo.java + │ ├── widget/ + │ │ ├── AudioOnlyLayout.java # Audio seat layout (no video surface) + │ │ ├── AudioSeatManager.java + │ │ └── WaveformView.java + │ └── gles/ # OpenGL ES helpers (for waveform visualization) + │ + ├── examples/ # All cases live here — ClassUtils scans this package + │ ├── basic/ # group = "BASIC" (index 0–9) + │ │ ├── JoinChannelAudioByToken.java # [0] "Live Interactive Audio Streaming(Token Verify)" + │ │ └── JoinChannelAudio.java # [1] "Live Interactive Audio Streaming" + │ ├── advanced/ # group = "ADVANCED" (index 10+) + │ │ ├── VoiceEffects.java # [10] "Set the Voice Beautifier and Effects" — setVoiceBeautifierPreset + │ │ ├── customaudio/CustomAudioSource.java # [11] "Custom Audio Sources" — push external audio + │ │ ├── customaudio/CustomAudioRender.java # [12] "Custom Audio Render" — pull audio for custom rendering + │ │ ├── customaudio/AudioPlayer.java # helper for CustomAudioRender + │ │ ├── ProcessAudioRawData.java # [13] "Raw Audio Data" — audio raw data processing + │ │ ├── PlayAudioFiles.java # [14] "Play Audio Files" — audio mixing + │ │ ├── PreCallTest.java # [15] "Pre-call Tests" — network/device test before joining + │ │ ├── RhythmPlayer.java # [16] "Rhythm Player" — metronome/rhythm playback + │ │ └── SpatialSound.java # [17] "Spatial Audio" — 3D spatial audio + │ └── audio/ # Audio-specific cases (grouped as ADVANCED) + │ └── AudioWaveform.java # [18] "Audio Waveform" — audio visualization + │ + └── utils/ + ├── ClassUtils.java # DEX scanner — auto-discovers @Example classes + ├── TokenUtils.java # Fetches RTC tokens from Agora token server + ├── PermissonUtils.java # Permission check/request helpers + ├── CommonUtil.java + ├── ErrorUtil.java + ├── FileUtils.java + ├── AudioFileReader.java + └── YUVUtils.java +``` + +## Case Index + +| Case | Path | Key APIs | Description | +|------|------|----------|-------------| +| Live Interactive Audio Streaming(Token Verify) | `basic/JoinChannelAudioByToken.java` | `RtcEngine.create()`, `joinChannel()`, `setClientRole()` | Demonstrates audio-only calling with manual App ID and token input | +| Live Interactive Audio Streaming | `basic/JoinChannelAudio.java` | `RtcEngine.create()`, `joinChannel()`, `setAudioProfile()`, `setAudioScenario()`, `muteLocalAudioStream()`, `enableInEarMonitoring()`, `adjustRecordingSignalVolume()`, `adjustPlaybackSignalVolume()` | Demonstrates audio-only calling with volume controls, in-ear monitoring, and audio routing | +| Set the Voice Beautifier and Effects | `advanced/VoiceEffects.java` | `setVoiceBeautifierPreset()`, `setAudioEffectPreset()`, `setVoiceConversionPreset()`, `setAudioEffectParameters()`, `setLocalVoicePitch()`, `setLocalVoiceEqualization()`, `setLocalVoiceReverb()`, `setLocalVoiceFormant()`, `setAINSMode()`, `enableVoiceAITuner()` | Demonstrates voice beautifier presets, audio effects, voice conversion, and AI noise suppression | +| Custom Audio Sources | `advanced/customaudio/CustomAudioSource.java` | `createCustomAudioTrack()`, `pushExternalAudioFrame()`, `enableCustomAudioLocalPlayback()`, `destroyCustomAudioTrack()` | Demonstrates pushing external audio frames via a custom audio track | +| Custom Audio Render | `advanced/customaudio/CustomAudioRender.java` | `setExternalAudioSink()`, `pullPlaybackAudioFrame()` | Demonstrates pulling audio frames for custom audio rendering | +| Raw Audio Data | `advanced/ProcessAudioRawData.java` | `registerAudioFrameObserver()`, `setRecordingAudioFrameParameters()`, `setPlaybackAudioFrameParameters()` | Demonstrates processing raw audio data through the audio frame observer | +| Play Audio Files | `advanced/PlayAudioFiles.java` | `startAudioMixing()`, `stopAudioMixing()`, `pauseAudioMixing()`, `resumeAudioMixing()`, `getAudioEffectManager()`, `adjustAudioMixingVolume()` | Demonstrates audio mixing and sound effect playback | +| Pre-call Tests | `advanced/PreCallTest.java` | `startLastmileProbeTest()`, `stopLastmileProbeTest()`, `startEchoTest()`, `stopEchoTest()` | Demonstrates network quality probing and echo testing before joining a channel | +| Rhythm Player | `advanced/RhythmPlayer.java` | `startRhythmPlayer()`, `stopRhythmPlayer()`, `enableAudioVolumeIndication()` | Demonstrates metronome/rhythm playback synchronized with audio streaming | +| Spatial Audio | `advanced/SpatialSound.java` | `ILocalSpatialAudioEngine.create()`, `updateSelfPosition()`, `updateRemotePosition()`, `updatePlayerPositionInfo()`, `setZones()`, `createMediaPlayer()` | Demonstrates 3D spatial audio positioning for remote users and media players | +| Audio Waveform | `audio/AudioWaveform.java` | `enableAudio()`, `enableAudioVolumeIndication()` | Demonstrates real-time audio waveform visualization | + +## Case Registration Mechanism + +Identical to `APIExample` — automatic via reflection, no manual list. + +**Startup flow:** +1. `MainApplication.onCreate()` calls `ClassUtils.getFileNameByPackageName(context, "io.agora.api.example.examples")`. +2. `ClassUtils` scans all DEX entries whose class name starts with that prefix. +3. For each class, it checks for `@Example` annotation and calls `Examples.addItem(annotation)`. +4. `Examples.sortItem()` sorts each group by `index`. +5. `MainFragment` reads `Examples.ITEM_MAP` and renders the list. + +**`@Example` annotation — all four fields are required:** +```java +@Example( + index = 2, // sort order within the group; BASIC: 0–9, ADVANCED: 10+ + group = BASIC, // "BASIC" or "ADVANCED" + name = R.string.item_my_case, // display name string resource + actionId = R.id.action_mainFragment_to_myCase, // nav action ID in nav_graph.xml + tipsId = R.string.my_case_tips // description string resource +) +public class MyCase extends BaseFragment { … } +``` + +## Navigation + +Identical to `APIExample` — single `nav_graph.xml` with Jetpack Navigation Component. + +Every case needs: +- A `` destination entry in `nav_graph.xml` +- An `` inside `` +- The action `id` must exactly match `actionId` in `@Example` + +## RtcEngine Lifecycle + +``` +onActivityCreated → RtcEngine.create() (voice-sdk — no video APIs) + → engine.setAudioProfile / setAudioScenario + → joinChannel() (after RECORD_AUDIO permission granted) + ↓ + [IRtcEngineEventHandler callbacks — background thread] + ↓ +onDestroy → engine.leaveChannel() + → RtcEngine.destroy() + → engine = null +``` + +## Token Flow + +```java +TokenUtils.gen(requireContext(), channelId, uid, token -> { + engine.joinChannel(token, channelId, uid, options); +}); +``` + +`TokenUtils` reads `AGORA_APP_ID` and `AGORA_APP_CERT` from `local.properties` via `BuildConfig`. If `AGORA_APP_CERT` is empty, token generation is skipped — valid for projects without certificate. diff --git a/Android/APIExample-Audio/CLAUDE.md b/Android/APIExample-Audio/CLAUDE.md new file mode 100644 index 000000000..2d1c323ad --- /dev/null +++ b/Android/APIExample-Audio/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +This project uses `AGENTS.md` instead of a `CLAUDE.md` file. + +Please see @AGENTS.md in this same directory and treat its content as the primary reference for this project. diff --git a/Android/APIExample-Audio/README.md b/Android/APIExample-Audio/README.md index 6cbdf868a..f2e1d131a 100644 --- a/Android/APIExample-Audio/README.md +++ b/Android/APIExample-Audio/README.md @@ -22,19 +22,16 @@ To build and run the sample application, get an App Id: 2. Navigate in the Dashboard tree on the left to **Projects** > **Project List**. 3. Save the **App Id** from the Dashboard for later use. 4. Save the **App Certificate** from the Dashboard for later use. -5. Generate a temp **Access Token** (valid for 24 hours) from dashboard page with given channel name, save for later use. - -6. Open `Android/APIExample` and edit the `app/src/main/res/values/string-config.xml` file. Update `YOUR APP ID` with your App Id, update `YOUR APP CERTIFICATE` with the main app certificate from dashboard, and update `YOUR ACCESS TOKEN` with the temp Access Token generated from dashboard. Note you can leave the token and certificate variable `null` if your project has not turned on security token. +5. Open `Android/APIExample-Audio` and edit the `local.properties` file in the project root. Update `YOUR APP ID` with your App Id. If your Agora project has App Certificate enabled and you want to use the sample's built-in token generation flow, update `YOUR APP CERTIFICATE` as well. ``` - YOUR APP ID - // assign token and certificate to null if you have not enabled app certificate - YOUR APP CERTIFICATE - // assign token and certificate to null if you have not enabled app certificate or you have set the certificate above. - // PS:It is unsafe to place the App Certificate on the client side, it is recommended to place it on the server side to ensure that the App Certificate is not leaked. - YOUR ACCESS TOKEN + sdk.dir=/path/to/Android/sdk + AGORA_APP_ID=YOUR APP ID + AGORA_APP_CERT=YOUR APP CERTIFICATE ``` +`AGORA_APP_ID` is required. If your project does not enable App Certificate, leave `AGORA_APP_CERT` blank. If you prefer to generate tokens on your own server, keep `AGORA_APP_CERT` empty on the client side and use the `JoinChannelAudio(Token)` example to paste the token at runtime. + You are all set. Now connect your Android device and run the project. diff --git a/Android/APIExample-Audio/README.zh.md b/Android/APIExample-Audio/README.zh.md index 6828cc087..38204654d 100644 --- a/Android/APIExample-Audio/README.zh.md +++ b/Android/APIExample-Audio/README.zh.md @@ -22,20 +22,16 @@ 2. 前往后台页面,点击左部导航栏的 **项目 > 项目列表** 菜单 3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它 4. 复制后台的 **App Certificate** 并备注,稍后启动应用时会用到它 -5. 在项目页面生成临时 **Access Token** (24小时内有效)并备注,注意生成的Token只能适用于对应的频道名。 - -6. 打开 `Android/APIExample` 并编辑 `app/src/main/res/values/string-config.xml`,将你的 AppID 、App主证书、 临时Token 分别替换到 `Your App Id` 、 `YOUR ACCESS TOKEN` 和 `YOUR APP CERTIFICATE` +5. 打开 `Android/APIExample-Audio` 并编辑项目根目录下的 `local.properties`,填入你的 App ID。如果你的 Agora 项目开启了 App Certificate,并且你希望使用示例内置的 token 生成功能,再填入 `YOUR APP CERTIFICATE` ``` - YOUR APP ID - // 如果你没有打开Token功能,certificate可以直接不填 - YOUR APP CERTIFICATE - // 如果你没有打开Token功能或者已经配置了certificate,token可以直接不填 - // 注意:App证书放在客户端不安全,推荐放在服务端以确保 App 证书不会泄露。 - YOUR ACCESS TOKEN - + sdk.dir=/path/to/Android/sdk + AGORA_APP_ID=YOUR APP ID + AGORA_APP_CERT=YOUR APP CERTIFICATE ``` +`AGORA_APP_ID` 为必填项。如果你的项目没有开启 App Certificate,`AGORA_APP_CERT` 留空即可。如果你使用自己的服务端生成 token,建议不要在客户端填写 `AGORA_APP_CERT`,直接使用 `JoinChannelAudio(Token)` 示例在运行时粘贴 token。 + 然后你就可以编译并运行项目了。 ## 联系我们 diff --git a/Android/APIExample-Audio/app/build.gradle b/Android/APIExample-Audio/app/build.gradle index 97830bac4..a12490b62 100644 --- a/Android/APIExample-Audio/app/build.gradle +++ b/Android/APIExample-Audio/app/build.gradle @@ -9,6 +9,18 @@ sdkVersionFile.withInputStream { stream -> def agoraSdkVersion = properties.getProperty("rtc_sdk_version") println("${rootProject.project.name} agoraSdkVersion: ${agoraSdkVersion}") def localSdkPath= "${rootProject.projectDir.absolutePath}/../../sdk" +def localPropertiesFile = rootProject.file("local.properties") +def localProperties = new Properties() +if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { stream -> + localProperties.load(stream) + } +} +def agoraAppId = localProperties.getProperty("AGORA_APP_ID", "") +if (agoraAppId.isEmpty()) { + throw new GradleException("Please configure correctly in the local.properties file in the project root directory: AGORA_APP_ID=") +} +def agoraAppCert = localProperties.getProperty("AGORA_APP_CERT", "") android { namespace "io.agora.api.example" @@ -22,6 +34,8 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + buildConfigField "String", "AGORA_APP_ID", "\"${agoraAppId}\"" + buildConfigField "String", "AGORA_APP_CERT", "\"${agoraAppCert}\"" } signingConfigs { diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java index cc64acdeb..c33918a41 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/common/BaseFragment.java @@ -20,6 +20,7 @@ import java.util.Map; +import io.agora.api.example.utils.AgoraConfig; import io.agora.api.example.utils.PermissonUtils; /** @@ -182,6 +183,14 @@ protected final void runOnUIThread(Runnable runnable, long delay) { } } + protected final String getAgoraAppId() { + return AgoraConfig.getAppId(); + } + + protected final String getAgoraAppCertificate() { + return AgoraConfig.getAppCertificate(); + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java index 40ea64b86..6a440245c 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java @@ -40,7 +40,7 @@ import io.agora.rtc2.proxy.LocalAccessPointConfiguration; @Example( - index = 15, + index = 14, group = ADVANCED, name = R.string.item_playaudiofiles, actionId = R.id.action_mainFragment_to_PlayAudioFiles, @@ -160,7 +160,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /** * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /** Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java index c541d602b..95c66efff 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java @@ -34,7 +34,7 @@ import io.agora.rtc2.proxy.LocalAccessPointConfiguration; @Example( - index = 16, + index = 15, group = ADVANCED, name = R.string.item_precalltest, actionId = R.id.action_mainFragment_to_PreCallTest, @@ -82,7 +82,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /** * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /** Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java index fa2112fe8..18276db68 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java @@ -44,7 +44,7 @@ * @author cjw */ @Example( - index = 9, + index = 13, group = ADVANCED, name = R.string.item_raw_audio, actionId = R.id.action_mainFragment_raw_audio, @@ -149,7 +149,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /** * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /** Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java index 1f6b9ebf8..bd6801bb4 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java @@ -37,7 +37,7 @@ * This demo demonstrates how to make a VideoProcessExtension */ @Example( - index = 19, + index = 16, group = ADVANCED, name = R.string.item_rhythmplayer, actionId = R.id.action_mainFragment_rhythm_player, @@ -102,7 +102,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /** * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /** Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java index 36e5fd7c3..a05becde0 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java @@ -57,7 +57,7 @@ * The type Spatial sound. */ @Example( - index = 22, + index = 17, group = ADVANCED, name = R.string.item_spatial_sound, actionId = R.id.action_mainFragment_to_spatial_sound, @@ -104,7 +104,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * How to get the App ID * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events.*/ - String appId = getString(R.string.agora_app_id); + String appId = getAgoraAppId(); RtcEngineConfig config = new RtcEngineConfig(); config.mContext = getContext().getApplicationContext(); config.mAppId = appId; diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java index 39780ae2c..0cb03300b 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java @@ -91,7 +91,7 @@ * The type Voice effects. */ @Example( - index = 4, + index = 10, group = ADVANCED, name = R.string.item_voiceeffects, actionId = R.id.action_mainFragment_to_VoiceEffects, @@ -249,7 +249,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java index 9d3be1823..e26c4fbda 100755 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java @@ -38,7 +38,7 @@ /** * This demo demonstrates how to make a one-to-one voice call */ -@Example(index = 6, group = ADVANCED, name = R.string.item_customaudiorender, actionId = R.id.action_mainFragment_to_CustomAudioRender, tipsId = R.string.customaudiorender) +@Example(index = 12, group = ADVANCED, name = R.string.item_customaudiorender, actionId = R.id.action_mainFragment_to_CustomAudioRender, tipsId = R.string.customaudiorender) public class CustomAudioRender extends BaseFragment implements View.OnClickListener { private static final String TAG = CustomAudioRender.class.getSimpleName(); private EditText et_channel; @@ -106,7 +106,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java index aa49bb8ff..3df640efa 100755 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java @@ -39,7 +39,7 @@ * This demo demonstrates how to make a one-to-one voice call */ @Example( - index = 5, + index = 11, group = ADVANCED, name = R.string.item_customaudiosource, actionId = R.id.action_mainFragment_to_CustomAudioSource, @@ -124,7 +124,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /** * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /** Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java index b2c134dc6..1b35bfc66 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java @@ -28,7 +28,7 @@ import io.agora.rtc2.proxy.LocalAccessPointConfiguration; @Example( - index = 7, + index = 18, group = Examples.ADVANCED, name = R.string.item_audiowaveform, actionId = R.id.action_mainFragment_to_AudioWaveform, @@ -58,7 +58,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java index 258cc4b13..4e84a5b0d 100755 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java @@ -62,7 +62,7 @@ * @author cjw */ @Example( - index = 2, + index = 1, group = BASIC, name = R.string.item_joinaudio, actionId = R.id.action_mainFragment_to_joinChannelAudio, @@ -251,7 +251,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java new file mode 100644 index 000000000..4f1d3e4e5 --- /dev/null +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java @@ -0,0 +1,22 @@ +package io.agora.api.example.utils; + +import android.text.TextUtils; + +import io.agora.api.example.BuildConfig; + +public final class AgoraConfig { + private AgoraConfig() { + } + + public static String getAppId() { + return BuildConfig.AGORA_APP_ID; + } + + public static String getAppCertificate() { + return BuildConfig.AGORA_APP_CERT; + } + + public static boolean hasAppCertificate() { + return !TextUtils.isEmpty(getAppCertificate()); + } +} diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java index 7d11f19e6..0b8a4a2df 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/utils/TokenUtils.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.Objects; -import io.agora.api.example.R; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; @@ -37,11 +36,11 @@ public class TokenUtils { } public static void genToken(Context context, String channelName, int uid, OnTokenGenCallback onGetToken) { - String cert = context.getString(R.string.agora_app_certificate); + String cert = AgoraConfig.getAppCertificate(); if (cert.isEmpty()) { onGetToken.onTokenGen(""); } else { - gen(context.getString(R.string.agora_app_id), context.getString(R.string.agora_app_certificate), channelName, uid, ret -> { + gen(AgoraConfig.getAppId(), cert, channelName, uid, ret -> { if (onGetToken != null) { runOnUiThread(() -> { onGetToken.onTokenGen(ret); @@ -59,7 +58,7 @@ public static void genToken(Context context, String channelName, int uid, OnToke } public static void gen(Context context, String channelName, int uid, OnTokenGenCallback onGetToken){ - gen(context.getString(R.string.agora_app_id), context.getString(R.string.agora_app_certificate), channelName, uid, ret -> { + gen(AgoraConfig.getAppId(), AgoraConfig.getAppCertificate(), channelName, uid, ret -> { if(onGetToken != null){ runOnUiThread(() -> { onGetToken.onTokenGen(ret); diff --git a/Android/APIExample-Audio/app/src/main/res/values/string_configs.xml b/Android/APIExample-Audio/app/src/main/res/values/string_configs.xml deleted file mode 100644 index 767727190..000000000 --- a/Android/APIExample-Audio/app/src/main/res/values/string_configs.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - YOUR APP ID - - - - YOUR APP CERTIFICATE - - diff --git a/Android/APIExample-Audio/ci.env.py b/Android/APIExample-Audio/ci.env.py index dd130dd6f..872df41ae 100644 --- a/Android/APIExample-Audio/ci.env.py +++ b/Android/APIExample-Audio/ci.env.py @@ -1,22 +1,29 @@ -#!/usr/bin/python -# -*- coding: UTF-8 -*- import os -import re +from pathlib import Path -def main(): - appId = "" - if "AGORA_APP_ID" in os.environ: - appId = os.environ["AGORA_APP_ID"] - token = "" +def upsert_property(lines, key, value): + target = f"{key}=" + replaced = False + new_lines = [] + for line in lines: + if line.startswith(target): + new_lines.append(f"{target}{value}\n") + replaced = True + else: + new_lines.append(line) + if not replaced and value: + new_lines.append(f"{target}{value}\n") + return new_lines - f = open("./app/src/main/res/values/string_configs.xml", 'r+') - content = f.read() - contentNew = re.sub(r'YOUR APP ID', appId, content) - contentNew = re.sub(r'YOUR ACCESS TOKEN', token, contentNew) - f.seek(0) - f.write(contentNew) - f.truncate() +def main(): + app_id = os.environ.get("AGORA_APP_ID", "") + app_cert = os.environ.get("AGORA_APP_CERT", "") or os.environ.get("AGORA_APP_CERTIFICATE", "") + local_properties = Path("./local.properties") + lines = local_properties.read_text().splitlines(keepends=True) if local_properties.exists() else [] + lines = upsert_property(lines, "AGORA_APP_ID", app_id) + lines = upsert_property(lines, "AGORA_APP_CERT", app_cert) + local_properties.write_text("".join(lines)) if __name__ == "__main__": diff --git a/Android/APIExample-Audio/cloud_build.sh b/Android/APIExample-Audio/cloud_build.sh index 675ad98f6..a19f7eadb 100755 --- a/Android/APIExample-Audio/cloud_build.sh +++ b/Android/APIExample-Audio/cloud_build.sh @@ -38,10 +38,22 @@ fi #sed -ie "s#https://services.gradle.org/distributions#https://mirrors.cloud.tencent.com/gradle#g" gradle/wrapper/gradle-wrapper.properties ## config appId -sed -i -e "s#YOUR APP ID#${APP_ID}#g" app/src/main/res/values/string_configs.xml -sed -i -e "s#YOUR APP CERTIFICATE##g" app/src/main/res/values/string_configs.xml -sed -i -e "s#YOUR ACCESS TOKEN##g" app/src/main/res/values/string_configs.xml -rm -f app/src/main/res/values/string_configs.xml-e +set_local_property() { + key="$1" + value="$2" + file="local.properties" + touch "$file" + if grep -q "^${key}=" "$file"; then + sed -i.bak "s#^${key}=.*#${key}=${value}#g" "$file" + rm -f "${file}.bak" + elif [ -n "$value" ]; then + echo "${key}=${value}" >> "$file" + fi +} + +set_local_property "AGORA_APP_ID" "${APP_ID}" +APP_CERT_VALUE="${APP_CERT:-${AGORA_APP_CERT:-${AGORA_APP_CERTIFICATE:-}}}" +set_local_property "AGORA_APP_CERT" "${APP_CERT_VALUE}" ./gradlew clean || exit 1 ./gradlew :app:assembleRelease || exit 1 diff --git a/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md b/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md new file mode 100644 index 000000000..e4bb11024 --- /dev/null +++ b/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md @@ -0,0 +1,117 @@ +--- +name: query-cases +description: > + Query and browse existing API example cases in the APIExample-Compose Android demo — + lists cases by group, finds which case demonstrates a specific Agora API, checks list + position availability, and resolves display names from string resources. Use when: + someone asks what Compose cases exist, which APIs are demonstrated, wants to find a + case by name or API (e.g. takeSnapshot, setClientRole), needs to know the current + list position before adding a new case, or wants to know if a feature is already + implemented in Compose. Registration is manual via Examples.kt — no @Example annotation. + Keywords: list cases, find case, query cases, Examples.kt, BasicExampleList, + AdvanceExampleList, available cases, existing cases, which case, is there a case, + Compose case, Jetpack Compose. +--- + +# Query Cases — APIExample-Compose + +## How cases are registered + +Unlike APIExample, this project does NOT use reflection. Cases are manually registered in: + +`app/src/main/java/io/agora/api/example/compose/model/Examples.kt` + +Two lists define the groups: + +```kotlin +val BasicExampleList = listOf( + Example(R.string.example_join_channel_video) { JoinChannelVideo() }, + // … +) + +val AdvanceExampleList = listOf( + Example(R.string.example_live_streaming) { LiveStreaming() }, + // … +) +``` + +List position is display order — there is no `index` field. String keys use the `example_` prefix. + +--- + +## Query procedure + +### Step 1: Decide scope before scanning + +Before reading files, ask: +- **Looking for a specific API?** — read Composable KDoc comments for the API name; no need to read all files +- **Need to know current list positions?** — read Examples.kt only; positions are 1-based list indices +- **Listing all cases?** — read Examples.kt for the full registry, then resolve names from strings.xml + +### Step 2: Read ARCHITECTURE.md first + +Read `ARCHITECTURE.md` (the `samples/` section of the Directory Layout). It contains a pre-built index of all cases with group, position, display name, and key API — no file scanning needed for most queries. + +Use ARCHITECTURE.md as the primary source. Fall back to reading Examples.kt only when: +- The query requires data not in ARCHITECTURE.md (e.g. exact list position, `description` field) +- ARCHITECTURE.md appears stale (a case exists in Examples.kt but not in the doc) +- The output involves list position availability, duplicate registration checks, or "is this case already registered?" decisions — these must be validated from `Examples.kt` immediately before final output + +### Step 3: Read Examples.kt (fallback / position queries) + +File: `app/src/main/java/io/agora/api/example/compose/model/Examples.kt` + +Parse `BasicExampleList` and `AdvanceExampleList`. Each entry is: + +```kotlin +Example(R.string.example_your_case_name) { YourCaseName() } +``` + +Position in the list (1-based) is the display order. There is no `index` field and no disabled/commented-out mechanism equivalent to `//@Example`. + +### Step 4: Resolve display names + +Resolve `R.string.example_*` from `app/src/main/res/values/strings.xml`: +`R.string.example_video_snapshot` → `Video Snapshot` + +### Step 5: Read Composable KDoc for API mapping + +Case implementations are in `app/src/main/java/io/agora/api/example/compose/samples/`. The KDoc above each public Composable lists key APIs: + +```kotlin +/** + * Demonstrates how to take a snapshot of the local video stream. + * + * Key APIs used: + * - RtcEngine.takeSnapshot() + */ +@Composable +fun VideoSnapshot() { … } +``` + +Use this to answer "which case uses X?" without reading the full implementation. If no KDoc, scan the function body for the API name. + +### Step 6: Present results + +Full listing — table format: + +| Group | Position | Case Name | File | Key APIs | +|-------|----------|-----------|------|----------| +| Basic | 1 | Join Channel Video | JoinChannelVideo.kt | joinChannel(), setupLocalVideo() | +| Advanced | 3 | Video Snapshot | VideoSnapshot.kt | takeSnapshot() | + +For a specific query, return only matching rows. + +For a position query, list current entries in the target list and identify the next available slot: +> AdvanceExampleList has 12 entries → next position: 13 (append at end) + +Before returning any position/registration-conflict result, re-read `Examples.kt` and recompute from the current list entries. + +--- + +## NEVER + +- **NEVER** look for `@Example` annotations — this project uses manual registration in Examples.kt, not reflection. +- **NEVER** treat list position as a unique ID that must be gap-free — position is just list order; new cases always append at the end of the appropriate list. +- **NEVER** use the `item_` string prefix — Compose cases use `example_` prefix; `item_` belongs to APIExample. +- **NEVER** scan `nav_graph.xml` for case registration — Compose navigation is position-based and requires no nav graph changes. diff --git a/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md b/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md new file mode 100644 index 000000000..fd0b84bad --- /dev/null +++ b/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md @@ -0,0 +1,48 @@ +--- +name: review-case +description: > + Review an existing case implementation against project-specific red lines + and coding standards. Use after implementing or modifying a case. + Use when: reviewing a Compose case for correctness, checking red-line compliance, + verifying lifecycle and state patterns, auditing an existing Composable. + Keywords: review, audit, check, red lines, lifecycle, state, compliance, Compose. +--- + +# Review Case — APIExample-Compose + +Run through every item below before considering a case implementation complete. +Open the case's Composable source file and verify each point against the actual code. + +## Checklist + +### Teardown & Lifecycle + +- [ ] **leaveChannel before destroy in onDispose** — `engine.leaveChannel()` is called before `RtcEngine.destroy()` in the `onDispose` block. Destroying without leaving first leaks the channel session on the server side. + +- [ ] **DisposableEffect key is lifecycleOwner not Unit** — `DisposableEffect(lifecycleOwner)` not `DisposableEffect(Unit)`. Using `Unit` fires only once and won't clean up on back navigation; the `onDispose` block never re-executes when the lifecycle owner changes. + +### State Management + +- [ ] **rememberSaveable for channelName/isJoined/uid and remember for RtcEngine** — `channelName`, `isJoined`, `uid` use `rememberSaveable`; `RtcEngine` uses `remember`. `rememberSaveable` survives configuration changes (rotation); `RtcEngine` is not serializable and will crash if placed in `rememberSaveable`. + +### Threading + +- [ ] **Callbacks dispatch to main thread via coroutineScope.launch(Dispatchers.Main)** — `IRtcEngineEventHandler` callbacks that show `Toast`, `Dialog`, or `AlertDialog` dispatch to the main thread via `coroutineScope.launch(Dispatchers.Main)`. SDK callbacks arrive on a background thread; `Toast` and dialog APIs require the main thread or they throw `CalledFromWrongThreadException`. Note: simple Compose state mutations (e.g. `isJoined = true`) are thread-safe via the snapshot system and do **not** need main-thread dispatch. + +### Permissions + +- [ ] **Permission check before joinChannel** — Permission launcher (`rememberLauncherForActivityResult`) is called before `joinChannel()`. Joining without the required permissions (`RECORD_AUDIO`, and `CAMERA` for video cases) causes a silent failure — no error callback, just no audio/video. + +## If a Check Fails + +- `DisposableEffect(Unit)` is used — change key to `lifecycleOwner`, then verify back navigation triggers cleanup. +- `RtcEngine` stored in `rememberSaveable` or state fields in `remember` only — fix to `RtcEngine -> remember`, UI/session state -> `rememberSaveable`, then verify rotation. +- Toast/Dialog shown directly in callback — move UI-thread-only calls into `coroutineScope.launch(Dispatchers.Main)`. +- Permission launcher bypassed before `joinChannel()` — gate join flow behind permission callback and re-test denied/granted paths. + +## NEVER + +- **NEVER** approve a review when `DisposableEffect` key is `Unit` for case teardown logic. +- **NEVER** approve a review when `RtcEngine` uses `rememberSaveable`. +- **NEVER** treat Compose callback state safety as permission to call Toast/Dialog off main thread. +- **NEVER** skip rotation and back-navigation checks for lifecycle-sensitive Compose cases. diff --git a/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md b/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..3faecf254 --- /dev/null +++ b/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md @@ -0,0 +1,185 @@ +--- +name: upsert-case +description: > + Add a new API example case or modify an existing one in the APIExample-Compose Android demo — + creates or updates a Kotlin Composable file, registers or updates it in Examples.kt, and manages + string resources. Use when: adding a new Agora RTC API demo screen in Jetpack Compose, modifying + an existing case's implementation or registration, porting an existing APIExample case to Compose, + implementing a new feature example in Kotlin + Compose UI, registering a new entry in + BasicExampleList or AdvanceExampleList, or updating an existing case's strings or Examples.kt entry. + Kotlin only — no XML layouts, no Fragments. Keywords: add case, modify case, update case, + new composable, Examples.kt, BasicExampleList, AdvanceExampleList, APIExample-Compose, Compose case, + new screen, Jetpack Compose, RTC API example, upsert case. +--- + +# Upsert Case — APIExample-Compose + +## Adding a New Case + +Touch exactly 3 files (all paths relative to `app/src/main/`): + +| File | What to add | +|---|---| +| `java/.../compose/samples/YourCaseName.kt` | Composable file | +| `java/.../compose/model/Examples.kt` | 1 list entry | +| `res/values/strings.xml` | 1 string | + +No `nav_graph.xml` changes — navigation routes by list position automatically. + +--- + +### Step 1: Clarify before coding + +Before writing a single line, ask: +- **What API am I demonstrating?** — determines which existing case is the closest reference (`JoinChannelVideo.kt` for video, `JoinChannelAudio.kt` for audio) +- **Video or audio-only?** — determines permissions (`CAMERA` + `RECORD_AUDIO` vs `RECORD_AUDIO` only), whether `enableVideo()` and `VideoGrid` are needed +- **BasicExampleList or AdvanceExampleList?** — Basic for fundamental join/leave patterns; Advance for feature-specific APIs +- **List position?** — run `query-cases` skill to see current entries; list order is display order + +--- + +### Step 2: Create the Composable file + +**MANDATORY — READ ENTIRE FILE before writing any code**: +[`references/composable-template.kt`](references/composable-template.kt) + +Do NOT skip — the `SettingPreferences.getArea()`, `DisposableEffect` key, `rememberSaveable` vs `remember` rules, and `@Preview` placement are only fully shown there and are required in every case. + +**Do NOT load** any other reference files for this task. + +Non-obvious points the template highlights: + +- `mAreaCode = SettingPreferences.getArea()` — **required**, do not hardcode or omit +- `DisposableEffect(lifecycleOwner)` — key must be `lifecycleOwner`, not `Unit`; wrong key means cleanup never fires on back navigation +- `rememberSaveable` for channelName, isJoined, uid, videoIdList — survives rotation +- `remember` for RtcEngine — must NOT be `rememberSaveable` (engine is not serializable) +- `IRtcEngineEventHandler` callbacks can mutate Compose state directly — snapshot system is thread-safe, no `runOnUIThread()` needed +- `Toast`/`Dialog`/`AlertDialog` inside callbacks still need main thread — use `coroutineScope.launch(Dispatchers.Main) { }` +- `@Preview` goes on the **private** `*View` function only — never on the public stateful entry + +--- + +### Step 3: Register in Examples.kt + +File: `app/src/main/java/io/agora/api/example/compose/model/Examples.kt` + +```kotlin +val AdvanceExampleList = listOf( + // … existing entries … + Example(R.string.example_your_case_name) { YourCaseName() } +) +``` + +List order is display order — position determines where the case appears in the UI. + +--- + +### Step 4: Add string resource + +File: `app/src/main/res/values/strings.xml` + +```xml +Your Case Name +``` + +String key must use the `example_` prefix. No separate tips string needed (unlike APIExample). + +--- + +### Step 5: Update ARCHITECTURE.md + +Add one line to the case list in `ARCHITECTURE.md` under the correct directory section: + +``` +├── YourCaseName.kt # "Display Name" — key API description +``` + +Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans. + +--- + +## Modifying an Existing Case + +When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating: + +| What changed | Files to touch | +|---|---| +| Implementation logic (API calls, event handling, Compose state) | `java/.../compose/samples/CaseName.kt` | +| Display name | `res/values/strings.xml` | +| List group (Basic ↔ Advance) or position | `java/.../compose/model/Examples.kt` (move entry between lists or reorder) | +| Composable function rename | `CaseName.kt` (file + function name), `Examples.kt` (lambda reference), `ARCHITECTURE.md` | + +After making changes: + +1. **Verify `Examples.kt` entry consistency** — ensure the string resource reference, composable lambda, and list placement (`BasicExampleList` or `AdvanceExampleList`) still match the actual case. A mismatch causes the case to silently disappear from the list or render the wrong screen. +2. **Update `res/values/strings.xml`** if the display name changed. +3. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description. + +--- + +## Verify + +```bash +./gradlew assembleDebug +``` + +- [ ] Case appears in the correct group at the expected list position +- [ ] Tap navigates to the case screen +- [ ] Channel join succeeds and `isJoined` flips to `true` +- [ ] Press back — check Logcat for `RtcEngine.destroy` within ~2 seconds; if missing, `DisposableEffect` key is wrong or `onDispose` is incomplete +- [ ] Rotate screen — `channelName` and `isJoined` survive (`rememberSaveable` working) +- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description +- [ ] `Examples.kt` entry is consistent — string resource, composable lambda, and list placement match the actual case + +--- + +## When to Use a Spec Instead + +If the case meets any of the following criteria, create a Spec rather than using this skill directly: + +1. Involves coordinated calls across two or more Agora API modules +2. Requires a custom Composable layout not covered by the standard template above +3. Manages multiple channels or multiple engine instances +4. Requires a foreground Service or background coroutine coordination +5. Involves developing new shared components (shared Composables / utils) +6. Requires optional module integration (simpleFilter / streamEncrypt) + +If none apply → use this skill directly; no Spec needed. + +### Spec Requirements Document Must Include + +- List of APIs the case demonstrates +- User interaction flow description +- Expected RtcEngine lifecycle behavior +- Required permissions list + +### Spec Design Document Must Include + +- Target project identifier: `APIExample-Compose` +- Composable function structure design +- API call sequence (Mermaid sequence diagram recommended) +- State management plan (`remember` vs `rememberSaveable` boundaries) +- UI layout plan +- Integration points with existing shared components +- Case registration info: `Examples.kt` list entry, `strings.xml` key (`example_` prefix) — finalize during design to avoid conflicts +- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing entries +- Compose-specific checks: `DisposableEffect(lifecycleOwner)`, `rememberSaveable` vs `remember`, main-thread dispatch for Toast/Dialog +- Risk identification and mitigation (API compatibility, performance, permissions, thread safety, rotation/config changes) + +### Spec Task List Integration + +- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters +- Mark which sub-tasks require manual coding, and provide target file paths and change summaries +- Tasks for creating new shared Composables must come before case implementation tasks + +--- + +## NEVER + +- **NEVER** use XML layouts, `Fragment`, or `ViewBinding` — Compose only. +- **NEVER** use `remember` for channelName, isJoined, or uid — they must be `rememberSaveable` to survive rotation. +- **NEVER** use `rememberSaveable` for `RtcEngine` — it is not serializable and will crash on rotation. +- **NEVER** use `Unit` as the `DisposableEffect` key — it fires only once and won't clean up on back navigation. Always use `lifecycleOwner`. +- **NEVER** put `@Preview` on the public stateful function — it will crash because `LocalContext` and `LocalLifecycleOwner` are unavailable in preview. Only preview the private `*View` function. +- **NEVER** call `Toast`/`Dialog`/`AlertDialog` directly inside `IRtcEngineEventHandler` callbacks — they require the main thread. Use `coroutineScope.launch(Dispatchers.Main) { }`. +- **NEVER** hardcode `mAreaCode` — always use `SettingPreferences.getArea()`. diff --git a/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt new file mode 100644 index 000000000..29d8c0da3 --- /dev/null +++ b/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt @@ -0,0 +1,161 @@ +package io.agora.api.example.compose.samples + +import android.Manifest +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.tooling.preview.Preview +import io.agora.api.example.compose.data.SettingPreferences +import io.agora.api.example.compose.ui.common.ChannelNameInput +import io.agora.api.example.compose.utils.AgoraConfig +import io.agora.api.example.compose.utils.TokenUtils +import io.agora.rtc2.ChannelMediaOptions +import io.agora.rtc2.Constants +import io.agora.rtc2.IRtcEngineEventHandler +import io.agora.rtc2.RtcEngine +import io.agora.rtc2.RtcEngineConfig + +// For video cases, also import: +// import io.agora.api.example.compose.ui.common.VideoGrid +// import io.agora.rtc2.video.VideoCanvas +// import io.agora.rtc2.video.VideoEncoderConfiguration + +/** + * Demonstrates how to use [describe the feature here]. + * + * Key APIs used: + * - RtcEngine.yourApi() + */ +// PUBLIC stateful entry point — no @Preview here +@Composable +fun YourCaseName() { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + // rememberSaveable: survives rotation — use for channelName, isJoined, uid, videoIdList + var isJoined by rememberSaveable { mutableStateOf(false) } + var channelName by rememberSaveable { mutableStateOf("") } + var localUid by rememberSaveable { mutableIntStateOf(0) } + + // remember: survives recomposition but NOT rotation — use for RtcEngine, collections + val rtcEngine = remember { + RtcEngine.create(RtcEngineConfig().apply { + mAreaCode = SettingPreferences.getArea() // REQUIRED — do not hardcode + mContext = context + mAppId = AgoraConfig.getAppId() + mEventHandler = object : IRtcEngineEventHandler() { + // IRtcEngineEventHandler callbacks are safe to mutate Compose state directly — + // the snapshot system is thread-safe. No runOnUIThread() needed. + override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { + super.onJoinChannelSuccess(channel, uid, elapsed) + isJoined = true + localUid = uid + } + + override fun onLeaveChannel(stats: RtcStats?) { + super.onLeaveChannel(stats) + isJoined = false + } + + override fun onUserJoined(uid: Int, elapsed: Int) { + super.onUserJoined(uid, elapsed) + // add uid to videoIdList for video cases + } + + override fun onUserOffline(uid: Int, reason: Int) { + super.onUserOffline(uid, reason) + // remove uid from videoIdList for video cases + } + } + }).apply { + // feature-specific engine setup goes here + // e.g. enableVideo(); setVideoEncoderConfiguration(...) + } + } + + // MUST use lifecycleOwner as key — ensures cleanup fires when screen leaves composition + DisposableEffect(lifecycleOwner) { + onDispose { + if (isJoined) rtcEngine.leaveChannel() + RtcEngine.destroy() + // NOTE: Toast/Dialog/AlertDialog MUST be called on main thread. + // Inside onDispose this is fine. Inside IRtcEngineEventHandler callbacks, + // wrap with: coroutineScope.launch(Dispatchers.Main) { ... } + } + } + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { grantedMap -> + // Permission callbacks run on main thread — Toast is safe here + if (grantedMap.values.all { it }) { + TokenUtils.gen(channelName, 0) { token -> + val options = ChannelMediaOptions().apply { + channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING + clientRoleType = Constants.CLIENT_ROLE_BROADCASTER + publishMicrophoneTrack = true + // publishCameraTrack = true // add for video cases + } + rtcEngine.joinChannel(token, channelName, 0, options) + } + } + } + + // Delegate all UI to the private stateless View function + YourCaseNameView( + channelName = channelName, + isJoined = isJoined, + onJoinClick = { name -> + channelName = name + permissionLauncher.launch( + arrayOf(Manifest.permission.RECORD_AUDIO) + // add Manifest.permission.CAMERA for video cases + ) + }, + onLeaveClick = { rtcEngine.leaveChannel() } + ) +} + +// @Preview goes here on the PRIVATE stateless function — never on the stateful entry above +@Preview +@Composable +private fun YourCaseNamePreview() { + YourCaseNameView( + channelName = "test", + isJoined = false, + onJoinClick = {}, + onLeaveClick = {} + ) +} + +// PRIVATE stateless View — receives only plain data and lambdas, no engine/state +@Composable +private fun YourCaseNameView( + channelName: String, + isJoined: Boolean, + onJoinClick: (String) -> Unit, + onLeaveClick: () -> Unit +) { + Column(Modifier.fillMaxSize()) { + // feature-specific UI here + // For video cases: VideoGrid(videoIdList, setupVideo, ...) + ChannelNameInput( + channelName = channelName, + isJoined = isJoined, + onJoinClick = onJoinClick, + onLeaveClick = onLeaveClick + ) + } +} diff --git a/Android/APIExample-Compose/AGENTS.md b/Android/APIExample-Compose/AGENTS.md new file mode 100644 index 000000000..3e63c9283 --- /dev/null +++ b/Android/APIExample-Compose/AGENTS.md @@ -0,0 +1,38 @@ +# AGENTS.md — APIExample-Compose + +Jetpack Compose version of the API demo. Mirrors cases from `APIExample/` but uses +`@Composable` functions instead of Fragments + XML layouts. Kotlin only. + +## Build Commands + +```bash +./gradlew assembleDebug # build debug APK +./gradlew installDebug # build + install to connected device +./gradlew test # unit tests +./gradlew connectedAndroidTest # instrumented tests (device required) +``` + +## App ID Configuration + +See [README.md — Obtain an App Id](README.md#obtain-an-app-id). + +## Architecture Red Lines + +- Do NOT use XML layouts, `Fragment`, or `ViewBinding` — Compose only. +- Do NOT use `View`-based widgets directly in Compose UI — wrap with `AndroidView` if unavoidable. +- `RtcEngine` must be created inside `remember { }` and destroyed inside `DisposableEffect(lifecycleOwner) { onDispose { } }` — key must be `lifecycleOwner`, not `Unit`; wrong key means cleanup never fires on back navigation. +- Always call `rtcEngine.leaveChannel()` before `RtcEngine.destroy()` in `onDispose`. +- Permissions use `rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions())`. +- `IRtcEngineEventHandler` callbacks are safe to mutate Compose state directly (snapshot system is thread-safe). + +## Skills + +| Skill | Path | Description | +|-------|------|-------------| +| upsert-case | `.agent/skills/upsert-case/` | Add a new Compose case or modify an existing one | +| query-cases | `.agent/skills/query-cases/` | Query and browse existing Compose cases | +| review-case | `.agent/skills/review-case/` | Review a case against project red lines | + +## Further Reading + +- `ARCHITECTURE.md` — full directory layout, Composable case pattern, registration details diff --git a/Android/APIExample-Compose/ARCHITECTURE.md b/Android/APIExample-Compose/ARCHITECTURE.md new file mode 100644 index 000000000..3b70c3991 --- /dev/null +++ b/Android/APIExample-Compose/ARCHITECTURE.md @@ -0,0 +1,197 @@ +# ARCHITECTURE.md — APIExample-Compose + +## Directory Layout + +``` +APIExample-Compose/ +├── gradle.properties # rtc_sdk_version +├── AGENTS.md # Agent entry point — build commands, red lines, skill index +├── ARCHITECTURE.md # This file — directory layout, patterns, registration +├── .kiro/ +│ ├── hooks/ +│ │ └── build-on-task-complete.json # Runs assembleDebug after each spec task completes +│ ├── skills/ +│ │ ├── add-new-case/SKILL.md # Step-by-step guide for adding a new Compose case +│ │ └── query-cases/SKILL.md # Query existing cases by API, group, or list position +│ └── steering/ +│ ├── project-routing.md # Which sub-project to use; hard constraints (always included) +│ ├── coding-standards.md # RtcEngine lifecycle, Kotlin/Compose rules (always included) +│ └── complex-case-spec.md # Spec workflow for complex cases (manual inclusion) +└── app/src/main/ + ├── AndroidManifest.xml + ├── assets/ # Audio/video sample files + ├── res/ + │ └── values/strings.xml # Display name strings (prefix: example_*) + └── java/io/agora/api/example/compose/ + ├── APIExampleApp.kt # Application class + ├── MainActivity.kt # Single-Activity, sets content to NavGraph() + ├── NavGraph.kt # Compose Navigation host — home / settings / example + │ + ├── model/ + │ ├── Example.kt # data class: name: Int, content: @Composable + │ ├── Examples.kt # Hardcoded lists: BasicExampleList, AdvanceExampleList + │ └── Components.kt # Groups the two lists into Components for the home screen + │ + ├── samples/ # One .kt file per case — all @Composable + │ ├── JoinChannelVideoToken.kt # Basic: "Join Video Channel (With Token)" + │ ├── JoinChannelVideo.kt # Basic: "Join Video Channel" — canonical reference + │ ├── JoinChannelAudio.kt # Basic: "Join Audio Channel" + │ ├── LiveStreaming.kt # Advanced: "Live Streaming" — setClientRole + │ ├── RTMPStreaming.kt # Advanced: "RTMP Streaming" — push to CDN + │ ├── MediaMetadata.kt # Advanced: "Media Metadata" — send/receive metadata + │ ├── VoiceEffects.kt # Advanced: "Voice Effects" — voice beautifier/effects + │ ├── OriginAudioData.kt # Advanced: "Origin Audio Data" — raw audio processing + │ ├── CustomAudioSource.kt # Advanced: "Custom Audio Source" — push external audio + │ ├── CustomAudioRender.kt # Advanced: "Custom Audio Render" — pull audio rendering + │ ├── OriginVideoData.kt # Advanced: "Origin Video Data" — raw video processing + │ ├── CustomVideoSource.kt # Advanced: "Custom Video Source" — push external video + │ ├── CustomVideoRender.kt # Advanced: "Custom Video Render" — custom video rendering + │ ├── PictureInPicture.kt # Advanced: "Picture In Picture" — PiP mode + │ ├── JoinMultiChannel.kt # Advanced: "Join Multi Channel" — multi-channel join + │ ├── ChannelEncryption.kt # Advanced: "Channel Encryption" — built-in encryption + │ ├── PlayAudioFiles.kt # Advanced: "Play Audio Files" — audio mixing + │ ├── PreCallTest.kt # Advanced: "Pre Call Test" — network/device test + │ ├── MediaRecorder.kt # Advanced: "Media Recorder" — record media streams + │ ├── MediaPlayer.kt # Advanced: "Media Player" — play media files + │ ├── ScreenSharing.kt # Advanced: "Screen Sharing" — screen capture & share + │ ├── VideoProcessExtension.kt # Advanced: "Video Process Extension" — video filter + │ ├── RhythmPlayer.kt # Advanced: "Rhythm Player" — metronome playback + │ ├── LocalVideoTranscoding.kt # Advanced: "Local Video Transcoding" — local compositing + │ ├── SendDataStream.kt # Advanced: "Send Data Stream" — data channel messaging + │ ├── HostAcrossChannel.kt # Advanced: "Host Across Channel" — cross-channel relay + │ ├── SpatialSound.kt # Advanced: "Spatial Sound" — 3D spatial audio + │ + ├── ui/ + │ ├── home/ + │ │ └── Home.kt # Home screen — renders grouped example list + │ ├── example/ + │ │ ├── Example.kt # Wrapper screen: calls example.content(back) + │ │ └── ExampleItem.kt # Single row in the example list + │ ├── settings/ + │ │ └── Settings.kt # Settings screen (area, resolution, frame rate) + │ ├── common/ + │ │ ├── APIExampleScaffold.kt # Shared scaffold with top bar + │ │ ├── APIExampleTopAppBar.kt + │ │ └── Widgets.kt # ChannelNameInput, VideoGrid, VideoStatsInfo, etc. + │ └── theme/ + │ └── Theme.kt + │ + ├── data/ + │ └── SettingPreferences.kt # DataStore-backed settings (area, resolution, frame rate) + │ + └── utils/ + ├── TokenUtils.java # Fetches RTC tokens from Agora token server + ├── AudioFileReader.java + ├── AudioPlayer.java + ├── VideoFileReader.java + ├── FileUtils.java + ├── YUVUtils.java + ├── YuvFboProgram.java + ├── YuvUploader.java + └── GLTextureView.java +``` + +## Case Index + +| Case | Path | Key APIs | Description | +|------|------|----------|-------------| +| Join Video Channel (With Token) | `JoinChannelVideoToken.kt` | `joinChannel()`, `enableVideo()`, `setupLocalVideo()`, `setupRemoteVideo()` | Joins a video channel using a manually provided token instead of fetching one automatically | +| Join Video Channel | `JoinChannelVideo.kt` | `joinChannel()`, `enableVideo()`, `setupLocalVideo()`, `setupRemoteVideo()`, `setVideoEncoderConfiguration()` | Canonical reference for joining a video channel with token generation and basic video rendering | +| Join Audio Channel | `JoinChannelAudio.kt` | `joinChannel()`, `enableAudio()`, `setChannelProfile()`, `setAudioScenario()`, `setAudioProfile()`, `enableInEarMonitoring()` | Joins an audio-only channel with audio route, in-ear monitoring, and volume controls | +| Live Streaming | `LiveStreaming.kt` | `joinChannel()`, `enableVideo()`, `setClientRole()`, `setDualStreamMode()`, `setVideoScenario()`, `addVideoWatermark()`, `setVideoEncoderConfiguration()` | Demonstrates live streaming with client role switching, dual stream, watermark, and encoder options | +| RTMP Streaming | `RTMPStreaming.kt` | `joinChannel()`, `enableVideo()`, `startRtmpStreamWithTranscoding()`, `startRtmpStreamWithoutTranscoding()`, `stopRtmpStream()`, `updateRtmpTranscoding()` | Pushes a live stream to a CDN via RTMP with optional transcoding | +| Media Metadata | `MediaMetadata.kt` | `joinChannel()`, `enableVideo()`, `registerMediaMetadataObserver()` | Sends and receives video metadata through the IMetadataObserver interface | +| Voice Effects | `VoiceEffects.kt` | `joinChannel()`, `enableAudio()`, `setVoiceBeautifierPreset()`, `setVoiceConversionPreset()`, `setAudioEffectPreset()`, `setAudioEffectParameters()`, `setAINSMode()` | Applies voice beautifier, voice changer, style transformation, and noise suppression presets | +| Origin Audio Data | `OriginAudioData.kt` | `joinChannel()`, `enableAudio()`, `registerAudioFrameObserver()`, `setRecordingAudioFrameParameters()`, `setPlaybackAudioFrameParameters()` | Accesses and rewrites raw audio frames via the IAudioFrameObserver interface | +| Custom Audio Source | `CustomAudioSource.kt` | `joinChannel()`, `enableAudio()`, `createCustomAudioTrack()`, `pushExternalAudioFrame()`, `destroyCustomAudioTrack()`, `enableCustomAudioLocalPlayback()` | Pushes external audio from a file into a custom audio track | +| Custom Audio Render | `CustomAudioRender.kt` | `joinChannel()`, `enableAudio()`, `setExternalAudioSink()`, `pullPlaybackAudioFrame()` | Pulls remote audio frames and renders them through a custom AudioTrack player | +| Origin Video Data | `OriginVideoData.kt` | `joinChannel()`, `enableVideo()`, `registerVideoFrameObserver()` | Captures raw video frames via IVideoFrameObserver for screenshot functionality | +| Custom Video Source | `CustomVideoSource.kt` | `joinChannel()`, `enableVideo()`, `pushExternalVideoFrameById()` | Pushes external video frames in I420, NV21, NV12, or Texture2D format | +| Custom Video Render | `CustomVideoRender.kt` | `joinChannel()`, `enableVideo()`, `registerVideoFrameObserver()` | Renders remote video frames using a custom OpenGL renderer via IVideoFrameObserver | +| Picture In Picture | `PictureInPicture.kt` | `joinChannel()`, `enableVideo()`, `setupLocalVideo()`, `setupRemoteVideo()`, `enterPictureInPictureMode()` | Demonstrates Android Picture-in-Picture mode during a video call | +| Join Multi Channel | `JoinMultiChannel.kt` | `joinChannel()`, `joinChannelEx()`, `leaveChannelEx()`, `enableVideo()`, `setupRemoteVideoEx()`, `takeSnapshotEx()` | Joins two channels simultaneously using RtcEngineEx multi-channel APIs | +| Channel Encryption | `ChannelEncryption.kt` | `joinChannel()`, `enableVideo()`, `enableEncryption()` | Enables built-in media encryption before joining a channel | +| Play Audio Files | `PlayAudioFiles.kt` | `joinChannel()`, `enableAudio()`, `startAudioMixing()`, `stopAudioMixing()`, `playEffect()`, `preloadEffect()`, `setAudioProfile()` | Plays audio mixing and sound effect files with volume controls | +| Pre Call Test | `PreCallTest.kt` | `startLastmileProbeTest()`, `stopLastmileProbeTest()`, `startEchoTest()`, `stopEchoTest()`, `enableVideo()` | Runs network quality probe and audio/video echo tests before joining a channel | +| Media Recorder | `MediaRecorder.kt` | `joinChannel()`, `enableVideo()`, `createMediaRecorder()`, `startRecording()`, `stopRecording()` | Records local or remote media streams to MP4 files using AgoraMediaRecorder | +| Media Player | `MediaPlayer.kt` | `joinChannel()`, `enableVideo()`, `createMediaPlayer()`, `open()`, `play()`, `stop()`, `updateChannelMediaOptions()` | Plays media files and publishes the player track to the channel | +| Screen Sharing | `ScreenSharing.kt` | `joinChannel()`, `enableVideo()`, `startScreenCapture()`, `stopScreenCapture()`, `updateScreenCaptureParameters()`, `setScreenCaptureScenario()` | Captures and shares the device screen with scenario and audio options | +| Video Process Extension | `VideoProcessExtension.kt` | `joinChannel()`, `enableVideo()`, `setBeautyEffectOptions()`, `setLowlightEnhanceOptions()`, `setColorEnhanceOptions()`, `setVideoDenoiserOptions()`, `enableVirtualBackground()`, `enableExtension()` | Applies beauty filters, low-light enhancement, color enhancement, denoiser, and virtual background | +| Rhythm Player | `RhythmPlayer.kt` | `joinChannel()`, `startRhythmPlayer()`, `stopRhythmPlayer()`, `updateChannelMediaOptions()` | Plays a metronome beat track and publishes it to the channel | +| Local Video Transcoding | `LocalVideoTranscoding.kt` | `joinChannel()`, `enableVideo()`, `startLocalVideoTranscoder()`, `stopLocalVideoTranscoder()`, `startCameraCapture()`, `stopCameraCapture()` | Composites camera and media player streams into a single transcoded video | +| Send Data Stream | `SendDataStream.kt` | `joinChannel()`, `enableVideo()`, `createDataStream()`, `sendStreamMessage()` | Sends and receives real-time data messages through a data channel | +| Host Across Channel | `HostAcrossChannel.kt` | `joinChannel()`, `enableVideo()`, `startOrUpdateChannelMediaRelay()`, `stopChannelMediaRelay()`, `pauseAllChannelMediaRelay()`, `resumeAllChannelMediaRelay()` | Relays media streams from one channel to another for cross-channel hosting | +| Spatial Sound | `SpatialSound.kt` | `joinChannel()`, `enableAudio()`, `ILocalSpatialAudioEngine.initialize()`, `updateSelfPosition()`, `updateRemotePosition()`, `updatePlayerPositionInfo()` | Demonstrates 3D spatial audio with draggable sound source positioning | + +## Case Registration Mechanism + +Registration is **manual** — no reflection, no annotation scanning. + +**To add a case, edit exactly two files:** + +**1. `model/Examples.kt`** — append to `BasicExampleList` or `AdvanceExampleList`: +```kotlin +val AdvanceExampleList = listOf( + // … existing entries … + Example(R.string.example_my_new_case) { MyNewCase() } +) +``` + +**2. `samples/MyNewCase.kt`** — create the Composable: +```kotlin +@Composable +fun MyNewCase() { … } +``` + +No `nav_graph.xml`, no `@Example` annotation, no action ID. `NavGraph.kt` routes to cases by their +index in the list — the order in `Examples.kt` is the display order. + +## Composable Case Pattern + +Every case follows a two-function structure. `JoinChannelVideo.kt` is the canonical reference. + +``` +MyNewCase() ← public, stateful: owns RtcEngine, state, permissions + └── MyNewCaseView(...) ← private, stateless: receives data + lambdas, pure UI +``` + +**Engine creation and cleanup:** +```kotlin +val rtcEngine = remember { + RtcEngine.create(RtcEngineConfig().apply { + mContext = context + mAppId = AgoraConfig.getAppId() + mEventHandler = object : IRtcEngineEventHandler() { … } + }) +} +DisposableEffect(lifecycleOwner) { // key must be lifecycleOwner, not Unit + onDispose { + if (isJoined) rtcEngine.leaveChannel() + RtcEngine.destroy() + } +} +``` + +**Permissions:** +```kotlin +val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() +) { grantedMap -> + if (grantedMap.values.all { it }) { /* join channel */ } +} +// trigger: +permissionLauncher.launch(arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)) +``` + +**State rules:** +- `rememberSaveable` — values that must survive rotation (channelName, isJoined, uid) +- `remember` — objects that must not be recreated (RtcEngine, collections) +- `IRtcEngineEventHandler` callbacks can mutate Compose state directly — the snapshot system is thread-safe + +## Token Flow + +```kotlin +TokenUtils.gen(channelName, uid) { token -> + rtcEngine.joinChannel(token, channelName, uid, options) +} +``` diff --git a/Android/APIExample-Compose/CLAUDE.md b/Android/APIExample-Compose/CLAUDE.md new file mode 100644 index 000000000..2d1c323ad --- /dev/null +++ b/Android/APIExample-Compose/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +This project uses `AGENTS.md` instead of a `CLAUDE.md` file. + +Please see @AGENTS.md in this same directory and treat its content as the primary reference for this project. diff --git a/Android/APIExample-Compose/README.md b/Android/APIExample-Compose/README.md index e2ee7b506..e5941d0a1 100644 --- a/Android/APIExample-Compose/README.md +++ b/Android/APIExample-Compose/README.md @@ -23,15 +23,16 @@ To build and run the sample application, get an App Id: 3. Save the **App Id** from the Dashboard for later use. 4. Save the **App Certificate** from the Dashboard for later use. -5. Open `Android/APIExample-Compose` and edit the `local.properties` file. Update `YOUR APP ID` with your App Id, update `YOUR APP CERTIFICATE` with the main app certificate from dashboard. Note you can leave the certificate variable `null` if your project has not turned on security token. +5. Open `Android/APIExample-Compose` and edit the `local.properties` file in the project root. Update `YOUR APP ID` with your App Id. If your Agora project has App Certificate enabled and you want to use the sample's built-in token generation flow, update `YOUR APP CERTIFICATE` as well. ``` - // Agora APP ID. + sdk.dir=/path/to/Android/sdk AGORA_APP_ID=YOUR APP ID - // Agora APP Certificate. If the project does not have certificates enabled, leave this field blank. AGORA_APP_CERT=YOUR APP CERTIFICATE ``` +`AGORA_APP_ID` is required. If your project does not enable App Certificate, leave `AGORA_APP_CERT` blank. If you generate tokens on your own server, keep `AGORA_APP_CERT` empty on the client side and use the token-based examples to paste the token at runtime. + You are all set. Now connect your Android device and run the project. diff --git a/Android/APIExample-Compose/README.zh.md b/Android/APIExample-Compose/README.zh.md index 21769f0e2..36e8db7d0 100644 --- a/Android/APIExample-Compose/README.zh.md +++ b/Android/APIExample-Compose/README.zh.md @@ -23,15 +23,16 @@ 3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它 4. 复制后台的 **App 证书** 并备注,稍后启动应用时会用到它 -5. 打开 `Android/APIExample` 并编辑 `local.properties`,将你的 AppID 、App主证书 分别替换到 `Your App Id` 和 `YOUR APP CERTIFICATE` +5. 打开 `Android/APIExample-Compose` 并编辑项目根目录下的 `local.properties`,填入你的 App ID。如果你的 Agora 项目开启了 App Certificate,并且你希望使用示例内置的 token 生成功能,再填入 `YOUR APP CERTIFICATE` ``` - // 声网APP ID。 + sdk.dir=/path/to/Android/sdk AGORA_APP_ID=YOUR APP ID - // 声网APP证书。如果项目没有开启证书鉴权,这个字段留空。 AGORA_APP_CERT=YOUR APP CERTIFICATE ``` +`AGORA_APP_ID` 为必填项。如果你的项目没有开启 App Certificate,`AGORA_APP_CERT` 留空即可。如果你使用自己的服务端生成 token,建议不要在客户端填写 `AGORA_APP_CERT`,直接使用 token 方式的示例在运行时粘贴 token。 + 然后你就可以编译并运行项目了。 ## 联系我们 diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ChannelEncryption.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ChannelEncryption.kt index c0409085d..780aed6c5 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ChannelEncryption.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ChannelEncryption.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -64,7 +64,7 @@ fun ChannelEncryption() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioRender.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioRender.kt index 34bde5ec8..5cdb4540a 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioRender.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioRender.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.AudioGrid @@ -70,7 +70,7 @@ fun CustomAudioRender() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioSource.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioSource.kt index b8ed266d2..f0dc59a53 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioSource.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomAudioSource.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.AudioGrid @@ -57,7 +57,7 @@ fun CustomAudioSource() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoRender.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoRender.kt index b0eae0fe0..15eda975e 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoRender.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoRender.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -64,7 +64,7 @@ fun CustomVideoRender() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoSource.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoSource.kt index daabf75ba..98a0fa923 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoSource.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/CustomVideoSource.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -66,7 +66,7 @@ fun CustomVideoSource() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/HostAcrossChannel.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/HostAcrossChannel.kt index 32d51e53f..208e2a0da 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/HostAcrossChannel.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/HostAcrossChannel.kt @@ -21,7 +21,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -58,7 +58,7 @@ fun HostAcrossChannel() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelAudio.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelAudio.kt index b568104a3..ca8a2341b 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelAudio.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelAudio.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.AudioGrid @@ -59,7 +59,7 @@ fun JoinChannelAudio() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onAudioRouteChanged(routing: Int) { diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideo.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideo.kt index 97dd506db..4c1c4fca7 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideo.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideo.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -58,7 +58,7 @@ fun JoinChannelVideo() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideoToken.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideoToken.kt index 6856f33fa..1602b7602 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideoToken.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinChannelVideoToken.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -61,7 +61,7 @@ fun JoinChannelVideoToken() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinMultiChannel.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinMultiChannel.kt index c2b2c21ad..c0e018736 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinMultiChannel.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/JoinMultiChannel.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -63,7 +63,7 @@ fun JoinMultiChannel() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LiveStreaming.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LiveStreaming.kt index 85b580d7c..79f649c95 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LiveStreaming.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LiveStreaming.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -90,7 +90,7 @@ fun LiveStreaming() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LocalVideoTranscoding.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LocalVideoTranscoding.kt index 576028134..dca899af8 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LocalVideoTranscoding.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/LocalVideoTranscoding.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -63,7 +63,7 @@ fun LocalVideoTranscoding() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaMetadata.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaMetadata.kt index b5340b845..335571e50 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaMetadata.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaMetadata.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -62,7 +62,7 @@ fun MediaMetadata() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaPlayer.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaPlayer.kt index 941d56a11..eb17d1d27 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaPlayer.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaPlayer.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -68,7 +68,7 @@ fun MediaPlayer() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaRecorder.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaRecorder.kt index 5ec29865b..ea1828f05 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaRecorder.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/MediaRecorder.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.content.ContentProviderCompat.requireContext -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -70,7 +70,7 @@ fun MediaRecorder() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginAudioData.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginAudioData.kt index e545e6f6f..08339a7a0 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginAudioData.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginAudioData.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.AudioGrid @@ -61,7 +61,7 @@ fun OriginAudioData() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginVideoData.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginVideoData.kt index 8855afd69..5c81ea0e5 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginVideoData.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/OriginVideoData.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -75,7 +75,7 @@ fun OriginVideoData() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PictureInPicture.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PictureInPicture.kt index 19bcb9e45..5b3ff8064 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PictureInPicture.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PictureInPicture.kt @@ -43,7 +43,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.core.app.PictureInPictureModeChangedInfo import androidx.core.util.Consumer -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -163,7 +163,7 @@ fun PictureInPicture() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) @@ -354,74 +354,74 @@ fun PictureInPicture() { Log.d("PiPDebug", "PictureInPicture: Rendering normal mode - full UI") // Normal mode with full UI - let Example component handle the scaffold Column(modifier = Modifier.fillMaxWidth()) { - videoView() - Spacer(modifier = Modifier.weight(1f)) + videoView() + Spacer(modifier = Modifier.weight(1f)) - Button( - modifier = Modifier.padding(16.dp, 8.dp), - enabled = isJoined, - onClick = { - if (Build.VERSION.SDK_INT >= 26) { - val appOpsManager: AppOpsManager = - context.getSystemService(AppOpsManager::class.java) - if (appOpsManager.checkOpNoThrow( - AppOpsManager.OPSTR_PICTURE_IN_PICTURE, - Process.myUid(), - context.packageName - ) == AppOpsManager.MODE_ALLOWED - ) { - context.enterPictureInPictureMode( - PictureInPictureParams.Builder() - .setAspectRatio( - Rational( - videoViewBound.width().toInt(), - videoViewBound.height().toInt() + Button( + modifier = Modifier.padding(16.dp, 8.dp), + enabled = isJoined, + onClick = { + if (Build.VERSION.SDK_INT >= 26) { + val appOpsManager: AppOpsManager = + context.getSystemService(AppOpsManager::class.java) + if (appOpsManager.checkOpNoThrow( + AppOpsManager.OPSTR_PICTURE_IN_PICTURE, + Process.myUid(), + context.packageName + ) == AppOpsManager.MODE_ALLOWED + ) { + context.enterPictureInPictureMode( + PictureInPictureParams.Builder() + .setAspectRatio( + Rational( + videoViewBound.width().toInt(), + videoViewBound.height().toInt() + ) ) - ) - .setActions(emptyList()) // Hide system actions (back button, etc.) - .build() - ) - val homeIntent = Intent(Intent.ACTION_MAIN) - homeIntent.addCategory(Intent.CATEGORY_HOME) - context.startActivity(homeIntent) - // isPipOn is now managed by rememberIsInPipMode(), no need to manually set - } else { - Toast.makeText( - context, - "Picture-in-Picture permission is not granted", - Toast.LENGTH_SHORT - ).show() - } + .setActions(emptyList()) // Hide system actions (back button, etc.) + .build() + ) + val homeIntent = Intent(Intent.ACTION_MAIN) + homeIntent.addCategory(Intent.CATEGORY_HOME) + context.startActivity(homeIntent) + // isPipOn is now managed by rememberIsInPipMode(), no need to manually set } else { Toast.makeText( context, - "Picture-in-Picture requires Android 8.0 (API 26) or higher", + "Picture-in-Picture permission is not granted", Toast.LENGTH_SHORT ).show() } + } else { + Toast.makeText( + context, + "Picture-in-Picture requires Android 8.0 (API 26) or higher", + Toast.LENGTH_SHORT + ).show() } - ) { - Text(text = "Enter Picture-in-Picture Mode") } + ) { + Text(text = "Enter Picture-in-Picture Mode") + } - ChannelNameInput( - channelName = channelName, - isJoined = isJoined, - onJoinClick = { - channelName = it - keyboard?.hide() - permissionLauncher.launch( - arrayOf( - android.Manifest.permission.RECORD_AUDIO, - android.Manifest.permission.CAMERA - ) + ChannelNameInput( + channelName = channelName, + isJoined = isJoined, + onJoinClick = { + channelName = it + keyboard?.hide() + permissionLauncher.launch( + arrayOf( + android.Manifest.permission.RECORD_AUDIO, + android.Manifest.permission.CAMERA ) - }, - onLeaveClick = { - rtcEngine.stopPreview() - rtcEngine.leaveChannel() - } - ) + ) + }, + onLeaveClick = { + rtcEngine.stopPreview() + rtcEngine.leaveChannel() + } + ) } } } \ No newline at end of file diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PlayAudioFiles.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PlayAudioFiles.kt index 0119bc612..6b87d9853 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PlayAudioFiles.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PlayAudioFiles.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.AudioGrid @@ -63,7 +63,7 @@ fun PlayAudioFiles() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PreCallTest.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PreCallTest.kt index acbb38381..e0d7f265f 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PreCallTest.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/PreCallTest.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.VideoCell @@ -65,7 +65,7 @@ fun PreCallTest() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onLastmileQuality(quality: Int) { super.onLastmileQuality(quality) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RTMPStreaming.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RTMPStreaming.kt index ff62acb62..8c7cac0c2 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RTMPStreaming.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RTMPStreaming.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -62,7 +62,7 @@ fun RTMPStreaming() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RhythmPlayer.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RhythmPlayer.kt index 2d0baab9f..480688fa2 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RhythmPlayer.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/RhythmPlayer.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -50,7 +50,7 @@ fun RhythmPlayer() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ScreenSharing.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ScreenSharing.kt index 855a8a01a..ffedfe400 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ScreenSharing.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/ScreenSharing.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -82,7 +82,7 @@ fun ScreenSharing() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context.applicationContext - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SendDataStream.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SendDataStream.kt index ca77b3b22..189f11bfa 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SendDataStream.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SendDataStream.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -53,7 +53,7 @@ fun SendDataStream() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SpatialSound.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SpatialSound.kt index 2fc0e176c..631190434 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SpatialSound.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/SpatialSound.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -88,7 +88,7 @@ fun SpatialSound() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VideoProcessExtension.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VideoProcessExtension.kt index 2bb0038a3..f82b315d6 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VideoProcessExtension.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VideoProcessExtension.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.ChannelNameInput @@ -64,7 +64,7 @@ fun VideoProcessExtension() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VoiceEffects.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VoiceEffects.kt index f46e8a806..4866f7bb3 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VoiceEffects.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/VoiceEffects.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.R import io.agora.api.example.compose.data.SettingPreferences import io.agora.api.example.compose.ui.common.AudioGrid @@ -57,7 +57,7 @@ fun VoiceEffects() { RtcEngine.create(RtcEngineConfig().apply { mAreaCode = SettingPreferences.getArea() mContext = context - mAppId = BuildConfig.AGORA_APP_ID + mAppId = AgoraConfig.getAppId() mEventHandler = object : IRtcEngineEventHandler() { override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { super.onJoinChannelSuccess(channel, uid, elapsed) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AgoraConfig.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AgoraConfig.java new file mode 100644 index 000000000..2401bec0f --- /dev/null +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/AgoraConfig.java @@ -0,0 +1,22 @@ +package io.agora.api.example.compose.utils; + +import android.text.TextUtils; + +import io.agora.api.example.compose.BuildConfig; + +public final class AgoraConfig { + private AgoraConfig() { + } + + public static String getAppId() { + return BuildConfig.AGORA_APP_ID; + } + + public static String getAppCertificate() { + return BuildConfig.AGORA_APP_CERT; + } + + public static boolean hasAppCertificate() { + return !TextUtils.isEmpty(getAppCertificate()); + } +} diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java index e744eb40e..b7de5683e 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/utils/TokenUtils.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.Objects; -import io.agora.api.example.compose.BuildConfig; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; @@ -44,11 +43,11 @@ private TokenUtils() { } public static void genToken(String channelName, int uid, OnTokenGenCallback onGetToken) { - String cert = BuildConfig.AGORA_APP_CERT; + String cert = AgoraConfig.getAppCertificate(); if (cert.isEmpty()) { onGetToken.onTokenGen(""); } else { - gen(BuildConfig.AGORA_APP_ID, BuildConfig.AGORA_APP_CERT, channelName, uid, ret -> { + gen(AgoraConfig.getAppId(), AgoraConfig.getAppCertificate(), channelName, uid, ret -> { if (onGetToken != null) { runOnUiThread(() -> { onGetToken.onTokenGen(ret); @@ -73,7 +72,7 @@ public static void genToken(String channelName, int uid, OnTokenGenCallback onGetToken) { - gen(BuildConfig.AGORA_APP_ID, BuildConfig.AGORA_APP_CERT, channelName, uid, ret -> { + gen(AgoraConfig.getAppId(), AgoraConfig.getAppCertificate(), channelName, uid, ret -> { if (onGetToken != null) { runOnUiThread(() -> { onGetToken.onTokenGen(ret); diff --git a/Android/APIExample/.agent/skills/query-cases/SKILL.md b/Android/APIExample/.agent/skills/query-cases/SKILL.md new file mode 100644 index 000000000..ee1944957 --- /dev/null +++ b/Android/APIExample/.agent/skills/query-cases/SKILL.md @@ -0,0 +1,110 @@ +--- +name: query-cases +description: > + Query and browse existing API example cases in the APIExample Android demo — lists + cases by group, finds which case demonstrates a specific Agora API, checks sort + index availability, and resolves display names from string resources. Use when: + someone asks what cases exist, which APIs are demonstrated, wants to find a case + by name or API (e.g. takeSnapshot, setClientRole), needs a free sort index before + adding a new case, or wants to know if a feature is already implemented. + Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED, + available cases, existing cases, which case, is there a case. +--- + +# Query Cases — APIExample + +## How cases are registered + +Every case is a Fragment under `app/src/main/java/io/agora/api/example/examples/{basic|advanced|audio}/` with an `@Example` annotation: + +```java +@Example( + index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+ + group = ADVANCED, + name = R.string.item_xxx, + actionId = R.id.action_mainFragment_to_xxx, + tipsId = R.string.xxx_tips +) +``` + +A commented-out `@Example` (`//@Example`) means the case is disabled and won't appear in the app. + +--- + +## Query procedure + +### Step 1: Decide scope before scanning + +Before listing files, ask: +- **Looking for a specific API?** — scan Javadoc comments for the API name; no need to read all files +- **Need a free sort index?** — collect all `index` values for the target group, then find the gap +- **Listing all cases?** — scan all three directories and collect annotations + +### Step 2: Read ARCHITECTURE.md first + +Read `ARCHITECTURE.md` (the `examples/` section of the Directory Layout). It contains a pre-built index of all cases with group, index, display name, and key API — no file scanning needed for most queries. + +Use ARCHITECTURE.md as the primary source. Fall back to scanning the source directories only when: +- The query requires data not in ARCHITECTURE.md (e.g. full `@Example` field values, `tipsId`) +- ARCHITECTURE.md appears stale (a case exists in source but not in the doc) +- The output involves free-index claims, index collisions, or "is index X available?" decisions — these must be validated from source immediately before final output + +### Step 3: Scan case directories (fallback only) + +| Directory | Group | Contents | +|-----------|-------|----------| +| `examples/basic/` | BASIC | Core join/leave patterns | +| `examples/advanced/` | ADVANCED | Feature-specific APIs | +| `examples/audio/` | ADVANCED | Audio-specific cases (still grouped ADVANCED) | + +Each `.java` file is a case. Subdirectories (e.g. `customaudio/`) contain multi-file cases — the main class is the file whose name matches the directory name (e.g. `customaudio/CustomAudioSource.java`). If no name match, look for the file containing `@Example`. + +### Step 4: Extract `@Example` fields + +For each file, read the annotation for `group`, `index`, `name` (string resource ID), and `tipsId`. If the annotation is commented out, the case is disabled. + +Resolve display names from `app/src/main/res/values/strings.xml`: +`R.string.item_video_snapshot` → `Video Snapshot` + +### Step 5: Read class Javadoc for API mapping + +The Javadoc above each class lists the key APIs demonstrated: + +```java +/** + * This demo demonstrates how to take a snapshot of the local video stream. + * + * Key APIs used: + * - RtcEngine.takeSnapshot() + */ +``` + +Use this to answer "which case uses X?" queries without reading the full implementation. + +If no Javadoc is present, scan the method body for the API name as a method call. If still not found, note "API mapping unavailable" in the results table. + +### Step 6: Present results + +Full listing — table format: + +| Group | Index | Case Name | File | Key APIs | +|-------|-------|-----------|------|----------| +| BASIC | 0 | Join Channel Video | JoinChannelVideo.java | joinChannel(), setupLocalVideo() | +| ADVANCED | 10 | Video Snapshot | VideoSnapshot.java | takeSnapshot() | + +For a specific query (e.g. "which case uses takeSnapshot?"), return only matching rows. + +For a free-index query, list all used indices in the target group and identify the next available slot: +> BASIC range: 0–9. ADVANCED range: 10+. +> ADVANCED indices in use: 10, 11, 12, 15, 20 → next free: 13 + +Before returning any free-index/collision result, re-scan source registration points (`@Example` across `basic/`, `advanced/`, `audio/`) and recompute once from source-of-truth data. + +--- + +## NEVER + +- **NEVER** count a commented-out `@Example` (`//@Example`) as an active case — it is disabled and won't appear in the app. +- **NEVER** mix index spaces across groups — `audio/` cases use `group=ADVANCED` but share the same index namespace as `advanced/`; always scan both directories together when finding a free index. +- **NEVER** use filename alone to identify a subdirectory case — the main class is the file whose name matches the directory name; if no match, look for the file with `@Example`. +- **NEVER** report a free index without scanning all three directories (`basic/`, `advanced/`, `audio/`) for the target group — missing one causes index collisions. diff --git a/Android/APIExample/.agent/skills/review-case/SKILL.md b/Android/APIExample/.agent/skills/review-case/SKILL.md new file mode 100644 index 000000000..6caabc6ce --- /dev/null +++ b/Android/APIExample/.agent/skills/review-case/SKILL.md @@ -0,0 +1,52 @@ +--- +name: review-case +description: > + Review an existing case implementation against project-specific red lines + and coding standards. Use after implementing or modifying a case. + Use when: reviewing a case for correctness, checking red-line compliance, + verifying lifecycle and threading patterns, auditing an existing Fragment. + Keywords: review, audit, check, red lines, lifecycle, threading, compliance. +--- + +# Review Case — APIExample + +Run through every item below before considering a case implementation complete. +Open the case's Fragment source file and verify each point against the actual code. + +## Checklist + +### Teardown & Lifecycle + +- [ ] **leaveChannel before destroy** — `engine.leaveChannel()` is called before `RtcEngine.destroy()` in the teardown path (typically `onDestroy()`). Destroying without leaving first leaks the channel session on the server side. + +- [ ] **handler.post for destroy** — `RtcEngine.destroy()` is invoked via `handler.post(RtcEngine::destroy)` and **not** called directly on the main thread. A direct call blocks the UI thread and causes ANR. + +### Threading + +- [ ] **runOnUIThread for callbacks** — All `IRtcEngineEventHandler` callbacks that update UI are wrapped with `runOnUIThread()`. SDK callbacks arrive on a background thread; touching Views without dispatching to the main thread causes crashes or silent rendering corruption. + +### Permissions + +- [ ] **Permission check before join** — `checkOrRequestPermission()` is called before `joinChannel()`. Joining without the required permissions (RECORD_AUDIO, and CAMERA for video cases) causes a silent failure — no error callback, just no audio/video. + +### Backend Reporting + +- [ ] **setParameters present** — `setParameters(...)` is called during engine initialisation. This is required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally. + +### Private Cloud + +- [ ] **getPrivateCloudConfig null-check** — `getPrivateCloudConfig()` is null-checked before `setLocalAccessPoint()` is called. The method returns `null` on standard (non-private-cloud) builds, so calling `setLocalAccessPoint()` without the guard causes a NullPointerException. + +## If a Check Fails + +- Teardown order wrong (`destroy` before `leaveChannel`) — fix teardown to `leaveChannel()` first, then `handler.post(RtcEngine::destroy)`, and re-test back navigation. +- UI touched in SDK callback without main-thread dispatch — wrap UI updates in `runOnUIThread()` and re-run the case to verify no thread exceptions. +- Permission flow missing before `joinChannel()` — add `checkOrRequestPermission()` gate and verify join succeeds only after permission is granted. +- Missing `setParameters(...)` or private-cloud null-check — add both safeguards in engine init and re-run the init path once. + +## NEVER + +- **NEVER** approve a case review with direct `RtcEngine.destroy()` on main thread. +- **NEVER** approve a case review when `leaveChannel()` is missing before destroy. +- **NEVER** ignore background-thread UI updates inside `IRtcEngineEventHandler` callbacks. +- **NEVER** assume runtime behavior is correct without at least one back-navigation teardown check in Logcat. diff --git a/Android/APIExample/.agent/skills/upsert-case/SKILL.md b/Android/APIExample/.agent/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..25211787d --- /dev/null +++ b/Android/APIExample/.agent/skills/upsert-case/SKILL.md @@ -0,0 +1,342 @@ +--- +name: upsert-case +description: > + Add a new API example case or modify an existing one in the APIExample Android demo — + creates or updates Fragment class, XML layout, string resources, and nav_graph registration. + Use when: adding a new Agora RTC API demo screen, modifying an existing case's implementation + or registration, implementing a new feature example in Java + XML layouts, registering a new + case via @Example annotation, subclassing BaseFragment for a new demo screen, or updating + an existing case's strings, layout, or nav entry. Keywords: add case, modify case, update case, + new fragment, nav_graph, @Example, BaseFragment, APIExample, new screen, demo case, RTC API example. +--- + +# Upsert Case — APIExample + +## Adding a New Case + +Touch exactly 4 files (all paths relative to `app/src/main/`): + +| File | What to add | +|---|---| +| `java/.../examples/{basic\|advanced\|audio}/YourCaseName.java` | Fragment class | +| `res/layout/fragment_your_case_name.xml` | XML layout | +| `res/values/strings.xml` | 2 strings | +| `res/navigation/nav_graph.xml` | 1 action + 1 destination | + +Registration is automatic via reflection — no other files needed. + +--- + +### Step 1: Clarify before coding + +Before writing a single line, ask: +- **What API am I demonstrating?** — determines which existing case is the closest reference to copy patterns from +- **Video or audio-only?** — determines permissions (`CAMERA` + `RECORD_AUDIO` vs `RECORD_AUDIO` only), layout complexity, and whether `VideoReportLayout` is needed +- **BASIC or ADVANCED group?** — BASIC for fundamental channel join/leave patterns; ADVANCED for feature-specific APIs +- **What's the sort index?** — index must be unique within the group. BASIC uses 0–9, ADVANCED starts from 10. Run `query-cases` skill first; a collision causes silent ordering bugs at runtime + +--- + +### Step 2: Create the Fragment + +**MANDATORY — READ ENTIRE FILE before writing any code**: +[`references/fragment-template.java`](references/fragment-template.java) + +Do NOT skip — the `setParameters`, `handler.post`, and `getPrivateCloudConfig()` null-check patterns are only fully shown there and are required in every case. + +**Do NOT load** any other reference files for this task. + +Non-obvious points the template highlights: + +- `setParameters(...)` for app scenario reporting — **required in every case**, do not remove +- `handler.post(RtcEngine::destroy)` — NOT `RtcEngine.destroy()` directly; direct call blocks UI thread (ANR) +- `getPrivateCloudConfig()` null-check before `setLocalAccessPoint()` — returns null on non-private-cloud builds (NPE) +- All `IRtcEngineEventHandler` callbacks run on a **background thread** — always `runOnUIThread()` for UI +- `onActivityCreated` → create engine; `onDestroy` → `leaveChannel()` then `handler.post(RtcEngine::destroy)` + +For video cases, add `VideoReportLayout` fields and wire `setupRemoteVideo` in `onUserJoined`/`onUserOffline`. + +--- + +### Step 3: Create the XML layout + +Minimum structure — channel input + join button at bottom: + +```xml + + + + + + + + + + + + +``` + +For video cases, use `VideoReportLayout` for each video slot. Pick one of the four standard layouts below — they cover the vast majority of cases. + +**General rules (apply to all layouts):** +- Video containers must sit **above** the bottom control bar. In `RelativeLayout` use `android:layout_above="@id/ll_join"`; in `ConstraintLayout` use `app:layout_constraintBottom_toTopOf="@id/ll_join"`. +- Each `VideoReportLayout` needs a unique `android:id` (`fl_local`, `fl_remote`, `fl_remote2`, …). + +--- + +**Layout A — Single broadcaster (local fullscreen)** +Use when: broadcaster-only demo, no remote video needed. + +```xml + + +``` + +--- + +**Layout B — 1v1 (local left, remote right, side by side)** +Use when: two-party call, equal-weight split. + +```xml + + + + + + + +``` + +--- + +**Layout C — Audience co-hosting (remote fullscreen background + local PiP top-right)** +Use when: live streaming where audience co-hosts; remote/host fills screen, local is a small overlay. + +```xml + + + + +``` + +--- + +**Layout D — 2×2 grid (up to 4 participants)** +Use when: multi-party call with up to 4 streams. + +```xml + + + + + + + + + + + + + +``` + +--- + +### Step 4: Add nav entries + +File: `res/navigation/nav_graph.xml` + +**Action** — inside `` (NOT mainFragment — mainFragment only has one action, to Ready): + +```xml + +``` + +**Destination** — at root `` level: + +```xml + +``` + +`action android:id` must exactly match `actionId` in `@Example`. + +--- + +### Step 5: Update ARCHITECTURE.md + +Add one line to the case list in `ARCHITECTURE.md` under the correct directory section (`basic/`, `advanced/`, or `audio/`): + +``` +├── YourCaseName.java # [index] "Display Name" — key API description +``` + +Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans. + +--- + + +## Modifying an Existing Case + +When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating: + +| What changed | Files to touch | +|---|---| +| Implementation logic (API calls, event handling) | `java/.../examples/{basic\|advanced\|audio}/CaseName.java` | +| UI layout (views, controls, video containers) | `res/layout/fragment_case_name.xml` | +| Display name or tips text | `res/values/strings.xml` | +| Sort index or group (BASIC ↔ ADVANCED) | `@Example` annotation in the Fragment class | +| Navigation label | `res/navigation/nav_graph.xml` (fragment label attribute) | +| Class rename or package move | Fragment class, `nav_graph.xml` (android:name + destination id), `@Example` annotation (actionId), layout file name, `ARCHITECTURE.md` | + +After making changes: + +1. **Verify `@Example` annotation consistency** — ensure `index`, `group`, `name`, `actionId`, and `tipsId` still match the actual string resources, nav action ID, and intended group/position. A mismatch causes the case to silently disappear from the list or navigate to the wrong screen. +2. **Update `res/values/strings.xml`** if the display name or tips text changed. +3. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed. +4. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description. + +--- + +## Verify + +```bash +./gradlew assembleDebug +``` + +- [ ] Case appears in correct group at expected sort position +- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment) +- [ ] `onJoinChannelSuccess` fires in Logcat +- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy` +- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description +- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries + +--- + +## When to Use a Spec Instead + +If the case meets any of the following criteria, create a Spec rather than using this skill directly: + +1. Involves coordinated calls across two or more Agora API modules +2. Requires a custom UI layout (not one of the standard Layout A/B/C/D templates above) +3. Involves multi-channel or multi-engine instance management +4. Requires a foreground Service or background thread coordination +5. Involves developing new shared components (widgets/utils, etc.) +6. Requires optional module integration (simpleFilter/streamEncrypt) + +If none apply → use this skill directly; no Spec needed. + +### Spec Requirements Document Must Include + +- List of APIs the case demonstrates +- User interaction flow description +- Expected RtcEngine lifecycle behavior +- Required permissions list + +### Spec Design Document Must Include + +- Target project identifier: `APIExample` +- Class/file structure design +- API call sequence (Mermaid sequence diagram recommended) +- State management approach +- UI layout plan +- Integration points with existing shared components +- Case registration info: class name, display name, group (BASIC/ADVANCED), sort index — finalize during design to avoid conflicts +- Generate `@Example` annotation parameters, `nav_graph.xml` action + destination, `strings.xml` key names (`item_` prefix) +- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing indices +- Risk identification and mitigation (API compatibility, performance, permissions, thread safety) + +### Spec Task List Integration + +- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters +- Mark which sub-tasks require manual coding, and provide target file paths and change summaries +- Tasks for creating new shared components must come before case implementation tasks + +--- + +## NEVER + +- **NEVER** put the nav action inside `` — it belongs in ``. mainFragment only routes to Ready; all case actions live in Ready. Wrong placement causes silent navigation failure at runtime. +- **NEVER** call `RtcEngine.destroy()` directly on the main thread — always `handler.post(RtcEngine::destroy)`. Direct call blocks the UI thread and causes ANR. +- **NEVER** call `setLocalAccessPoint()` without null-checking `getPrivateCloudConfig()` first — it returns null on standard builds, causing NPE. +- **NEVER** update UI directly inside `IRtcEngineEventHandler` callbacks — they run on a background thread. Always wrap with `runOnUIThread()`. +- **NEVER** omit `setParameters(...)` — it's required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally. diff --git a/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java b/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java new file mode 100644 index 000000000..a96649654 --- /dev/null +++ b/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java @@ -0,0 +1,207 @@ +package io.agora.api.example.examples.advanced; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc2.Constants.RENDER_MODE_HIDDEN; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ConcurrentHashMap; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.widget.VideoReportLayout; +import io.agora.api.example.utils.PermissonUtils; +import io.agora.api.example.utils.TokenUtils; +import io.agora.rtc2.ChannelMediaOptions; +import io.agora.rtc2.Constants; +import io.agora.rtc2.IRtcEngineEventHandler; +import io.agora.rtc2.RtcEngine; +import io.agora.rtc2.RtcEngineConfig; +import io.agora.rtc2.proxy.LocalAccessPointConfiguration; +import io.agora.rtc2.video.VideoCanvas; + +/** + * This demo demonstrates how to use [describe the feature here]. + * + * Key APIs used: + * - RtcEngine.yourApi() + */ +@Example( + index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+ + group = ADVANCED, // BASIC or ADVANCED + name = R.string.item_your_case_name, + actionId = R.id.action_mainFragment_to_yourCaseName, + tipsId = R.string.your_case_name_tips +) +public class YourCaseName extends BaseFragment implements View.OnClickListener { + private static final String TAG = YourCaseName.class.getSimpleName(); + + // For video cases: add VideoReportLayout fields here + // private VideoReportLayout fl_local, fl_remote; + // private Map remoteViews = new ConcurrentHashMap<>(); + + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_your_case_name, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + join.setOnClickListener(this); + // bind additional views here + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Context context = getContext(); + if (context == null) return; + try { + RtcEngineConfig config = new RtcEngineConfig(); + config.mContext = context.getApplicationContext(); + config.mAppId = getAgoraAppId(); + config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; + config.mEventHandler = iRtcEngineEventHandler; + config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAreaCode = ((MainApplication) getActivity().getApplication()) + .getGlobalSettings().getAreaCode(); + engine = RtcEngine.create(config); + // REQUIRED in every case — do not remove + engine.setParameters("{" + + "\"rtc.report_app_scenario\":" + + "{" + + "\"appScenario\":" + 100 + "," + + "\"serviceType\":" + 11 + "," + + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" + + "}" + + "}"); + // null-check is mandatory — returns null on non-private-cloud builds + LocalAccessPointConfiguration localAccessPointConfiguration = + ((MainApplication) getActivity().getApplication()) + .getGlobalSettings().getPrivateCloudConfig(); + if (localAccessPointConfiguration != null) { + engine.setLocalAccessPoint(localAccessPointConfiguration); + } + } catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (engine != null) { + engine.leaveChannel(); + } + // MUST use handler.post — do NOT call RtcEngine.destroy() directly on main thread + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + String channelId = et_channel.getText().toString(); + checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { + @Override + public void onPermissionsResult(boolean allPermissionsGranted, + String[] permissions, int[] grantResults) { + if (allPermissionsGranted) { + joinChannel(channelId); + } + } + }); + } else { + joined = false; + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) { + engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); + // --- feature-specific setup goes here --- + // e.g. engine.enableVideo(); engine.setVideoEncoderConfiguration(...); + + ChannelMediaOptions options = new ChannelMediaOptions(); + options.autoSubscribeAudio = true; + options.autoSubscribeVideo = true; + options.publishMicrophoneTrack = true; + options.publishCameraTrack = true; // remove for audio-only cases + + int uid = new Random().nextInt(1000) + 100000; + TokenUtils.gen(requireContext(), channelId, uid, token -> { + int res = engine.joinChannel(token, channelId, uid, options); + if (res != 0) { + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + join.setEnabled(false); + }); + } + + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + // ALL UI updates must go through runOnUIThread — callbacks run on background thread + runOnUIThread(() -> { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + }); + } + + @Override + public void onUserJoined(int uid, int elapsed) { + Log.i(TAG, "onUserJoined -> " + uid); + runOnUIThread(() -> { + // For video cases: create SurfaceView, call engine.setupRemoteVideo(...) + }); + } + + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline, reason %d", uid, reason)); + runOnUIThread(() -> { + // For video cases: removeAllViews(), call engine.setupRemoteVideo(null, ...) + }); + } + + @Override + public void onError(int err) { + showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err)); + } + }; +} diff --git a/Android/APIExample/AGENTS.md b/Android/APIExample/AGENTS.md new file mode 100644 index 000000000..b5c9e8bd7 --- /dev/null +++ b/Android/APIExample/AGENTS.md @@ -0,0 +1,49 @@ +# AGENTS.md — APIExample + +Full demo project. Covers all Agora RTC APIs using Java/Kotlin + XML layouts. +Default project for video, screen sharing, beauty, or extension demos. + +## Build Commands + +```bash +./gradlew assembleDebug # build debug APK +./gradlew installDebug # build + install to connected device +./gradlew test # unit tests +./gradlew connectedAndroidTest # instrumented tests (device required) +``` + +## App ID Configuration + +See [README.md — Obtain an App Id](README.md#obtain-an-app-id). + +## Optional Modules + +Controlled via `gradle.properties`: +- `simpleFilter = true` — enables the C++ video extension module (`agora-simple-filter`). Requires OpenCV and Agora C++ SDK headers. See README for setup. +- `streamEncrypt = true` — enables the custom stream encryption module (`agora-stream-encrypt`). Requires Agora C++ SDK headers. See README for setup. + +Both are `false` by default. Do not enable unless the feature explicitly requires it. + +## Architecture Red Lines + +- Do NOT add audio-only cases that require `voice-sdk` exclusivity — use `APIExample-Audio/` instead. +- Do NOT use Jetpack Compose — this project is XML + ViewBinding only. +- Each case Fragment must create and destroy its own `RtcEngine` instance. +- Always call `engine.leaveChannel()` before `RtcEngine.destroy()` in `onDestroy()`. +- Call `RtcEngine.destroy()` via `handler.post(RtcEngine::destroy)` — direct call blocks the main thread (ANR). +- All `IRtcEngineEventHandler` callbacks run on a background thread — use `runOnUIThread()` for UI updates. +- Always call `checkOrRequestPermission()` before `joinChannel()`. +- `setParameters(...)` is required in every case for backend reporting — do not omit it. +- Always null-check `getPrivateCloudConfig()` before calling `setLocalAccessPoint()` — returns null on non-private-cloud builds. + +## Skills + +| Skill | Path | Description | +|-------|------|-------------| +| upsert-case | `.agent/skills/upsert-case/` | Add a new case or modify an existing one | +| query-cases | `.agent/skills/query-cases/` | Query and browse existing cases | +| review-case | `.agent/skills/review-case/` | Review a case against project red lines | + +## Further Reading + +- `ARCHITECTURE.md` — full directory layout, case registration internals, navigation details diff --git a/Android/APIExample/ARCHITECTURE.md b/Android/APIExample/ARCHITECTURE.md new file mode 100644 index 000000000..e541dfee6 --- /dev/null +++ b/Android/APIExample/ARCHITECTURE.md @@ -0,0 +1,215 @@ +# ARCHITECTURE.md — APIExample + +## Directory Layout + +``` +APIExample/ +├── gradle.properties # rtc_sdk_version, simpleFilter, streamEncrypt flags +├── agora-simple-filter/ # Optional C++ video extension module +├── agora-stream-encrypt/ # Optional custom stream encryption module +└── app/src/main/ + ├── AndroidManifest.xml + ├── assets/ # Audio/video sample files, beauty resources + ├── res/ + │ ├── navigation/nav_graph.xml # Single nav graph — all case destinations live here + │ ├── values/strings.xml # All display names and tips strings + │ └── layout/ # XML layouts for each case Fragment + └── java/io/agora/api/example/ + ├── MainApplication.java # Scans DEX and registers all @Example cases at startup + ├── MainActivity.java # Single-Activity host, owns NavController + ├── MainFragment.java # Home screen — renders BASIC / ADVANCED section list + ├── ReadyFragment.java # Splash / config check screen + ├── SettingActivity.java # Global settings (resolution, frame rate, area code) + │ + ├── annotation/ + │ └── Example.java # @Example annotation — the case registration contract + │ + ├── common/ + │ ├── BaseFragment.java # Base class ALL case Fragments must extend + │ ├── BaseVbFragment.java # ViewBinding variant of BaseFragment + │ ├── Constant.java # App-wide constants + │ ├── adapter/ + │ │ └── SectionAdapter.java # RecyclerView adapter for the grouped case list + │ ├── model/ + │ │ ├── Examples.java # Static registry: ITEM_MAP keyed by group name + │ │ ├── GlobalSettings.java # Video/audio config shared across cases + │ │ ├── ExampleBean.java + │ │ └── StatisticsInfo.java + │ ├── widget/ + │ │ ├── VideoReportLayout.java # Video container with stats overlay + │ │ ├── AudioOnlyLayout.java # Audio-only seat layout + │ │ ├── AudioSeatManager.java + │ │ └── WaveformView.java + │ ├── floatwindow/ # Floating window helper for in-call overlay + │ └── gles/ # OpenGL ES helpers for custom video rendering + │ + ├── examples/ # All cases live here — ClassUtils scans this package + │ ├── basic/ # group = "BASIC" (index 0–9) + │ │ ├── JoinChannelVideoByToken.java # [0] "Live Interactive Video Streaming(Token Verify)" + │ │ ├── JoinChannelVideo.java # [1] "Live Interactive Video Streaming" + │ │ └── JoinChannelAudio.java # [2] "Live Interactive Audio Streaming" + │ ├── advanced/ # group = "ADVANCED" (index 10+) + │ │ ├── LiveStreaming.java # [10] "RTC Live Streaming" — setClientRole, broadcaster/audience + │ │ ├── RTMPStreaming.java # [11] "Push Streams to CDN" — RTMP push streaming + │ │ ├── MediaMetadata.java # [12] "Media Metadata" — send/receive metadata in video stream + │ │ ├── VoiceEffects.java # [13] "Set the Voice Beautifier and Effects" — setVoiceBeautifierPreset + │ │ ├── customaudio/CustomAudioSource.java # [14] "Custom Audio Sources" — push external audio + │ │ ├── customaudio/CustomAudioRender.java # [15] "Custom Audio Render" — pull audio for custom rendering + │ │ ├── PushExternalVideoYUV.java # [16] "Custom Video Source" — push YUV external video + │ │ ├── CustomRemoteVideoRender.java # [17] "Custom Video Renderer" — custom remote video rendering + │ │ ├── ProcessAudioRawData.java # [18] "Raw Audio Data" — audio raw data processing + │ │ ├── MultiVideoSourceTracks.java # [19] "Multi Video Source Tracks" — multiple video sources + │ │ ├── ProcessRawData.java # [20] "Raw Video Data" — video raw data processing + │ │ ├── SimpleExtension.java # [21] "Simple Extension" — custom video extension + │ │ ├── PictureInPicture.java # [22] "Picture In Picture" — PiP mode + │ │ ├── FaceCapture.java # [23] "Face Capture" — face detection + │ │ ├── VideoQuickSwitch.java # [24] "Quick Switch Channel" — fast channel switching + │ │ ├── JoinMultipleChannel.java # [25] "Join Multiple Channel" — multi-channel join + │ │ ├── ChannelEncryption.java # [26] "Media Stream Encryption" — built-in encryption + │ │ ├── PlayAudioFiles.java # [27] "Play Audio Files" — audio mixing + │ │ ├── PreCallTest.java # [28] "Pre-call Tests" — network/device test before joining + │ │ ├── MediaPlayer.java # [29] "MediaPlayer" — play media files + │ │ ├── MediaRecorder.java # [30] "Local/Remote MediaRecorder" — record media streams + │ │ ├── ScreenSharing.java # [31] "Screen Sharing" — screen capture & share + │ │ ├── VideoProcessExtension.java # [32] "Video Process Extension" — video filter extension + │ │ ├── LocalVideoTranscoding.java # [33] "LocalVideoTranscoding" — local video compositing + │ │ ├── RhythmPlayer.java # [34] "Rhythm Player" — metronome/rhythm playback + │ │ ├── SendDataStream.java # [35] "Send Data Stream" — data channel messaging + │ │ ├── HostAcrossChannel.java # [36] "Relay Streams across Channels" — cross-channel relay + │ │ ├── SpatialSound.java # [37] "Spatial Audio" — 3D spatial audio + │ │ ├── ContentInspect.java # [38] "Content Inspect" — content moderation + │ │ ├── ThirdPartyBeauty.java # [39] "Third-party beauty" — third-party beauty SDK + │ │ ├── KtvCopyrightMusic.java # [40] "KTV Copyright Music" — licensed music + │ │ ├── TransparentRendering.java # [41] "TransparentRendering" — alpha channel rendering + │ │ ├── UrlLiveStream.java # [42] "Ultra Live Streaming with Url" — URL-based live stream + │ │ ├── AgoraBeauty.java # [43] "Agora beauty 2.0" — built-in beauty effects + │ │ ├── Simulcast.java # [44] "Simulcast" — multi-quality stream publishing + │ │ ├── Multipath.java # [45] "Multipath" — multi-path transmission + │ │ ├── beauty/ # Third-party beauty integrations + │ │ └── videoRender/ # Custom video rendering helpers + │ └── audio/ # Audio-specific cases (grouped as ADVANCED) + │ ├── AudioWaveform.java # [5] "Audio Waveform" — audio visualization + │ ├── AudioRouterPlayer.java # [6] "AudioRouter(Third Party Player)" — third-party audio routing + │ └── AudioRouterPlayer*.java # Exo / Ijk / Native variants + │ + ├── service/ + │ └── MediaProjectionService.java # Foreground service required for screen sharing + │ + └── utils/ + ├── ClassUtils.java # DEX scanner — auto-discovers @Example classes + ├── TokenUtils.java # Fetches RTC tokens from Agora token server + ├── PermissonUtils.java # Permission check/request helpers + ├── CommonUtil.java + ├── ErrorUtil.java + ├── FileUtils.java + ├── FileKtUtils.kt + ├── AudioFileReader.java + ├── VideoFileReader.java + └── YUVUtils.java +``` + +## Case Index + +| Case | Path | Key APIs | Description | +|------|------|----------|-------------| +| Live Interactive Video Streaming(Token Verify) | `basic/JoinChannelVideoByToken.java` | `RtcEngine.create()`, `joinChannel()`, `setupLocalVideo()`, `enableVideo()`, `setVideoEncoderConfiguration()` | Demonstrates one-to-one video calling with manual App ID and token input | +| Live Interactive Video Streaming | `basic/JoinChannelVideo.java` | `RtcEngine.create()`, `joinChannel()`, `setupLocalVideo()`, `enableVideo()`, `setVideoEncoderConfiguration()` | Demonstrates basic one-to-one video calling with auto-generated token | +| Live Interactive Audio Streaming | `basic/JoinChannelAudio.java` | `RtcEngine.create()`, `joinChannel()`, `setAudioProfile()`, `setAudioScenario()`, `muteLocalAudioStream()`, `enableInEarMonitoring()`, `adjustRecordingSignalVolume()`, `adjustPlaybackSignalVolume()` | Demonstrates audio-only calling with volume controls, in-ear monitoring, and audio routing | +| RTC Live Streaming | `advanced/LiveStreaming.java` | `setClientRole()`, `enableDualStreamMode()`, `startPreview()`, `preloadChannel()`, `enableInstantMediaRendering()`, `startMediaRenderingTracing()`, `addVideoWatermark()`, `setRemoteDefaultVideoStreamType()`, `takeSnapshot()`, `enableVideoImageSource()` | Demonstrates broadcaster/audience role switching with dual-stream, watermark, and snapshot features | +| Streaming from RTC to CDN | `advanced/RTMPStreaming.java` | `startRtmpStreamWithTranscoding()`, `startRtmpStreamWithoutTranscoding()`, `stopRtmpStream()`, `updateRtmpTranscoding()` | Demonstrates pushing media streams from RTC to a CDN via RTMP | +| Media Metadata | `advanced/MediaMetadata.java` | `registerMediaMetadataObserver()`, `sendAudioMetadata()` | Demonstrates sending and receiving metadata alongside video streams | +| Set the Voice Beautifier and Effects | `advanced/VoiceEffects.java` | `setVoiceBeautifierPreset()`, `setAudioEffectPreset()`, `setVoiceConversionPreset()`, `setAudioEffectParameters()`, `setLocalVoicePitch()`, `setLocalVoiceEqualization()`, `setLocalVoiceReverb()`, `setLocalVoiceFormant()`, `setAINSMode()`, `enableVoiceAITuner()` | Demonstrates voice beautifier presets, audio effects, voice conversion, and AI noise suppression | +| Custom Audio Sources | `advanced/customaudio/CustomAudioSource.java` | `createCustomAudioTrack()`, `pushExternalAudioFrame()`, `enableCustomAudioLocalPlayback()`, `destroyCustomAudioTrack()` | Demonstrates pushing external audio frames via a custom audio track | +| Custom Audio Render | `advanced/customaudio/CustomAudioRender.java` | `setExternalAudioSink()`, `pullPlaybackAudioFrame()` | Demonstrates pulling audio frames for custom audio rendering | +| Custom Video Source | `advanced/PushExternalVideoYUV.java` | `setExternalVideoSource()`, `pushExternalVideoFrame()`, `setExternalRemoteEglContext()` | Demonstrates pushing external YUV video frames as a custom video source | +| Custom Video Renderer | `advanced/CustomRemoteVideoRender.java` | `registerVideoFrameObserver()`, `setExternalRemoteEglContext()` | Demonstrates custom rendering of remote video streams via video frame observer | +| Raw Audio Data | `advanced/ProcessAudioRawData.java` | `registerAudioFrameObserver()`, `setRecordingAudioFrameParameters()`, `setPlaybackAudioFrameParameters()` | Demonstrates processing raw audio data through the audio frame observer | +| Multi Video Source Tracks | `advanced/MultiVideoSourceTracks.java` | `createCustomVideoTrack()`, `pushExternalVideoFrameById()`, `joinChannelEx()`, `destroyCustomVideoTrack()`, `createCustomEncodedVideoTrack()`, `pushExternalEncodedVideoFrameById()` | Demonstrates publishing multiple custom video tracks simultaneously | +| Raw Video Data | `advanced/ProcessRawData.java` | `registerVideoFrameObserver()`, `startPreview()` | Demonstrates processing raw video data through the video frame observer | +| Simple Extension | `advanced/SimpleExtension.java` | `enableExtension()`, `setExtensionProperty()`, `enableAudioVolumeIndication()` | Demonstrates loading and configuring a custom audio/video extension | +| Picture In Picture | `advanced/PictureInPicture.java` | `joinChannel()`, `setupLocalVideo()`, `enableVideo()` | Demonstrates Android Picture-in-Picture mode during a video call | +| Face Capture | `advanced/FaceCapture.java` | `enableExtension()`, `setExtensionProperty()`, `registerVideoFrameObserver()`, `registerFaceInfoObserver()` | Demonstrates face capture and lip-sync driven video using extensions | +| Quick Switch Channel | `advanced/VideoQuickSwitch.java` | `joinChannel()`, `leaveChannel()`, `startPreview()`, `setClientRole()` | Demonstrates fast channel switching for audience members | +| Join Multiple Channel | `advanced/JoinMultipleChannel.java` | `joinChannel()`, `joinChannelEx()`, `leaveChannelEx()`, `startPreview()`, `takeSnapshotEx()` | Demonstrates joining two channels simultaneously using RtcEngineEx | +| Media Stream Encryption | `advanced/ChannelEncryption.java` | `enableEncryption()` | Demonstrates built-in media stream encryption | +| Play Audio Files | `advanced/PlayAudioFiles.java` | `startAudioMixing()`, `stopAudioMixing()`, `pauseAudioMixing()`, `resumeAudioMixing()`, `getAudioEffectManager()`, `adjustAudioMixingVolume()` | Demonstrates audio mixing and sound effect playback | +| Pre-call Tests | `advanced/PreCallTest.java` | `startLastmileProbeTest()`, `stopLastmileProbeTest()`, `startEchoTest()`, `stopEchoTest()` | Demonstrates network quality probing and echo testing before joining a channel | +| MediaPlayer | `advanced/MediaPlayer.java` | `createMediaPlayer()`, `mediaPlayer.open()`, `mediaPlayer.play()`, `mediaPlayer.stop()`, `mediaPlayer.pause()`, `mediaPlayer.seek()`, `updateChannelMediaOptions()` | Demonstrates playing media files with the built-in media player | +| Local/Remote MediaRecorder | `advanced/MediaRecorder.java` | `createMediaRecorder()`, `destroyMediaRecorder()`, `startRecordingDeviceTest()` | Demonstrates recording local and remote media streams | +| Scree Sharing | `advanced/ScreenSharing.java` | `startScreenCapture()`, `stopScreenCapture()`, `updateScreenCaptureParameters()`, `setScreenCaptureScenario()` | Demonstrates screen capture and sharing during a video call | +| Video Enhancement | `advanced/VideoProcessExtension.java` | `setBeautyEffectOptions()`, `setFilterEffectOptions()`, `setLowlightEnhanceOptions()`, `setVideoDenoiserOptions()`, `setColorEnhanceOptions()`, `enableVirtualBackground()`, `setFaceShapeBeautyOptions()`, `setFaceShapeAreaOptions()` | Demonstrates built-in video enhancement including beauty, filter, denoising, and virtual background | +| LocalVideoTranscoding | `advanced/LocalVideoTranscoding.java` | `startLocalVideoTranscoder()`, `startCameraCapture()`, `startScreenCapture()`, `stopScreenCapture()`, `enableVirtualBackground()` | Demonstrates compositing multiple local video sources into a single stream | +| Rhythm Player | `advanced/RhythmPlayer.java` | `startRhythmPlayer()`, `stopRhythmPlayer()`, `enableAudioVolumeIndication()` | Demonstrates metronome/rhythm playback synchronized with audio streaming | +| Send Data Stream | `advanced/SendDataStream.java` | `createDataStream()`, `sendStreamMessage()` | Demonstrates sending and receiving data channel messages | +| Relay Streams across Channels | `advanced/HostAcrossChannel.java` | `startOrUpdateChannelMediaRelay()`, `stopChannelMediaRelay()`, `pauseAllChannelMediaRelay()`, `resumeAllChannelMediaRelay()` | Demonstrates relaying media streams from one channel to another | +| Spatial Audio | `advanced/SpatialSound.java` | `enableAudio()`, `setRemoteUserSpatialAudioParams()`, `createMediaPlayer()` | Demonstrates 3D spatial audio positioning for remote users | +| Content Inspect | `advanced/ContentInspect.java` | `enableContentInspect()` | Demonstrates real-time content moderation on video streams | +| Third-party beauty | `advanced/ThirdPartyBeauty.java` | `registerVideoFrameObserver()` | Demonstrates integration with third-party beauty SDKs (e.g. FaceUnity) | +| KTV Copyright Music | `advanced/KtvCopyrightMusic.java` | N/A (browser-based documentation link) | Demonstrates the KTV copyright music feature via documentation reference | +| TransparentRendering | `advanced/TransparentRendering.java` | `setExternalVideoSource()`, `pushExternalVideoFrame()`, `createMediaPlayer()`, `startPreview()` | Demonstrates alpha-channel transparent video rendering | +| Ultra Live Streaming with Url | `advanced/UrlLiveStream.java` | `Rte()`, `Player()`, `Canvas()`, `player.openWithUrl()`, `player.stop()` | Demonstrates ultra-low-latency live streaming playback via URL using the RTE SDK | +| Agora beauty 2.0 | `advanced/AgoraBeauty.java` | `enableVirtualBackground()`, `setFaceShapeAreaOptions()` | Demonstrates built-in Agora beauty effects with face shaping and virtual background | +| Simulcast | `advanced/Simulcast.java` | `setSimulcastConfig()`, `setRemoteVideoStreamType()` | Demonstrates publishing multiple quality streams with simulcast | +| Multipath | `advanced/Multipath.java` | `joinChannel()`, `updateChannelMediaOptions()` | Demonstrates multi-path transmission for improved network reliability | +| Audio Waveform | `audio/AudioWaveform.java` | `enableAudio()`, `enableAudioVolumeIndication()` | Demonstrates real-time audio waveform visualization | +| AudioRouter(Third Party Player) | `audio/AudioRouterPlayer.java` | `setEnableSpeakerphone()`, `joinChannel()` | Demonstrates audio routing with third-party media players (ExoPlayer, IjkPlayer, Native) | + +## Case Registration Mechanism + +Registration is **automatic via reflection**. No manual list to maintain. + +**Startup flow:** +1. `MainApplication.onCreate()` calls `ClassUtils.getFileNameByPackageName(context, "io.agora.api.example.examples")`. +2. `ClassUtils` scans all DEX entries whose class name starts with that prefix. +3. For each class, it checks for `@Example` annotation and calls `Examples.addItem(annotation)`. +4. `Examples.sortItem()` sorts each group by `index`. +5. `MainFragment` reads `Examples.ITEM_MAP` and renders the list. + +**`@Example` annotation — all four fields are required:** +```java +@Example( + index = 2, // sort order within the group; BASIC: 0–9, ADVANCED: 10+ + group = BASIC, // "BASIC" or "ADVANCED" + name = R.string.item_my_case, // display name string resource + actionId = R.id.action_mainFragment_to_myCase, // nav action ID in nav_graph.xml + tipsId = R.string.my_case_tips // description string resource +) +public class MyCase extends BaseFragment { … } +``` + +A missing or malformed annotation causes the case to silently not appear — no crash. + +## Navigation + +Single `nav_graph.xml` with Jetpack Navigation Component. + +Every case needs: +- A `` destination entry under the root `` in `nav_graph.xml` +- An `` inside `` — **not** `mainFragment`; `mainFragment` has only one action pointing to `Ready`, all case actions live in `Ready` +- The action `id` must exactly match `actionId` in `@Example` + +`MainActivity` calls `Navigation.findNavController(...).navigate(example.actionId())` on list item tap. + +## RtcEngine Lifecycle + +``` +onActivityCreated → RtcEngine.create() + → engine.setParameters / setVideoEncoderConfiguration + → joinChannel() (after permission granted) + ↓ + [IRtcEngineEventHandler callbacks — background thread] + ↓ +onDestroy → engine.leaveChannel() + → RtcEngine.destroy() + → engine = null +``` + +## Token Flow + +```java +TokenUtils.gen(requireContext(), channelId, uid, token -> { + engine.joinChannel(token, channelId, uid, options); +}); +``` + +`TokenUtils` reads `AGORA_APP_ID` and `AGORA_APP_CERT` from `local.properties` via `BuildConfig`. If `AGORA_APP_CERT` is empty, token generation is skipped — valid for projects without certificate. diff --git a/Android/APIExample/CLAUDE.md b/Android/APIExample/CLAUDE.md new file mode 100644 index 000000000..2d1c323ad --- /dev/null +++ b/Android/APIExample/CLAUDE.md @@ -0,0 +1,5 @@ +# CLAUDE.md + +This project uses `AGENTS.md` instead of a `CLAUDE.md` file. + +Please see @AGENTS.md in this same directory and treat its content as the primary reference for this project. diff --git a/Android/APIExample/README.md b/Android/APIExample/README.md index ee36e5f0c..fb949216c 100644 --- a/Android/APIExample/README.md +++ b/Android/APIExample/README.md @@ -23,16 +23,16 @@ To build and run the sample application, get an App Id: 3. Save the **App Id** from the Dashboard for later use. 4. Save the **App Certificate** from the Dashboard for later use. -5. Open `Android/APIExample` and edit the `app/src/main/res/values/string-configs.xml` file. Update `YOUR APP ID` with your App Id, update `YOUR APP CERTIFICATE` with the main app certificate from dashboard. Note you can leave the certificate variable `null` if your project has not turned on security token. +5. Open `Android/APIExample` and edit the `local.properties` file in the project root. Update `YOUR APP ID` with your App Id. If your Agora project has App Certificate enabled and you want to use the sample's built-in token generation flow, update `YOUR APP CERTIFICATE` as well. ``` - // Agora APP ID. - YOUR APP ID - // Agora APP Certificate. If the project does not have certificates enabled, leave this field blank. - // PS:It is unsafe to place the App Certificate on the client side, it is recommended to place it on the server side to ensure that the App Certificate is not leaked. - YOUR APP CERTIFICATE + sdk.dir=/path/to/Android/sdk + AGORA_APP_ID=YOUR APP ID + AGORA_APP_CERT=YOUR APP CERTIFICATE ``` +`AGORA_APP_ID` is required. If your project does not enable App Certificate, leave `AGORA_APP_CERT` blank. If you generate tokens on your own server, keep `AGORA_APP_CERT` empty on the client side and use the `ByToken` examples to paste the token at runtime. + You are all set. Now connect your Android device and run the project. ### Beauty Configuration @@ -68,22 +68,6 @@ follows: | sticker resource(e.g. fashi.bundle) | app/src/main/assets/beauty_faceunity/sticker | | authpack.java | app/src/main/java/io/agora/api/example/examples/advanced/beauty/authpack.java | -#### ByteDance - -1. Contact ByteDance customer service to obtain the download link and certificate of the beauty sdk -2. Unzip the ByteDance beauty resource and copy the following files/directories to the corresponding path - -| ByteDance Beauty Resources | Location | -|---------------------------------|--------------------------------------| -| resource/LicenseBag.bundle | app/src/main/assets/beauty_bytedance | -| resource/ModelResource.bundle | app/src/main/assets/beauty_bytedance | -| resource/ComposeMakeup.bundle | app/src/main/assets/beauty_bytedance | -| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance | -| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance | - -3. Modify the LICENSE_NAME in the app/src/main/java/io/agora/api/example/examples/advanced/beauty/ByteDanceBeauty.java file to the name of the applied certificate file. - - ### For Agora Extension Developers Since version 4.0.0, Agora SDK provides an Extension Interface Framework. Developers could publish their own video/audio extension to Agora Extension Market. In this project includes a sample SimpleFilter example, by default it is disabled. diff --git a/Android/APIExample/README.zh.md b/Android/APIExample/README.zh.md index 17d1953b2..1f804ea25 100644 --- a/Android/APIExample/README.zh.md +++ b/Android/APIExample/README.zh.md @@ -23,16 +23,16 @@ 3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它 4. 复制后台的 **App 证书** 并备注,稍后启动应用时会用到它 -5. 打开 `Android/APIExample` 并编辑 `app/src/main/res/values/string-configs.xml`,将你的 AppID 、App主证书 分别替换到 `Your App Id` 和 `YOUR APP CERTIFICATE` +5. 打开 `Android/APIExample` 并编辑项目根目录下的 `local.properties`,填入你的 App ID。如果你的 Agora 项目开启了 App Certificate,并且你希望使用示例内置的 token 生成功能,再填入 `YOUR APP CERTIFICATE` ``` - // 声网APP ID。 - YOUR APP ID - // 声网APP证书。如果项目没有开启证书鉴权,这个字段留空。 - // 注意:App证书放在客户端不安全,推荐放在服务端以确保 App 证书不会泄露。 - YOUR APP CERTIFICATE + sdk.dir=/path/to/Android/sdk + AGORA_APP_ID=YOUR APP ID + AGORA_APP_CERT=YOUR APP CERTIFICATE ``` +`AGORA_APP_ID` 为必填项。如果你的项目没有开启 App Certificate,`AGORA_APP_CERT` 留空即可。如果你使用自己的服务端生成 token,建议不要在客户端填写 `AGORA_APP_CERT`,直接使用 `ByToken` 系列示例在运行时粘贴 token。 + 然后你就可以编译并运行项目了。 ### 美颜配置 @@ -65,22 +65,6 @@ | 贴纸资源(如fashi.bundle) | app/src/main/assets/beauty_faceunity/sticker | | 证书authpack.java | app/src/main/java/io/agora/api/example/examples/advanced/beauty/authpack.java | -#### 字节美颜 - -1. 联系字节客服获取美颜sdk下载链接以及证书 -2. 解压字节/火山美颜资源并复制以下文件/目录到对应路径下 - -| 字节SDK文件/目录 | 项目路径 | -|--------------------------------------------------|-------------------------------------------------------| -| resource/LicenseBag.bundle | app/src/main/assets/beauty_bytedance | -| resource/ModelResource.bundle | app/src/main/assets/beauty_bytedance | -| resource/ComposeMakeup.bundle | app/src/main/assets/beauty_bytedance | -| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance | -| resource/StickerResource.bundle | app/src/main/assets/beauty_bytedance | - -3. -修改app/src/main/java/io/agora/api/example/examples/advanced/beauty/ByteDanceBeauty.java文件里LICENSE_NAME为申请到的证书文件名 - ### 对于Agora Extension开发者 从4.0.0SDK开始,Agora SDK支持插件系统和开放的云市场帮助开发者发布自己的音视频插件,本项目包含了一个SimpleFilter示例,默认是禁用的状态,如果需要开启编译和使用需要完成以下步骤: diff --git a/Android/APIExample/agora-simple-filter/src/main/agoraLibs b/Android/APIExample/agora-simple-filter/src/main/agoraLibs new file mode 120000 index 000000000..a4d04c322 --- /dev/null +++ b/Android/APIExample/agora-simple-filter/src/main/agoraLibs @@ -0,0 +1 @@ +../../../../../sdk \ No newline at end of file diff --git a/Android/APIExample/agora-stream-encrypt/src/main/agoraLibs b/Android/APIExample/agora-stream-encrypt/src/main/agoraLibs new file mode 120000 index 000000000..a4d04c322 --- /dev/null +++ b/Android/APIExample/agora-stream-encrypt/src/main/agoraLibs @@ -0,0 +1 @@ +../../../../../sdk \ No newline at end of file diff --git a/Android/APIExample/app/build.gradle b/Android/APIExample/app/build.gradle index f12013ca8..adffb056b 100644 --- a/Android/APIExample/app/build.gradle +++ b/Android/APIExample/app/build.gradle @@ -12,6 +12,18 @@ sdkVersionFile.withInputStream { stream -> def agoraSdkVersion = properties.getProperty("rtc_sdk_version") println("${rootProject.project.name} agoraSdkVersion: ${agoraSdkVersion}") def localSdkPath= "${rootProject.projectDir.absolutePath}/../../sdk" +def localPropertiesFile = rootProject.file("local.properties") +def localProperties = new Properties() +if (localPropertiesFile.exists()) { + localPropertiesFile.withInputStream { stream -> + localProperties.load(stream) + } +} +def agoraAppId = localProperties.getProperty("AGORA_APP_ID", "") +if (agoraAppId.isEmpty()) { + throw new GradleException("Please configure correctly in the local.properties file in the project root directory: AGORA_APP_ID=") +} +def agoraAppCert = localProperties.getProperty("AGORA_APP_CERT", "") android { @@ -26,6 +38,8 @@ android { versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk.abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86-64' + buildConfigField "String", "AGORA_APP_ID", "\"${agoraAppId}\"" + buildConfigField "String", "AGORA_APP_CERT", "\"${agoraAppCert}\"" } signingConfigs { diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java index 6d0c899ed..2edb8c468 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java @@ -20,6 +20,7 @@ import java.util.Map; +import io.agora.api.example.utils.AgoraConfig; import io.agora.api.example.utils.PermissonUtils; /** @@ -182,6 +183,14 @@ protected final void runOnUIThread(Runnable runnable, long delay) { } } + protected final String getAgoraAppId() { + return AgoraConfig.getAppId(); + } + + protected final String getAgoraAppCertificate() { + return AgoraConfig.getAppCertificate(); + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseVbFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseVbFragment.java index efba51360..5af6f98dc 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseVbFragment.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseVbFragment.java @@ -62,7 +62,7 @@ protected RtcEngine initRtcEngine(IRtcEngineEventHandler engineEventHandler) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AgoraBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AgoraBeauty.java index 99c40b66e..a48f07d71 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AgoraBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AgoraBeauty.java @@ -54,7 +54,7 @@ * The type Agora beauty. */ @Example( - index = 27, + index = 43, group = ADVANCED, name = R.string.item_agora_beauty, actionId = R.id.action_mainFragment_agora_beauty, @@ -288,7 +288,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/AudienceFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/AudienceFragment.java deleted file mode 100644 index 96bbd8d8b..000000000 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/AudienceFragment.java +++ /dev/null @@ -1,593 +0,0 @@ -package io.agora.api.example.examples.advanced.CDNStreaming; - -import static io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER; -import static io.agora.rtc2.Constants.RENDER_MODE_HIDDEN; -import static io.agora.rtc2.video.VideoEncoderConfiguration.STANDARD_BITRATE; - -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.CompoundButton; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.Spinner; -import android.widget.Switch; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import io.agora.api.example.MainApplication; -import io.agora.api.example.R; -import io.agora.api.example.common.BaseFragment; -import io.agora.mediaplayer.IMediaPlayer; -import io.agora.mediaplayer.IMediaPlayerObserver; -import io.agora.mediaplayer.data.CacheStatistics; -import io.agora.mediaplayer.data.PlayerPlaybackStats; -import io.agora.mediaplayer.data.PlayerUpdatedInfo; -import io.agora.mediaplayer.data.SrcInfo; -import io.agora.rtc2.ChannelMediaOptions; -import io.agora.rtc2.Constants; -import io.agora.rtc2.IRtcEngineEventHandler; -import io.agora.rtc2.RtcEngine; -import io.agora.rtc2.RtcEngineConfig; -import io.agora.rtc2.proxy.LocalAccessPointConfiguration; -import io.agora.rtc2.video.VideoCanvas; -import io.agora.rtc2.video.VideoEncoderConfiguration; - -/** - * The type Audience fragment. - */ -public class AudienceFragment extends BaseFragment implements IMediaPlayerObserver { - private static final String TAG = AudienceFragment.class.getSimpleName(); - private static final String AGORA_CHANNEL_PREFIX = "rtmp://pull.webdemo.agoraio.cn/lbhd/"; - private boolean isAgoraChannel = true; - private boolean rtcStreaming = false; - private String channel; - private FrameLayout fl_local, fl_remote, fl_remote_2, fl_remote_3; - private Map remoteViews = new ConcurrentHashMap(); - private LinearLayout rtc_control, video_row2, channel_control, vol_control; - private RtcEngine engine; - private IMediaPlayer mediaPlayer; - private SeekBar volSeekBar; - private Switch rtcSwitcher; - private Spinner channelSpinner; - private AlertDialog mPlayerFailDialog; - private AlertDialog mPlayerCompletedDialog; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_cdn_audience, container, false); - Bundle bundle = this.getArguments(); - isAgoraChannel = bundle.getBoolean(getString(R.string.key_is_agora_channel)); - channel = bundle.getString(getString(R.string.key_channel_name)); - return view; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - fl_local = view.findViewById(R.id.fl_local); - fl_remote = view.findViewById(R.id.fl_remote); - fl_remote_2 = view.findViewById(R.id.fl_remote2); - fl_remote_3 = view.findViewById(R.id.fl_remote3); - channel_control = view.findViewById(R.id.channel_ctrl); - channel_control.setVisibility(isAgoraChannel ? View.VISIBLE : View.INVISIBLE); - rtc_control = view.findViewById(R.id.rtc_ctrl); - rtc_control.setVisibility(isAgoraChannel ? View.VISIBLE : View.INVISIBLE); - vol_control = view.findViewById(R.id.vol_bar); - vol_control.setVisibility(View.INVISIBLE); - video_row2 = view.findViewById(R.id.video_container_row2); - rtcSwitcher = view.findViewById(R.id.rtc_switch); - rtcSwitcher.setOnCheckedChangeListener(checkedChangeListener); - volSeekBar = view.findViewById(R.id.record_vol); - volSeekBar.setOnSeekBarChangeListener(seekBarChangeListener); - channelSpinner = view.findViewById(R.id.channels_spinner); - channelSpinner.setOnItemSelectedListener(itemSelectedListener); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - try { - RtcEngineConfig config = new RtcEngineConfig(); - /* - * The context of Android Activity - */ - config.mContext = context.getApplicationContext(); - /* - * The App ID issued to you by Agora. See How to get the App ID - */ - config.mAppId = getString(R.string.agora_app_id); - /* Sets the channel profile of the Agora RtcEngine. - CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. - Use this profile in one-on-one calls or group calls, where all users can talk freely. - CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast - channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; - an audience can only receive streams.*/ - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - /* - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - config.mEventHandler = iRtcEngineEventHandler; - config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - /* - * This parameter is for reporting the usages of APIExample to agora background. - * Generally, it is not necessary for you to set this parameter. - */ - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - // Setup video encoding configs - engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( - ((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), - VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), - STANDARD_BITRATE, - VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) - )); - /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/ - LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - // This api can only be used in the private media server scenario, otherwise some problems may occur. - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - engine.enableVideo(); - //prepare media player - mediaPlayer = engine.createMediaPlayer(); - mediaPlayer.registerPlayerObserver(this); - SurfaceView surfaceView = new SurfaceView(this.getActivity()); - surfaceView.setZOrderMediaOverlay(false); - if (fl_local.getChildCount() > 0) { - fl_local.removeAllViews(); - } - fl_local.addView(surfaceView); - // Setup local video to render your local media player view - VideoCanvas videoCanvas = new VideoCanvas(surfaceView, Constants.RENDER_MODE_HIDDEN, 0); - videoCanvas.sourceType = Constants.VIDEO_SOURCE_MEDIA_PLAYER; - videoCanvas.mediaPlayerId = mediaPlayer.getMediaPlayerId(); - engine.setupLocalVideo(videoCanvas); - // Your have to call startPreview to see player video - engine.startPreview(); - // Set audio route to microPhone - engine.setDefaultAudioRoutetoSpeakerphone(true); - openPlayerWithUrl(); - } catch (Exception e) { - e.printStackTrace(); - getActivity().onBackPressed(); - } - } - - private void openPlayerWithUrl() { - if (isAgoraChannel) { - mediaPlayer.openWithAgoraCDNSrc(getUrl(), 0); - } else { - mediaPlayer.open(getUrl(), 0); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (rtcStreaming) { - engine.leaveChannel(); - } - mediaPlayer.stop(); - /*leaveChannel and Destroy the RtcEngine instance*/ - engine.stopPreview(); - handler.post(RtcEngine::destroy); - engine = null; - } - - - /** - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { - - /** - * Error code description can be found at: - * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - */ - @Override - public void onError(int err) { - Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); - } - - /**Occurs when a user leaves the channel. - * @param stats With this callback, the application retrieves the channel information, - * such as the call duration and statistics.*/ - @Override - public void onLeaveChannel(RtcStats stats) { - super.onLeaveChannel(stats); - } - - /**Occurs when the local user joins a specified channel. - * The channel name assignment is based on channelName specified in the joinChannel method. - * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. - * @param channel Channel name - * @param uid User ID - * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - handler.post(new Runnable() { - @Override - public void run() { - vol_control.setVisibility(View.VISIBLE); - volSeekBar.setProgress(100); - } - }); - } - - - /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. - * @param uid ID of the user whose audio state changes. - * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole - * until this callback is triggered.*/ - @Override - public void onUserJoined(int uid, int elapsed) { - super.onUserJoined(uid, elapsed); - Log.i(TAG, "onUserJoined->" + uid); - showLongToast(String.format("user %d joined!", uid)); - /*Check if the context is correct*/ - Context context = getContext(); - if (context == null) { - return; - } - if (remoteViews.containsKey(uid)) { - return; - } else { - handler.post(new Runnable() { - @Override - public void run() { - /*Display remote video stream*/ - SurfaceView surfaceView = null; - // Create render view by RtcEngine - surfaceView = new SurfaceView(context); - surfaceView.setZOrderMediaOverlay(true); - surfaceView.setZOrderOnTop(true); - ViewGroup view = getAvailableView(); - remoteViews.put(uid, view); - // Add to the remote container - view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - // Setup remote video to render - engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); - } - }); - } - } - - /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. - * @param uid ID of the user whose audio state changes. - * @param reason Reason why the user goes offline: - * USER_OFFLINE_QUIT(0): The user left the current channel. - * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data - * packet was received within a certain period of time. If a user quits the - * call and the message is not passed to the SDK (due to an unreliable channel), - * the SDK assumes the user dropped offline. - * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from - * the host to the audience.*/ - @Override - public void onUserOffline(int uid, int reason) { - Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); - showLongToast(String.format("user %d offline! reason:%d", uid, reason)); - handler.post(new Runnable() { - @Override - public void run() { - /*Clear render view - Note: The video will stay at its last frame, to completely remove it you will need to - remove the SurfaceView from its parent*/ - engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); - remoteViews.get(uid).removeAllViews(); - remoteViews.remove(uid); - } - }); - } - - @Override - public void onRtmpStreamingStateChanged(String url, int state, int errCode) { - super.onRtmpStreamingStateChanged(url, state, errCode); - showLongToast(String.format("onRtmpStreamingStateChanged state %s errCode %s", state, errCode)); - - } - }; - - private final SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { - - @Override - public void onProgressChanged(SeekBar seekBar, int i, boolean b) { - engine.adjustRecordingSignalVolume(i); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - - } - }; - - private final CompoundButton.OnCheckedChangeListener checkedChangeListener = new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - rtcStreaming = b; - if (rtcStreaming) { - ChannelMediaOptions channelMediaOptions = new ChannelMediaOptions(); - channelMediaOptions.publishMicrophoneTrack = true; - channelMediaOptions.publishCameraTrack = true; - channelMediaOptions.clientRoleType = CLIENT_ROLE_BROADCASTER; - int ret = engine.joinChannel(null, channel, 0, channelMediaOptions); - if (ret != 0) { - showLongToast(String.format("Join Channel call failed! reason:%d", ret)); - } - } else { - remoteViews.clear(); - engine.leaveChannel(); - vol_control.setVisibility(View.INVISIBLE); - } - handler.post(new Runnable() { - @Override - public void run() { - toggleVideoLayout(rtcStreaming); - } - }); - } - }; - - private void toggleVideoLayout(boolean isMultiple) { - if (isMultiple) { - fl_remote.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0.5f)); - fl_remote_2.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0.5f)); - fl_remote_3.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0.5f)); - video_row2.setLayoutParams(new LinearLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 0, 1)); - // Create render view by RtcEngine - SurfaceView surfaceView = new SurfaceView(getContext()); - if (fl_local.getChildCount() > 0) { - fl_local.removeAllViews(); - } - // Add to the local container - fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - // Setup local video to render your local camera preview - engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); - } else { - fl_remote.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0)); - fl_remote_2.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0)); - fl_remote_3.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0)); - video_row2.setLayoutParams(new LinearLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 0, 0)); - fl_remote.removeAllViews(); - fl_remote_2.removeAllViews(); - fl_remote_3.removeAllViews(); - SurfaceView surfaceView = new SurfaceView(getContext()); - surfaceView.setZOrderMediaOverlay(false); - if (fl_local.getChildCount() > 0) { - fl_local.removeAllViews(); - } - fl_local.addView(surfaceView); - // Setup local video to render your local media player view - VideoCanvas videoCanvas = new VideoCanvas(surfaceView, Constants.RENDER_MODE_HIDDEN, 0); - videoCanvas.sourceType = Constants.VIDEO_SOURCE_MEDIA_PLAYER; - videoCanvas.mediaPlayerId = mediaPlayer.getMediaPlayerId(); - engine.setupLocalVideo(videoCanvas); - } - engine.startPreview(); - } - - private ViewGroup getAvailableView() { - if (fl_remote.getChildCount() == 0) { - return fl_remote; - } else if (fl_remote_2.getChildCount() == 0) { - return fl_remote_2; - } else if (fl_remote_3.getChildCount() == 0) { - return fl_remote_3; - } else { - return fl_remote; - } - } - - private String getUrl() { - if (isAgoraChannel) { - return AGORA_CHANNEL_PREFIX + channel; - } else { - return channel; - } - } - - @Override - public void onPlayerStateChanged(io.agora.mediaplayer.Constants.MediaPlayerState state, io.agora.mediaplayer.Constants.MediaPlayerReason reason) { - showShortToast("player state change to " + state.name()); - handler.post(new Runnable() { - @Override - public void run() { - switch (state) { - case PLAYER_STATE_FAILED: - mediaPlayer.stop(); - //showLongToast(String.format("media player error: %s", mediaPlayerError.name())); - if (mPlayerFailDialog == null) { - mPlayerFailDialog = new AlertDialog.Builder(requireContext()) - .setTitle(R.string.tip) - .setCancelable(false) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - dialog.dismiss(); - onBackPressed(); - }) - .setPositiveButton(R.string.confirm, (dialog, which) -> openPlayerWithUrl()) - .create(); - } - mPlayerFailDialog.setMessage(getString(R.string.media_player_error, reason.name()) + "\n\n" + getString(R.string.reopen_url_again)); - mPlayerFailDialog.show(); - break; - case PLAYER_STATE_OPEN_COMPLETED: - mediaPlayer.play(); - if (isAgoraChannel) { - loadAgoraChannels(); - } - rtcSwitcher.setEnabled(true); - if (mPlayerFailDialog != null) { - mPlayerFailDialog.dismiss(); - } - break; - case PLAYER_STATE_PLAYBACK_COMPLETED: - if (mPlayerCompletedDialog == null) { - mPlayerCompletedDialog = new AlertDialog.Builder(requireContext()) - .setTitle(R.string.tip) - .setMessage(getString(R.string.media_player_complete) + "\n\n" + getString(R.string.reopen_url_again)) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - dialog.dismiss(); - onBackPressed(); - }) - .setCancelable(false) - .setPositiveButton(R.string.confirm, (dialog, which) -> { - mediaPlayer.stop(); - openPlayerWithUrl(); - }) - .create(); - } - mPlayerCompletedDialog.show(); - break; - case PLAYER_STATE_STOPPED: - default: - break; - } - } - }); - } - - private void loadAgoraChannels() { - int count = mediaPlayer.getAgoraCDNLineCount(); - ArrayAdapter arrayAdapter = new ArrayAdapter(getContext(), android.R.layout.simple_spinner_dropdown_item, getChannelArray(count)); - channelSpinner.setAdapter(arrayAdapter); - } - - private List getChannelArray(int count) { - List list = new ArrayList<>(); - for (int i = 0; i < count; i++) { - list.add("Channel" + (i + 1)); - } - return list; - } - - @Override - public void onPositionChanged(long positionMs, long timestampMs) { - - } - - @Override - public void onPlayerEvent(io.agora.mediaplayer.Constants.MediaPlayerEvent mediaPlayerEvent, long l, String s) { - Log.i(TAG, "onPlayerEvent " + mediaPlayerEvent.name()); - handler.post(new Runnable() { - @Override - public void run() { - switch (mediaPlayerEvent) { - case PLAYER_EVENT_SWITCH_COMPLETE: - showLongToast(String.format("player switch channel completed")); - break; - case PLAYER_EVENT_SWITCH_ERROR: - showLongToast(String.format("player switch channel failed: %s", s)); - break; - default: - break; - } - } - }); - } - - @Override - public void onMetaData(io.agora.mediaplayer.Constants.MediaPlayerMetadataType mediaPlayerMetadataType, byte[] bytes) { - - } - - @Override - public void onPlayBufferUpdated(long l) { - - } - - @Override - public void onPreloadEvent(String s, io.agora.mediaplayer.Constants.MediaPlayerPreloadEvent mediaPlayerPreloadEvent) { - - } - - - @Override - public void onAgoraCDNTokenWillExpire() { - - } - - @Override - public void onPlayerSrcInfoChanged(SrcInfo srcInfo, SrcInfo srcInfo1) { - - } - - @Override - public void onPlayerInfoUpdated(PlayerUpdatedInfo playerUpdatedInfo) { - - } - - @Override - public void onPlayerCacheStats(CacheStatistics stats) { - - } - - @Override - public void onPlayerPlaybackStats(PlayerPlaybackStats stats) { - - } - - @Override - public void onAudioVolumeIndication(int i) { - - } - - private final AdapterView.OnItemSelectedListener itemSelectedListener = new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - Log.i(TAG, "Start to switch cdn, current index is " + mediaPlayer.getAgoraCDNLineCount() + ". target index is " + i); - mediaPlayer.switchAgoraCDNLineByIndex(i); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - - } - }; - - @Override - protected void onBackPressed() { - - if (rtcSwitcher.isChecked()) { - rtcSwitcher.setChecked(false); - } else { - super.onBackPressed(); - } - } -} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/EntryFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/EntryFragment.java deleted file mode 100644 index 6d0488094..000000000 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/EntryFragment.java +++ /dev/null @@ -1,116 +0,0 @@ -//package io.agora.api.example.examples.advanced.CDNStreaming; -// -//import static io.agora.api.example.common.model.Examples.ADVANCED; -// -//import android.os.Bundle; -//import android.view.LayoutInflater; -//import android.view.View; -//import android.view.ViewGroup; -//import android.widget.AdapterView; -//import android.widget.EditText; -//import android.widget.Spinner; -// -//import androidx.annotation.NonNull; -//import androidx.annotation.Nullable; -//import androidx.navigation.Navigation; -// -//import io.agora.api.example.R; -//import io.agora.api.example.annotation.Example; -//import io.agora.api.example.common.BaseFragment; -//import io.agora.api.example.utils.PermissonUtils; -// -///** -// * The type Entry fragment. -// */ -//@Example( -// index = 2, -// group = ADVANCED, -// name = R.string.item_rtmpstreaming, -// actionId = R.id.action_mainFragment_to_CDNStreaming, -// tipsId = R.string.rtmpstreaming -//) -//public class EntryFragment extends BaseFragment implements View.OnClickListener { -// private static final String TAG = EntryFragment.class.getSimpleName(); -// private Spinner streamMode; -// private EditText et_channel; -// -// private boolean isAgoraChannel() { -// return "AGORA_CHANNEL".equals(streamMode.getSelectedItem().toString()); -// } -// -// private String getChannelName() { -// return et_channel.getText().toString(); -// } -// -// @Nullable -// @Override -// public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { -// View view = inflater.inflate(R.layout.fragment_cdn_entry, container, false); -// return view; -// } -// -// @Override -// public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { -// super.onViewCreated(view, savedInstanceState); -// view.findViewById(R.id.btn_host_join).setOnClickListener(this); -// view.findViewById(R.id.btn_audience_join).setOnClickListener(this); -// et_channel = view.findViewById(R.id.et_channel); -// streamMode = view.findViewById(R.id.streamModeSpinner); -// streamMode.setOnItemSelectedListener(new StreamModeOnItemSelectedListener()); -// } -// -// private final class StreamModeOnItemSelectedListener implements AdapterView.OnItemSelectedListener { -// @Override -// public void onItemSelected(AdapterView adapter, View view, int position, long id) { -// et_channel.setHint(position == 0 ? R.string.agora_channel_hint : R.string.cdn_url_hint); -// } -// -// @Override -// public void onNothingSelected(AdapterView arg0) { -// } -// } -// -// @Override -// public void onActivityCreated(@Nullable Bundle savedInstanceState) { -// super.onActivityCreated(savedInstanceState); -// } -// -// @Override -// public void onDestroy() { -// super.onDestroy(); -// } -// -// @Override -// public void onClick(View v) { -// // Check permission -// checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { -// @Override -// public void onPermissionsResult(boolean allPermissionsGranted, String[] permissions, int[] grantResults) { -// // Permissions Granted -// if (allPermissionsGranted) { -// join(v); -// } -// } -// }); -// } -// -// private void join(View v) { -// if (v.getId() == R.id.btn_host_join) { -// Bundle bundle = new Bundle(); -// bundle.putString(getString(R.string.key_channel_name), getChannelName()); -// bundle.putBoolean(getString(R.string.key_is_agora_channel), isAgoraChannel()); -// Navigation.findNavController(requireView()).navigate( -// R.id.action_cdn_streaming_to_host, -// bundle -// ); -// } else if (v.getId() == R.id.btn_audience_join) { -// Bundle bundle = new Bundle(); -// bundle.putString(getString(R.string.key_channel_name), getChannelName()); -// bundle.putBoolean(getString(R.string.key_is_agora_channel), isAgoraChannel()); -// Navigation.findNavController(requireView()).navigate( -// R.id.action_cdn_streaming_to_audience, -// bundle -// ); -// } -// } -//} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/HostFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/HostFragment.java deleted file mode 100644 index fc7cca2cf..000000000 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CDNStreaming/HostFragment.java +++ /dev/null @@ -1,569 +0,0 @@ -package io.agora.api.example.examples.advanced.CDNStreaming; - -import static io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER; -import static io.agora.rtc2.Constants.RENDER_MODE_HIDDEN; -import static io.agora.rtc2.video.VideoEncoderConfiguration.STANDARD_BITRATE; - -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.CompoundButton; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.SeekBar; -import android.widget.Switch; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import io.agora.api.example.MainApplication; -import io.agora.api.example.R; -import io.agora.api.example.common.BaseFragment; -import io.agora.rtc2.ChannelMediaOptions; -import io.agora.rtc2.Constants; -import io.agora.rtc2.DirectCdnStreamingMediaOptions; -import io.agora.rtc2.DirectCdnStreamingReason; -import io.agora.rtc2.DirectCdnStreamingState; -import io.agora.rtc2.DirectCdnStreamingStats; -import io.agora.rtc2.IDirectCdnStreamingEventHandler; -import io.agora.rtc2.IRtcEngineEventHandler; -import io.agora.rtc2.LeaveChannelOptions; -import io.agora.rtc2.RtcEngine; -import io.agora.rtc2.RtcEngineConfig; -import io.agora.rtc2.live.LiveTranscoding; -import io.agora.rtc2.proxy.LocalAccessPointConfiguration; -import io.agora.rtc2.video.CameraCapturerConfiguration; -import io.agora.rtc2.video.VideoCanvas; -import io.agora.rtc2.video.VideoEncoderConfiguration; - -/** - * The type Host fragment. - */ -public class HostFragment extends BaseFragment { - private static final String TAG = HostFragment.class.getSimpleName(); - private static final String AGORA_CHANNEL_PREFIX = "rtmp://push.webdemo.agoraio.cn/lbhd/"; - - private volatile boolean isAgoraChannel = true; - private volatile boolean cdnStreaming = false; - private volatile boolean rtcStreaming = false; - private String channel; - private FrameLayout fl_local, fl_remote, fl_remote_2, fl_remote_3; - private Map remoteViews = new ConcurrentHashMap(); - private LinearLayout rtc_control, video_row2; - private RtcEngine engine; - private LiveTranscoding liveTranscoding = new LiveTranscoding(); - private Button streamingButton; - private Switch rtcSwitcher; - private SeekBar volSeekBar; - private VideoEncoderConfiguration videoEncoderConfiguration; - private int canvas_width = 480; - private int canvas_height = 640; - private int localUid = (int) (Math.random() * Integer.MAX_VALUE / 2); - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_cdn_host, container, false); - Bundle bundle = this.getArguments(); - isAgoraChannel = bundle.getBoolean(getString(R.string.key_is_agora_channel)); - channel = bundle.getString(getString(R.string.key_channel_name)); - return view; - } - - @Override - public void onResume() { - super.onResume(); - getView().setFocusableInTouchMode(true); - getView().requestFocus(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - fl_local = view.findViewById(R.id.fl_local); - fl_remote = view.findViewById(R.id.fl_remote); - fl_remote_2 = view.findViewById(R.id.fl_remote2); - fl_remote_3 = view.findViewById(R.id.fl_remote3); - rtc_control = view.findViewById(R.id.rtc_ctrl); - rtc_control.setVisibility(isAgoraChannel ? View.VISIBLE : View.INVISIBLE); - video_row2 = view.findViewById(R.id.video_container_row2); - streamingButton = view.findViewById(R.id.streaming_btn); - streamingButton.setOnClickListener(streamingOnCLickListener); - rtcSwitcher = view.findViewById(R.id.rtc_switch); - rtcSwitcher.setOnCheckedChangeListener(checkedChangeListener); - volSeekBar = view.findViewById(R.id.record_vol); - volSeekBar.setOnSeekBarChangeListener(seekBarChangeListener); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - try { - - RtcEngineConfig config = new RtcEngineConfig(); - /* - * The context of Android Activity - */ - config.mContext = context.getApplicationContext(); - /* - * The App ID issued to you by Agora. See How to get the App ID - */ - config.mAppId = getString(R.string.agora_app_id); - /* Sets the channel profile of the Agora RtcEngine. - CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. - Use this profile in one-on-one calls or group calls, where all users can talk freely. - CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast - channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; - an audience can only receive streams.*/ - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - /* - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - config.mEventHandler = iRtcEngineEventHandler; - config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - /* - * This parameter is for reporting the usages of APIExample to agora background. - * Generally, it is not necessary for you to set this parameter. - */ - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/ - LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - // This api can only be used in the private media server scenario, otherwise some problems may occur. - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - - CameraCapturerConfiguration.CaptureFormat captureFormat = new CameraCapturerConfiguration.CaptureFormat(); - captureFormat.fps = 30; - engine.setCameraCapturerConfiguration(new CameraCapturerConfiguration(CameraCapturerConfiguration.CAMERA_DIRECTION.CAMERA_FRONT, captureFormat)); - - engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration()); - setupEngineConfig(context); - } catch (Exception e) { - e.printStackTrace(); - getActivity().onBackPressed(); - } - } - - private void setupEngineConfig(Context context) { - // setup local video to render local camera preview - SurfaceView surfaceView = new SurfaceView(context); - if (fl_local.getChildCount() > 0) { - fl_local.removeAllViews(); - } - // Add to the local container - fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - // Setup local video to render your local camera preview - engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); - // You have to call startPreview to see local video - engine.startPreview(); - // Set audio route to microPhone - engine.setDefaultAudioRoutetoSpeakerphone(true); - /*In the demo, the default is to enter as the anchor.*/ - engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); - // Enable video module - engine.enableVideo(); - // Setup video encoding configs - VideoEncoderConfiguration.VideoDimensions videoDimensions = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(); - canvas_height = Math.max(videoDimensions.height, videoDimensions.width); - canvas_width = Math.min(videoDimensions.height, videoDimensions.width); - videoEncoderConfiguration = new VideoEncoderConfiguration( - videoDimensions, VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15, STANDARD_BITRATE, VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT - ); - liveTranscoding.width = canvas_width; - liveTranscoding.height = canvas_height; - liveTranscoding.videoFramerate = 15; - engine.setVideoEncoderConfiguration(videoEncoderConfiguration); - engine.setDirectCdnStreamingVideoConfiguration(videoEncoderConfiguration); - } - - private void stopStreaming() { - rtcStreaming = false; - cdnStreaming = false; - rtcSwitcher.setChecked(false); - rtcSwitcher.setEnabled(false); - streamingButton.setText(getString(R.string.start_live_streaming)); - } - - private final View.OnClickListener streamingOnCLickListener = new View.OnClickListener() { - @Override - public void onClick(View view) { - if (rtcStreaming) { - engine.stopRtmpStream(getUrl()); - engine.leaveChannel(); - stopStreaming(); - } else if (cdnStreaming) { - engine.stopDirectCdnStreaming(); - engine.startPreview(); - rtcSwitcher.setChecked(false); - rtcSwitcher.setEnabled(false); - } else { - engine.setDirectCdnStreamingVideoConfiguration(videoEncoderConfiguration); - int ret = startCdnStreaming(); - if (ret == 0) { - streamingButton.setText(R.string.text_streaming); - } else { - showLongToast(String.format("startCdnStreaming failed! error code: %d", ret)); - } - } - } - }; - - private int startCdnStreaming() { - DirectCdnStreamingMediaOptions directCdnStreamingMediaOptions = new DirectCdnStreamingMediaOptions(); - directCdnStreamingMediaOptions.publishCameraTrack = true; - directCdnStreamingMediaOptions.publishMicrophoneTrack = true; - return engine.startDirectCdnStreaming(iDirectCdnStreamingEventHandler, getUrl(), directCdnStreamingMediaOptions); - } - - private String getUrl() { - if (isAgoraChannel) { - return AGORA_CHANNEL_PREFIX + channel; - } else { - return channel; - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (engine != null) { - if (rtcStreaming) { - engine.leaveChannel(); - } else if (cdnStreaming) { - engine.stopDirectCdnStreaming(); - } - /*leaveChannel and Destroy the RtcEngine instance*/ - engine.stopPreview(); - handler.post(RtcEngine::destroy); - engine = null; - } - } - - /** - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { - - /** - * Error code description can be found at: - * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - */ - @Override - public void onError(int err) { - Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); - } - - /**Occurs when a user leaves the channel. - * @param stats With this callback, the application retrieves the channel information, - * such as the call duration and statistics.*/ - @Override - public void onLeaveChannel(RtcStats stats) { - super.onLeaveChannel(stats); - } - - /**Occurs when the local user joins a specified channel. - * The channel name assignment is based on channelName specified in the joinChannel method. - * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. - * @param channel Channel name - * @param uid User ID - * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - localUid = uid; - LiveTranscoding.TranscodingUser user = new LiveTranscoding.TranscodingUser(); - user.x = 0; - user.y = 0; - user.width = canvas_width; - user.height = canvas_height; - user.uid = localUid; - liveTranscoding.addUser(user); - // engine.updateRtmpTranscoding(liveTranscoding); - int ret = engine.startRtmpStreamWithTranscoding(getUrl(), liveTranscoding); - if (ret != 0) { - showLongToast(String.format(Locale.US, "startRtmpStreamWithTranscoding failed! reason:%d", ret)); - } - } - - - /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. - * @param uid ID of the user whose audio state changes. - * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole - * until this callback is triggered.*/ - @Override - public void onUserJoined(int uid, int elapsed) { - super.onUserJoined(uid, elapsed); - Log.i(TAG, "onUserJoined->" + uid); - showLongToast(String.format("user %d joined!", uid)); - /*Check if the context is correct*/ - Context context = getContext(); - if (context == null) { - return; - } - - if (remoteViews.containsKey(uid)) { - return; - } else { - handler.post(new Runnable() { - @Override - public void run() { - /*Display remote video stream*/ - SurfaceView surfaceView = null; - // Create render view by RtcEngine - surfaceView = new SurfaceView(context); - surfaceView.setZOrderMediaOverlay(true); - ViewGroup view = getAvailableView(); - remoteViews.put(uid, view); - // Add to the remote container - view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - // Setup remote video to render - engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); - updateTranscodeLayout(); - } - }); - } - } - - /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. - * @param uid ID of the user whose audio state changes. - * @param reason Reason why the user goes offline: - * USER_OFFLINE_QUIT(0): The user left the current channel. - * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data - * packet was received within a certain period of time. If a user quits the - * call and the message is not passed to the SDK (due to an unreliable channel), - * the SDK assumes the user dropped offline. - * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from - * the host to the audience.*/ - @Override - public void onUserOffline(int uid, int reason) { - Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); - showLongToast(String.format("user %d offline! reason:%d", uid, reason)); - handler.post(new Runnable() { - @Override - public void run() { - /*Clear render view - Note: The video will stay at its last frame, to completely remove it you will need to - remove the SurfaceView from its parent*/ - engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); - remoteViews.get(uid).removeAllViews(); - remoteViews.remove(uid); - updateTranscodeLayout(); - } - }); - } - - @Override - public void onRtmpStreamingStateChanged(String url, int state, int errCode) { - super.onRtmpStreamingStateChanged(url, state, errCode); - showShortToast(String.format("onRtmpStreamingStateChanged state %s errCode %s", state, errCode)); - if (state == Constants.RTMP_STREAM_PUBLISH_STATE_IDLE) { - if (cdnStreaming) { - runOnUIThread(() -> { - LeaveChannelOptions leaveChannelOptions = new LeaveChannelOptions(); - leaveChannelOptions.stopMicrophoneRecording = false; - engine.leaveChannel(leaveChannelOptions); - fl_remote.removeAllViews(); - fl_remote_2.removeAllViews(); - fl_remote_3.removeAllViews(); - remoteViews.clear(); - engine.startPreview(); - engine.setDirectCdnStreamingVideoConfiguration(videoEncoderConfiguration); - int ret = startCdnStreaming(); - if (ret != 0) { - showLongToast(String.format("startCdnStreaming failed! error code: %d", ret)); - stopStreaming(); - } - }); - } - } - } - - @Override - public void onTranscodingUpdated() { - showLongToast("RTMP transcoding updated successfully!"); - } - }; - - private void updateTranscodeLayout() { - boolean hasRemote = remoteViews.size() > 0; - LiveTranscoding.TranscodingUser user = new LiveTranscoding.TranscodingUser(); - user.x = 0; - user.y = 0; - user.width = hasRemote ? canvas_width / 2 : canvas_width; - user.height = hasRemote ? canvas_height / 2 : canvas_height; - user.uid = localUid; - liveTranscoding.addUser(user); - if (hasRemote) { - int index = 0; - for (int uid : remoteViews.keySet()) { - index++; - switch (index) { - case 1: - LiveTranscoding.TranscodingUser user1 = new LiveTranscoding.TranscodingUser(); - user1.x = canvas_width / 2; - user1.y = 0; - user1.width = canvas_width / 2; - user1.height = canvas_height / 2; - user1.uid = uid; - liveTranscoding.addUser(user1); - break; - case 2: - LiveTranscoding.TranscodingUser user2 = new LiveTranscoding.TranscodingUser(); - user2.x = 0; - user2.y = canvas_height / 2; - user2.width = canvas_width / 2; - user2.height = canvas_height / 2; - user2.uid = uid; - liveTranscoding.addUser(user2); - break; - case 3: - LiveTranscoding.TranscodingUser user3 = new LiveTranscoding.TranscodingUser(); - user3.x = canvas_width / 2; - user3.y = canvas_height / 2; - user3.width = canvas_width / 2; - user3.height = canvas_height / 2; - user3.uid = uid; - liveTranscoding.addUser(user3); - break; - default: - Log.i(TAG, "ignored user as only 2x2 video layout supported in this demo. uid:" + uid); - } - } - } - engine.updateRtmpTranscoding(liveTranscoding); - } - - private final IDirectCdnStreamingEventHandler iDirectCdnStreamingEventHandler = new IDirectCdnStreamingEventHandler() { - - - @Override - public void onDirectCdnStreamingStateChanged(DirectCdnStreamingState state, DirectCdnStreamingReason reason, String message) { - showShortToast(String.format("onDirectCdnStreamingStateChanged state:%s, error:%s", state, reason)); - runOnUIThread(new Runnable() { - @Override - public void run() { - switch (state) { - case RUNNING: - streamingButton.setText(R.string.stop_streaming); - cdnStreaming = true; - break; - case STOPPED: - if (rtcStreaming) { - // Switch to RTC streaming when direct CDN streaming completely stopped. - ChannelMediaOptions channelMediaOptions = new ChannelMediaOptions(); - channelMediaOptions.publishMicrophoneTrack = true; - channelMediaOptions.publishCameraTrack = true; - channelMediaOptions.clientRoleType = CLIENT_ROLE_BROADCASTER; - int ret = engine.joinChannel(null, channel, localUid, channelMediaOptions); - if (ret != 0) { - showLongToast(String.format("Join Channel call failed! reason:%d", ret)); - } - } else { - streamingButton.setText(getString(R.string.start_live_streaming)); - cdnStreaming = false; - } - break; - case FAILED: - showLongToast(String.format("Start Streaming failed, please go back to previous page and check the settings.")); - default: - Log.i(TAG, String.format("onDirectCdnStreamingStateChanged, state: %s error: %s message: %s", state.name(), reason.name(), message)); - } - rtcSwitcher.setEnabled(true); - } - }); - } - - @Override - public void onDirectCdnStreamingStats(DirectCdnStreamingStats directCdnStreamingStats) { - - } - }; - - private final SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { - - @Override - public void onProgressChanged(SeekBar seekBar, int i, boolean b) { - engine.adjustRecordingSignalVolume(i); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - - } - }; - - private final CompoundButton.OnCheckedChangeListener checkedChangeListener = new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean b) { - rtcStreaming = b; - if (rtcStreaming) { - engine.stopDirectCdnStreaming(); - } else if (cdnStreaming) { - engine.stopRtmpStream(getUrl()); - } - handler.post(new Runnable() { - @Override - public void run() { - toggleVideoLayout(rtcStreaming); - } - }); - } - }; - - private void toggleVideoLayout(boolean isMultiple) { - if (isMultiple) { - fl_remote.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0.5f)); - fl_remote_2.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0.5f)); - fl_remote_3.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0.5f)); - video_row2.setLayoutParams(new LinearLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 0, 1)); - } else { - fl_remote.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0)); - fl_remote_2.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0)); - fl_remote_3.setLayoutParams(new LinearLayout.LayoutParams(0, FrameLayout.LayoutParams.MATCH_PARENT, 0)); - video_row2.setLayoutParams(new LinearLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 0, 0)); - } - } - - private ViewGroup getAvailableView() { - if (fl_remote.getChildCount() == 0) { - return fl_remote; - } else if (fl_remote_2.getChildCount() == 0) { - return fl_remote_2; - } else if (fl_remote_3.getChildCount() == 0) { - return fl_remote_3; - } else { - return fl_remote; - } - } -} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java index 61480657b..b61249795 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java @@ -47,7 +47,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 14, + index = 26, group = ADVANCED, name = R.string.item_channelencryption, actionId = R.id.action_mainFragment_to_channel_encryption, @@ -101,7 +101,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ContentInspect.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ContentInspect.java index ea09bc805..4839435fb 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ContentInspect.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ContentInspect.java @@ -40,7 +40,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 23, + index = 38, group = ADVANCED, name = R.string.item_content_inspect, actionId = R.id.action_mainFragment_to_content_inspect, @@ -96,7 +96,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java index 753964de3..790f995d8 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java @@ -56,7 +56,7 @@ * This example demonstrates how to customize the renderer to render the local scene of the remote video stream. */ @Example( - index = 8, + index = 17, group = ADVANCED, name = R.string.item_customremoterender, actionId = R.id.action_mainFragment_to_CustomRemoteRender, @@ -132,7 +132,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/FaceCapture.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/FaceCapture.java index b70faab1b..7b002db61 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/FaceCapture.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/FaceCapture.java @@ -45,7 +45,7 @@ * The type Process raw data. */ @Example( - index = 12, + index = 23, group = ADVANCED, name = R.string.item_face_capture, actionId = R.id.action_mainFragment_to_face_capture, @@ -80,7 +80,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java index a78260ef7..770e0cdf5 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java @@ -41,7 +41,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 21, + index = 36, group = ADVANCED, name = R.string.item_hostacrosschannel, actionId = R.id.action_mainFragment_to_hostacrosschannel, @@ -101,7 +101,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java index 36ae325be..2eb40d915 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java @@ -118,7 +118,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java index 0af1dbf5f..17e40f8ff 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java @@ -49,7 +49,7 @@ * The type Join multiple channel. */ @Example( - index = 13, + index = 25, group = ADVANCED, name = R.string.item_joinmultichannel, actionId = R.id.action_mainFragment_to_MultiChannel, @@ -105,7 +105,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java index fe2c14df3..cd49cf805 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java @@ -12,7 +12,7 @@ * The type Ktv copyright music. */ @Example( - index = 24, + index = 40, group = ADVANCED, name = R.string.item_ktv_copyright_music, actionId = R.id.action_mainFragment_to_ktv_copyright_music, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java index 2dde01164..b82fa47d8 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java @@ -68,7 +68,7 @@ * When turn the Co-host on, others will see you. */ @Example( - index = 0, + index = 10, group = ADVANCED, name = R.string.item_livestreaming, actionId = R.id.action_mainFragment_to_live_streaming, @@ -305,7 +305,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events.*/ RtcEngineConfig rtcEngineConfig = new RtcEngineConfig(); - rtcEngineConfig.mAppId = getString(R.string.agora_app_id); + rtcEngineConfig.mAppId = getAgoraAppId(); rtcEngineConfig.mContext = context.getApplicationContext(); rtcEngineConfig.mEventHandler = iRtcEngineEventHandler; /* Sets the channel profile of the Agora RtcEngine. */ diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java index 8e64a1256..36e88262d 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java @@ -49,7 +49,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 19, + index = 33, group = ADVANCED, name = R.string.item_localvideotranscoding, actionId = R.id.action_mainFragment_to_LocalVideoTranscoding, @@ -101,7 +101,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaMetadata.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaMetadata.java index 9ecd38b78..8747dc500 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaMetadata.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaMetadata.java @@ -47,7 +47,7 @@ * The type Video metadata. */ @Example( - index = 3, + index = 12, group = ADVANCED, name = R.string.item_mediametadata, actionId = R.id.action_mainFragment_to_MediaMetadata, @@ -115,7 +115,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayer.java index dc7ebb47e..4278d7540 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayer.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayer.java @@ -65,7 +65,7 @@ * The type Media player. */ @Example( - index = 17, + index = 29, group = ADVANCED, name = R.string.item_mediaplayer, actionId = R.id.action_mainFragment_to_MediaPlayer, @@ -118,7 +118,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaRecorder.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaRecorder.java index 98fdb4136..f93c4b8a9 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaRecorder.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaRecorder.java @@ -54,7 +54,7 @@ * The type Media recorder. */ @Example( - index = 17, + index = 30, group = ADVANCED, name = R.string.item_media_recorder, actionId = R.id.action_mainFragment_to_MediaRecorder, @@ -112,7 +112,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiVideoSourceTracks.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiVideoSourceTracks.java index 72d0c1d9b..5dcf0d621 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiVideoSourceTracks.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiVideoSourceTracks.java @@ -66,7 +66,7 @@ * The type Multi video source tracks. */ @Example( - index = 10, + index = 19, group = ADVANCED, name = R.string.item_multiVideoSourceTracks, actionId = R.id.action_mainFragment_to_MultiVideoSourceTracks, @@ -135,7 +135,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Multipath.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Multipath.java index 6f460ee81..5b3139186 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Multipath.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Multipath.java @@ -47,7 +47,7 @@ * This example demonstrates how to use Multipath */ @Example( - index = 29, + index = 45, group = ADVANCED, name = R.string.item_multipath, actionId = R.id.action_mainFragment_to_multipath, @@ -117,7 +117,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PictureInPicture.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PictureInPicture.java index 3997576b3..7c73b7303 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PictureInPicture.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PictureInPicture.java @@ -50,7 +50,7 @@ /** * This demo demonstrates how to make a one-to-one video call */ -@Example(index = 11, group = ADVANCED, name = R.string.item_picture_in_picture, actionId = R.id.action_mainFragment_to_picture_in_picture, tipsId = R.string.picture_in_picture) +@Example(index = 22, group = ADVANCED, name = R.string.item_picture_in_picture, actionId = R.id.action_mainFragment_to_picture_in_picture, tipsId = R.string.picture_in_picture) public class PictureInPicture extends BaseFragment implements View.OnClickListener { private static final String TAG = PictureInPicture.class.getSimpleName(); @@ -111,7 +111,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java index 964dd692a..0363996b6 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java @@ -43,7 +43,7 @@ * The type Play audio files. */ @Example( - index = 15, + index = 27, group = ADVANCED, name = R.string.item_playaudiofiles, actionId = R.id.action_mainFragment_to_PlayAudioFiles, @@ -163,7 +163,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java index 23d3754ed..ee2abd493 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java @@ -36,7 +36,7 @@ * The type Pre call test. */ @Example( - index = 16, + index = 28, group = ADVANCED, name = R.string.item_precalltest, actionId = R.id.action_mainFragment_to_PreCallTest, @@ -82,7 +82,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java index c71801453..5b6450bc5 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java @@ -44,7 +44,7 @@ * @author cjw */ @Example( - index = 9, + index = 18, group = ADVANCED, name = R.string.item_raw_audio, actionId = R.id.action_mainFragment_raw_audio, @@ -149,7 +149,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java index c973a0168..e051003d1 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java @@ -55,7 +55,7 @@ * The type Process raw data. */ @Example( - index = 11, + index = 20, group = ADVANCED, name = R.string.item_processraw, actionId = R.id.action_mainFragment_to_ProcessRawData, @@ -90,7 +90,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java index dcc49c6fc..e31816c58 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java @@ -144,7 +144,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideoYUV.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideoYUV.java index 1bcb3a580..17b3d7127 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideoYUV.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideoYUV.java @@ -59,7 +59,7 @@ * The type Push external video yuv. */ @Example( - index = 7, + index = 16, group = ADVANCED, name = R.string.item_pushexternal, actionId = R.id.action_mainFragment_to_PushExternalVideo, @@ -136,7 +136,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java index 46a20a4b7..eea6a0007 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java @@ -49,7 +49,7 @@ * otherwise unexpected errors will occur. */ @Example( - index = 1, + index = 11, group = ADVANCED, name = R.string.item_rtctortmp, actionId = R.id.action_mainFragment_to_RTCToRTMP, @@ -115,7 +115,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java index 3a31b7cb3..28386196f 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RhythmPlayer.java @@ -37,7 +37,7 @@ * This demo demonstrates how to make a VideoProcessExtension */ @Example( - index = 19, + index = 34, group = ADVANCED, name = R.string.item_rhythmplayer, actionId = R.id.action_mainFragment_rhythm_player, @@ -102,7 +102,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ScreenSharing.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ScreenSharing.java index f750807d5..001fb78eb 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ScreenSharing.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ScreenSharing.java @@ -60,7 +60,7 @@ * screen share stream during an audio-video call. */ @Example( - index = 18, + index = 31, group = ADVANCED, name = R.string.item_screensharing, actionId = R.id.action_mainFragment_to_ScreenSharing, @@ -156,7 +156,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java index 70e44a44c..a5ea8a318 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java @@ -41,7 +41,7 @@ * The type Send data stream. */ @Example( - index = 20, + index = 35, group = ADVANCED, name = R.string.item_senddatastream, actionId = R.id.action_mainFragment_senddatastream, @@ -102,7 +102,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SimpleExtension.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SimpleExtension.java index e026253da..b3430bc2e 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SimpleExtension.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SimpleExtension.java @@ -46,7 +46,7 @@ * @author cjw */ @Example( - index = 11, + index = 21, group = ADVANCED, name = R.string.item_ext, actionId = R.id.action_mainFragment_extension, @@ -171,7 +171,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Simulcast.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Simulcast.java index 4fb3144ef..3e7442449 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Simulcast.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/Simulcast.java @@ -50,7 +50,7 @@ * This example demonstrates how to use Simulcast */ @Example( - index = 28, + index = 44, group = ADVANCED, name = R.string.item_simulcast, actionId = R.id.action_mainFragment_to_simulcast, @@ -122,7 +122,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java index 36e5fd7c3..6e796ce0f 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SpatialSound.java @@ -57,7 +57,7 @@ * The type Spatial sound. */ @Example( - index = 22, + index = 37, group = ADVANCED, name = R.string.item_spatial_sound, actionId = R.id.action_mainFragment_to_spatial_sound, @@ -104,7 +104,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * How to get the App ID * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events.*/ - String appId = getString(R.string.agora_app_id); + String appId = getAgoraAppId(); RtcEngineConfig config = new RtcEngineConfig(); config.mContext = getContext().getApplicationContext(); config.mAppId = appId; diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java index 7f3a6e08a..213be52f7 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java @@ -110,7 +110,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java index ccc9669ad..e75fedc51 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java @@ -29,7 +29,7 @@ * The type Third party beauty. */ @Example( - index = 24, + index = 39, group = ADVANCED, name = R.string.item_third_party_beauty, actionId = R.id.action_mainFragment_to_third_party_beauty, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java index 556dd440f..f53e803e1 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java @@ -45,7 +45,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 25, + index = 41, group = ADVANCED, name = R.string.item_transparentrendering, actionId = R.id.action_mainFragment_to_transparentrendering, @@ -98,7 +98,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/UrlLiveStream.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/UrlLiveStream.java index a9a006e7c..25bb10b35 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/UrlLiveStream.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/UrlLiveStream.java @@ -32,7 +32,7 @@ * This demo demonstrates how to make a live stream with url */ @Example( - index = 26, + index = 42, group = ADVANCED, name = R.string.ultra_live_streaming_with_url, actionId = R.id.action_mainFragment_to_url_live_stream, @@ -67,11 +67,11 @@ protected void initView() { @Override protected void initData() { - binding.etRteUrl.setText("rte://" + getString(R.string.agora_app_id)); + binding.etRteUrl.setText("rte://" + getAgoraAppId()); try { mRte = new Rte(null); Config config = new Config(); - config.setAppId(getContext().getString(R.string.agora_app_id)); + config.setAppId(getAgoraAppId()); mRte.setConfigs(config); mRte.initMediaEngine((Error error) -> { io.agora.rte.Constants.ErrorCode errCode = error.code(); diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoProcessExtension.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoProcessExtension.java index f1a38a2b7..a890e3ecf 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoProcessExtension.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoProcessExtension.java @@ -57,7 +57,7 @@ * This demo demonstrates how to make a VideoProcessExtension */ @Example( - index = 19, + index = 32, group = ADVANCED, name = R.string.item_videoProcessExtension, actionId = R.id.action_mainFragment_video_enhancement, @@ -194,7 +194,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java index 44830dd19..6a7cb8abc 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java @@ -56,7 +56,7 @@ * @author cjw */ @Example( - index = 12, + index = 24, group = ADVANCED, name = R.string.item_quickswitck, actionId = R.id.action_mainFragment_to_QuickSwitch, @@ -103,7 +103,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java index 270ec862c..9a9439ec1 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java @@ -91,7 +91,7 @@ * The type Voice effects. */ @Example( - index = 4, + index = 13, group = ADVANCED, name = R.string.item_voiceeffects, actionId = R.id.action_mainFragment_to_VoiceEffects, @@ -249,7 +249,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/FaceUnityBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/FaceUnityBeauty.java index 64ffd435a..05d82f6fa 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/FaceUnityBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/FaceUnityBeauty.java @@ -201,7 +201,7 @@ public void onRemoteVideoStats(RemoteVideoStats stats) { } } }; - rtcEngine = RtcEngine.create(getContext(), getString(R.string.agora_app_id), mRtcEngineEventHandler); + rtcEngine = RtcEngine.create(getContext(), getAgoraAppId(), mRtcEngineEventHandler); if (rtcEngine == null) { return; diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeauty.java index 03b69da34..dd3763b09 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeauty.java @@ -228,7 +228,7 @@ public void onRemoteVideoStats(RemoteVideoStats stats) { } } }; - rtcEngine = RtcEngine.create(getContext(), getString(R.string.agora_app_id), mRtcEngineEventHandler); + rtcEngine = RtcEngine.create(getContext(), getAgoraAppId(), mRtcEngineEventHandler); if (rtcEngine == null) { return; diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java index 1124f07a6..f7cb510f8 100755 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioRender.java @@ -38,7 +38,7 @@ /** * This demo demonstrates how to make a one-to-one voice call */ -@Example(index = 6, group = ADVANCED, name = R.string.item_customaudiorender, actionId = R.id.action_mainFragment_to_CustomAudioRender, tipsId = R.string.customaudiorender) +@Example(index = 15, group = ADVANCED, name = R.string.item_customaudiorender, actionId = R.id.action_mainFragment_to_CustomAudioRender, tipsId = R.string.customaudiorender) public class CustomAudioRender extends BaseFragment implements View.OnClickListener { private static final String TAG = CustomAudioRender.class.getSimpleName(); private EditText et_channel; @@ -106,7 +106,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java index ae9ab30e1..d1f79394b 100755 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java @@ -38,7 +38,7 @@ /** * This demo demonstrates how to make a one-to-one voice call */ -@Example(index = 5, group = ADVANCED, name = R.string.item_customaudiosource, actionId = R.id.action_mainFragment_to_CustomAudioSource, tipsId = R.string.customaudio) +@Example(index = 14, group = ADVANCED, name = R.string.item_customaudiosource, actionId = R.id.action_mainFragment_to_CustomAudioSource, tipsId = R.string.customaudio) public class CustomAudioSource extends BaseFragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { private static final String TAG = CustomAudioSource.class.getSimpleName(); private EditText et_channel; @@ -121,7 +121,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java index 66a693653..45b7859c8 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java @@ -21,7 +21,7 @@ * The type Audio router player. */ @Example( - index = 17, + index = 6, group = ADVANCED, name = R.string.item_audiorouter_player, actionId = R.id.action_mainFragment_to_AudioRouterPlayer, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerExo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerExo.java index 367b01c4e..ae75d7edc 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerExo.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerExo.java @@ -68,7 +68,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerIjk.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerIjk.java index 808ec3cf9..339a95ce4 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerIjk.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerIjk.java @@ -66,7 +66,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerNative.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerNative.java index fa3e86c44..c8876a924 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerNative.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerNative.java @@ -67,7 +67,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java index 109b634d0..e083f8479 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java @@ -32,7 +32,7 @@ * The type Audio waveform. */ @Example( - index = 17, + index = 5, group = ADVANCED, name = R.string.item_audiowaveform, actionId = R.id.action_mainFragment_to_AudioWaveform, @@ -62,7 +62,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java index 88cb69e8b..f2591d056 100755 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java @@ -299,7 +299,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java index 89c2d1149..7d341f5f4 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java @@ -100,7 +100,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { /* * The App ID issued to you by Agora. See How to get the App ID */ - config.mAppId = getString(R.string.agora_app_id); + config.mAppId = getAgoraAppId(); /* Sets the channel profile of the Agora RtcEngine. CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. Use this profile in one-on-one calls or group calls, where all users can talk freely. diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java new file mode 100644 index 000000000..4f1d3e4e5 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/AgoraConfig.java @@ -0,0 +1,22 @@ +package io.agora.api.example.utils; + +import android.text.TextUtils; + +import io.agora.api.example.BuildConfig; + +public final class AgoraConfig { + private AgoraConfig() { + } + + public static String getAppId() { + return BuildConfig.AGORA_APP_ID; + } + + public static String getAppCertificate() { + return BuildConfig.AGORA_APP_CERT; + } + + public static boolean hasAppCertificate() { + return !TextUtils.isEmpty(getAppCertificate()); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TokenUtils.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TokenUtils.java index e72d8185e..52ea546d8 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TokenUtils.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TokenUtils.java @@ -14,7 +14,6 @@ import java.io.IOException; import java.util.Objects; -import io.agora.api.example.R; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; @@ -44,11 +43,11 @@ private TokenUtils() { } public static void genToken(Context context, String channelName, int uid, OnTokenGenCallback onGetToken) { - String cert = context.getString(R.string.agora_app_certificate); + String cert = AgoraConfig.getAppCertificate(); if (cert.isEmpty()) { onGetToken.onTokenGen(""); } else { - gen(context.getString(R.string.agora_app_id), context.getString(R.string.agora_app_certificate), channelName, uid, ret -> { + gen(AgoraConfig.getAppId(), cert, channelName, uid, ret -> { if (onGetToken != null) { runOnUiThread(() -> { onGetToken.onTokenGen(ret); @@ -74,7 +73,7 @@ public static void genToken(Context context, String channelName, int uid, OnToke * @param onGetToken the on get token */ public static void gen(Context context, String channelName, int uid, OnTokenGenCallback onGetToken) { - gen(context.getString(R.string.agora_app_id), context.getString(R.string.agora_app_certificate), channelName, uid, ret -> { + gen(AgoraConfig.getAppId(), AgoraConfig.getAppCertificate(), channelName, uid, ret -> { if (onGetToken != null) { runOnUiThread(() -> { onGetToken.onTokenGen(ret); diff --git a/Android/APIExample/app/src/main/res/layout/fragment_cdn_audience.xml b/Android/APIExample/app/src/main/res/layout/fragment_cdn_audience.xml deleted file mode 100644 index d4736e9ff..000000000 --- a/Android/APIExample/app/src/main/res/layout/fragment_cdn_audience.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Android/APIExample/app/src/main/res/layout/fragment_cdn_entry.xml b/Android/APIExample/app/src/main/res/layout/fragment_cdn_entry.xml deleted file mode 100644 index 9f5115198..000000000 --- a/Android/APIExample/app/src/main/res/layout/fragment_cdn_entry.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/Android/APIExample/app/src/main/res/layout/fragment_cdn_host.xml b/Android/APIExample/app/src/main/res/layout/fragment_cdn_host.xml deleted file mode 100644 index 589d26031..000000000 --- a/Android/APIExample/app/src/main/res/layout/fragment_cdn_host.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -