diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..af39f90f7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,56 @@ +# AGENTS.md + +Entry point for AI agents working on the Agora RTC Native SDK API-Examples repository. +Read this file first, then navigate to the relevant platform directory. + +## Repository Overview + +This repository contains sample projects demonstrating Agora RTC Native SDK APIs across four independent platforms. Each platform is self-contained — do not share source files, build scripts, or dependencies across platforms. + +| Platform | Language(s) | Directory | SDK | +|----------|-------------|-----------|-----| +| Android | Java / Kotlin | `Android/` | RTC Java SDK (full / voice) | +| iOS | Swift / Objective-C | `iOS/` | RTC Objective-C SDK (full / audio) | +| macOS | Swift | `macOS/` | RTC Objective-C SDK (full) | +| Windows | C++ | `windows/` | RTC C++ SDK (full) | + +## Navigation + +Each platform directory contains its own `AGENTS.md` with platform-specific rules, project selection guidance, and architecture constraints. Always read the platform-level `AGENTS.md` before making any changes. + +| Platform | Entry Point | +|----------|-------------| +| Android | `Android/AGENTS.md` | +| iOS | `iOS/AGENTS.md` | +| macOS | `macOS/AGENTS.md` | +| Windows | `windows/AGENTS.md` | + +## Cross-Platform Rules + +1. Never share source files, build scripts, or SDK dependencies between platforms. +2. Each platform manages its own SDK version — check the platform-level config file before assuming a version. +3. All examples follow the same structural pattern within their platform: one self-contained class per API feature, managing its own engine lifecycle. +4. Always call the SDK's leave-channel and destroy APIs when an example screen is closed. +5. SDK event/delegate callbacks may arrive on a background thread — always dispatch UI updates to the main thread. + +## Repository-Level Files + +| File | Purpose | +|------|---------| +| `HOOKS-GUIDE.md` | Git hook installation (sensitive-info detection, commit-message rules) | +| `.pre-commit-config.yaml` | Pre-commit hook configuration | +| `.gitleaks.toml` | Gitleaks allowlist configuration | +| `azure-pipelines.yml` | CI/CD pipeline definition | + +## Git Hooks + +This repository enforces two rules via Git hooks: +- No sensitive information (API keys, tokens) in committed code. +- Commit messages must be in English only (no Chinese characters). + +Run `.git-hooks/install-hooks.sh` once after cloning to activate the hooks. +See `HOOKS-GUIDE.md` for details and troubleshooting. + +## Sensitive Configuration + +API keys and App IDs are never committed. Each platform stores them in a `KeyCenter` file (Swift/OC) or `KeyCenter.java` / `KeyCenter.kt` (Android) or `CConfig` (Windows). These files are git-ignored and must be populated locally before building. 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/AGENTS.md b/Android/APIExample-Audio/AGENTS.md new file mode 100644 index 000000000..092f45f59 --- /dev/null +++ b/Android/APIExample-Audio/AGENTS.md @@ -0,0 +1,38 @@ +# 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..02f1bfdfd 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; /** @@ -153,6 +154,14 @@ protected final void showShortToast(final String msg) { }); } + protected final String getAgoraAppId() { + return AgoraConfig.getAppId(); + } + + protected final String getAgoraAppCertificate() { + return AgoraConfig.getAppCertificate(); + } + /** * Run on ui thread. * 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..e4a5bcb2e 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 @@ -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. @@ -330,7 +330,7 @@ private void joinChannel(String channelId) { Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) ); - /**Please configure accessToken in the string_config file. + /** * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..cefd9f124 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 @@ -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..c953bffd1 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 @@ -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. @@ -268,7 +268,7 @@ private void joinChannel(String channelId) { engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); engine.setDefaultAudioRoutetoSpeakerphone(true); - /**Please configure accessToken in the string_config file. + /** * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..70beefdaf 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 @@ -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. @@ -227,7 +227,7 @@ private void joinChannel(String channelId) { engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); engine.enableAudioVolumeIndication(1000, 3, true); - /**Please configure accessToken in the string_config file. + /** * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..616988e25 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 @@ -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; @@ -219,7 +219,7 @@ private void joinChannel() { engine.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..888b457db 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 @@ -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. @@ -397,7 +397,7 @@ private void joinChannel(String channelId) { Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) ); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..d92c0633e 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 @@ -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. @@ -233,7 +233,7 @@ private void joinChannel(String channelId) { engine.setExternalAudioSink(true, SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..c10b8e986 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 @@ -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. @@ -278,7 +278,7 @@ private void joinChannel(String channelId) { config.enableLocalPlayback = false; customAudioTrack = engine.createCustomAudioTrack(Constants.AudioTrackType.AUDIO_TRACK_MIXABLE, config); - /**Please configure accessToken in the string_config file. + /** * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..472d340b5 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 @@ -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. @@ -168,7 +168,7 @@ private void joinChannel(String channelId) { option.autoSubscribeVideo = true; option.publishMicrophoneTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..477311248 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 @@ -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. @@ -463,7 +463,7 @@ private void joinChannel(String channelId) { option.autoSubscribeAudio = true; option.autoSubscribeVideo = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..645cf0b1a 100644 --- a/Android/APIExample-Audio/ci.env.py +++ b/Android/APIExample-Audio/ci.env.py @@ -1,22 +1,30 @@ -#!/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..a6dd49ba7 100755 --- a/Android/APIExample-Audio/cloud_build.sh +++ b/Android/APIExample-Audio/cloud_build.sh @@ -37,11 +37,23 @@ fi #sed -ie "s#google()#maven { url \"https\://maven.aliyun.com/repository/public\" }\n google()#g" settings.gradle #sed -ie "s#https://services.gradle.org/distributions#https://mirrors.cloud.tencent.com/gradle#g" gradle/wrapper/gradle-wrapper.properties +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 +} + ## 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 "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 @@ -68,4 +80,4 @@ if [ "$WORKSPACE" != "" ]; then APK_NAME="${PROJECT_NAME}_${BUILD_NUMBER}_${SDK_VERSION}_$(date "+%Y%m%d%H%M%S").apk" echo "Copying APK to: $WORKSPACE/$APK_NAME" cp app/build/outputs/apk/release/*.apk "$WORKSPACE/$APK_NAME" -fi \ No newline at end of file +fi 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/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/.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/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/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..029ef5e60 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; /** @@ -153,6 +154,14 @@ protected final void showShortToast(final String msg) { }); } + protected final String getAgoraAppId() { + return AgoraConfig.getAppId(); + } + + protected final String getAgoraAppCertificate() { + return AgoraConfig.getAppCertificate(); + } + /** * Run on ui thread. * 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..9fed308de 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 @@ -11,7 +11,6 @@ import androidx.viewbinding.ViewBinding; import io.agora.api.example.MainApplication; -import io.agora.api.example.R; import io.agora.rtc2.Constants; import io.agora.rtc2.IRtcEngineEventHandler; import io.agora.rtc2.RtcEngine; @@ -62,7 +61,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..df2305953 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 @@ -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. @@ -394,7 +394,7 @@ private void joinChannel(String channelId) { VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) )); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..85603c837 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 @@ -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. @@ -287,7 +287,7 @@ private void joinChannel(String channelId) { ORIENTATION_MODE_ADAPTIVE )); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..10c0894e6 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 @@ -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. @@ -231,7 +231,7 @@ private void joinChannel(String channelId) { engine.enableContentInspect(true, contentInspectConfig); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..94444a61d 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 @@ -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. @@ -273,7 +273,7 @@ private void joinChannel(String channelId) { engine.startPreview(); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..fbcfdbb6f 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 @@ -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. @@ -239,7 +239,7 @@ private void joinChannel(String channelId) { engine.startPreview(); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..a54072f85 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 @@ -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. @@ -261,7 +261,7 @@ private void joinChannel(String channelId) { VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) )); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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 deleted file mode 100644 index 36ae325be..000000000 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java +++ /dev/null @@ -1,485 +0,0 @@ -package io.agora.api.example.examples.advanced; - -import static io.agora.rtc2.video.VideoCanvas.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.EditText; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatTextView; - -import io.agora.api.example.MainApplication; -import io.agora.api.example.R; -import io.agora.api.example.common.BaseFragment; -import io.agora.api.example.common.model.StatisticsInfo; -import io.agora.api.example.utils.CommonUtil; -import io.agora.api.example.utils.PermissonUtils; -import io.agora.api.example.utils.TokenUtils; -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; - -//@Example( -// index = 17, -// group = ADVANCED, -// name = R.string.item_incallreport, -// actionId = R.id.action_mainFragment_to_InCallReport, -// tipsId = R.string.incallstats -//) - -/** - * The type In call report. - * - * @deprecated The report has been moved to - * {@link io.agora.api.example.common.widget.VideoReportLayout}. - * You can refer to {@link LiveStreaming} or {@link io.agora.api.example.examples.basic.JoinChannelVideo} example. - */ -@Deprecated -public class InCallReport extends BaseFragment implements View.OnClickListener { - private static final String TAG = InCallReport.class.getSimpleName(); - - private FrameLayout fl_local, fl_remote; - private Button join; - private EditText et_channel; - private AppCompatTextView localStats, remoteStats; - private RtcEngine engine; - private StatisticsInfo statisticsInfo; - private int myUid; - private boolean joined = false; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_in_call_report, container, false); - return view; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - join = view.findViewById(R.id.btn_join); - statisticsInfo = new StatisticsInfo(); - et_channel = view.findViewById(R.id.et_channel); - localStats = view.findViewById(R.id.local_stats); - localStats.bringToFront(); - remoteStats = view.findViewById(R.id.remote_stats); - remoteStats.bringToFront(); - view.findViewById(R.id.btn_join).setOnClickListener(this); - fl_local = view.findViewById(R.id.fl_local); - fl_remote = view.findViewById(R.id.fl_remote); - } - - private void updateLocalStats() { - handler.post(new Runnable() { - @Override - public void run() { - localStats.setText(statisticsInfo.getLocalVideoStats()); - } - }); - } - - private void updateRemoteStats() { - handler.post(new Runnable() { - @Override - public void run() { - remoteStats.setText(statisticsInfo.getRemoteVideoStats()); - } - }); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - try { - RtcEngineConfig config = new RtcEngineConfig(); - /* - * The context of Android Activity - */ - config.mContext = context.getApplicationContext(); - /* - * The App ID issued to you by Agora. See How to get the App ID - */ - config.mAppId = getString(R.string.agora_app_id); - /* Sets the channel profile of the Agora RtcEngine. - CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. - Use this profile in one-on-one calls or group calls, where all users can talk freely. - CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast - channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; - an audience can only receive streams.*/ - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - /* - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); - config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - /* - * This parameter is for reporting the usages of APIExample to agora background. - * Generally, it is not necessary for you to set this parameter. - */ - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/ - LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - // This api can only be used in the private media server scenario, otherwise some problems may occur. - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - } - catch (Exception e) { - e.printStackTrace(); - getActivity().onBackPressed(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - /*leaveChannel and Destroy the RtcEngine instance*/ - if (engine != null) { - engine.leaveChannel(); - engine.stopPreview(); - } - handler.post(RtcEngine::destroy); - engine = null; - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.btn_join) { - if (!joined) { - CommonUtil.hideInputBoard(getActivity(), et_channel); - // call when join button hit - String channelId = et_channel.getText().toString(); - // Check permission - checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { - @Override - public void onPermissionsResult(boolean allPermissionsGranted, String[] permissions, int[] grantResults) { - // Permissions Granted - if (allPermissionsGranted) { - joinChannel(channelId); - } - } - }); - } else { - joined = false; - /*After joining a channel, the user must call the leaveChannel method to end the - * call before joining another channel. This method returns 0 if the user leaves the - * channel and releases all resources related to the call. This method call is - * asynchronous, and the user has not exited the channel when the method call returns. - * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. - * A successful leaveChannel method call triggers the following callbacks: - * 1:The local client: onLeaveChannel. - * 2:The remote client: onUserOffline, if the user leaving the channel is in the - * Communication channel, or is a BROADCASTER in the Live Broadcast profile. - * @returns 0: Success. - * < 0: Failure. - * PS: - * 1:If you call the destroy method immediately after calling the leaveChannel - * method, the leaveChannel process interrupts, and the SDK does not trigger - * the onLeaveChannel callback. - * 2:If you call the leaveChannel method during CDN live streaming, the SDK - * triggers the removeInjectStreamUrl method.*/ - engine.leaveChannel(); - join.setText(getString(R.string.join)); - } - } - } - - private void joinChannel(String channelId) { - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - - // Create render view by RtcEngine - 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)); - // 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(); - // start preview - engine.startPreview(); - // 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()) - )); - - /*Please configure accessToken in the string_config file. - * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see - * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token - * A token generated at the server. This applies to scenarios with high-security requirements. For details, see - * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ - TokenUtils.gen(requireContext(), channelId, 0, ret -> { - /* Allows a user to join a channel. - if you do not specify the uid, we will generate the uid for you*/ - int res = engine.joinChannel(ret, channelId, "Extra Optional Data", 0); - if (res != 0) { - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - showAlert(RtcEngine.getErrorDescription(Math.abs(res))); - return; - } - // Prevent repeated entry - join.setEnabled(false); - }); - } - - /** - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { - /** - * Error code description can be found at: - * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - */ - @Override - public void onError(int err) { - Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); - } - - - /**Occurs when a user leaves the channel. - * @param stats With this callback, the application retrieves the channel information, - * such as the call duration and statistics.*/ - @Override - public void onLeaveChannel(RtcStats stats) { - super.onLeaveChannel(stats); - Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); - showLongToast(String.format("local user %d leaveChannel!", myUid)); - } - - /**Occurs when the local user joins a specified channel. - * The channel name assignment is based on channelName specified in the joinChannel method. - * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. - * @param channel Channel name - * @param uid User ID - * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - myUid = uid; - joined = true; - handler.post(new Runnable() { - @Override - public void run() { - join.setEnabled(true); - join.setText(getString(R.string.leave)); - } - }); - } - - /**Since v2.9.0. - * This callback indicates the state change of the remote audio stream. - * PS: This callback does not work properly when the number of users (in the Communication profile) or - * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. - * @param uid ID of the user whose audio state changes. - * @param state State of the remote audio - * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due - * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), - * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). - * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. - * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, - * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), - * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). - * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to - * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). - * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to - * REMOTE_AUDIO_REASON_INTERNAL(0). - * @param reason The reason of the remote audio state change. - * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. - * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. - * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. - * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio - * stream or disables the audio module. - * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio - * stream or enables the audio module. - * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or - * disables the audio module. - * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream - * or enables the audio module. - * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. - * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method - * until the SDK triggers this callback.*/ - @Override - public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { - super.onRemoteAudioStateChanged(uid, state, reason, elapsed); - Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); - } - - /**Since v2.9.0. - * Occurs when the remote video state changes. - * PS: This callback does not work properly when the number of users (in the Communication - * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. - * @param uid ID of the remote user whose video state changes. - * @param state State of the remote video: - * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due - * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), - * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). - * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. - * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, - * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), - * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), - * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). - * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to - * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). - * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to - * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). - * @param reason The reason of the remote video state change: - * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. - * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. - * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. - * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote - * video stream or disables the video module. - * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote - * video stream or enables the video module. - * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video - * stream or disables the video module. - * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video - * stream or enables the video module. - * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. - * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the - * audio-only stream due to poor network conditions. - * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches - * back to the video stream after the network conditions improve. - * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until - * the SDK triggers this callback.*/ - @Override - public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { - super.onRemoteVideoStateChanged(uid, state, reason, elapsed); - Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); - } - - /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. - * @param uid ID of the user whose audio state changes. - * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole - * until this callback is triggered.*/ - @Override - public void onUserJoined(int uid, int elapsed) { - super.onUserJoined(uid, elapsed); - Log.i(TAG, "onUserJoined->" + uid); - showLongToast(String.format("user %d joined!", uid)); - /*Check if the context is correct*/ - Context context = getContext(); - if (context == null) { - return; - } - handler.post(() -> { - /*Display remote video stream*/ - SurfaceView surfaceView = null; - if (fl_remote.getChildCount() > 0) { - fl_remote.removeAllViews(); - } - // Create render view by RtcEngine - surfaceView = new SurfaceView(context); - surfaceView.setZOrderMediaOverlay(true); - // Add to the remote container - fl_remote.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)); - } - }); - } - - @Override - public void onRemoteAudioStats(RemoteAudioStats remoteAudioStats) { - statisticsInfo.setRemoteAudioStats(remoteAudioStats); - updateRemoteStats(); - } - - @Override - public void onLocalAudioStats(LocalAudioStats localAudioStats) { - statisticsInfo.setLocalAudioStats(localAudioStats); - updateLocalStats(); - } - - @Override - public void onRemoteVideoStats(RemoteVideoStats remoteVideoStats) { - statisticsInfo.setRemoteVideoStats(remoteVideoStats); - updateRemoteStats(); - } - - @Override - public void onLocalVideoStats(Constants.VideoSourceType source, LocalVideoStats stats) { - super.onLocalVideoStats(source, stats); - statisticsInfo.setLocalVideoStats(stats); - updateLocalStats(); - } - - @Override - public void onRtcStats(RtcStats rtcStats) { - statisticsInfo.setRtcStats(rtcStats); - } - }; -} 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..5297e2db0 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 @@ -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. @@ -269,7 +269,7 @@ private void joinChannel(String channelId) { VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) )); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..e0f0de695 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 @@ -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. */ @@ -549,7 +549,6 @@ private void joinChannel(String channelId) { /* - * Please configure accessToken in the string_config file. * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..156d54c4d 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 @@ -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. @@ -280,7 +280,7 @@ private void joinChannel(String channelId) { option.publishMicrophoneTrack = true; option.publishTranscodedVideoTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..3765e8cd6 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 @@ -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. @@ -260,7 +260,7 @@ private void joinChannel(String channelId) { int code = engine.registerMediaMetadataObserver(iMetadataObserver, IMetadataObserver.VIDEO_METADATA); Log.e(TAG, code + ""); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..c15cfe19d 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 @@ -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. @@ -338,7 +338,7 @@ private void joinChannel(String channelId) { options.publishMicrophoneTrack = false; options.enableAudioRecordingOrPlayout = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..9222a0faa 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 @@ -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. @@ -265,7 +265,7 @@ private void joinChannel(String channelId) { option.publishMicrophoneTrack = true; option.publishCameraTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..6dc6e3972 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 @@ -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. @@ -241,7 +241,7 @@ private void joinChannel(String channelId) { VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) )); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..d74421d59 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 @@ -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. @@ -288,7 +288,7 @@ private void joinChannel(String channelId, boolean broadcast) { Log.d(TAG, mediaOptions.toString()); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..d356e4626 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 @@ -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. @@ -265,7 +265,7 @@ private void joinChannel(String channelId) { option.publishMicrophoneTrack = true; option.publishCameraTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..388f159f6 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 @@ -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. @@ -333,7 +333,7 @@ private void joinChannel(String channelId) { Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) ); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..7e0cb52f9 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 @@ -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..4551a21d2 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 @@ -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. @@ -268,7 +268,7 @@ private void joinChannel(String channelId) { engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); engine.setDefaultAudioRoutetoSpeakerphone(true); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..97179ddb0 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 @@ -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. @@ -244,7 +244,7 @@ private void joinChannel(String channelId) { engine.startPreview(); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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 deleted file mode 100644 index dcc49c6fc..000000000 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java +++ /dev/null @@ -1,555 +0,0 @@ -package io.agora.api.example.examples.advanced; - -import static io.agora.rtc2.video.VideoCanvas.RENDER_MODE_HIDDEN; -import static io.agora.rtc2.video.VideoEncoderConfiguration.STANDARD_BITRATE; - -import android.content.Context; -import android.graphics.SurfaceTexture; -import android.hardware.Camera; -import android.opengl.EGLSurface; -import android.opengl.GLES11Ext; -import android.opengl.GLES20; -import android.opengl.Matrix; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.SurfaceView; -import android.view.TextureView; -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.io.IOException; -import java.util.concurrent.Callable; - -import io.agora.api.example.MainApplication; -import io.agora.api.example.R; -import io.agora.api.example.common.BaseFragment; -import io.agora.api.example.common.gles.ProgramTextureOES; -import io.agora.api.example.common.gles.core.EglCore; -import io.agora.api.example.common.gles.core.GlUtil; -import io.agora.api.example.utils.CommonUtil; -import io.agora.api.example.utils.PermissonUtils; -import io.agora.api.example.utils.TokenUtils; -import io.agora.base.TextureBufferHelper; -import io.agora.base.VideoFrame; -import io.agora.base.internal.video.EglBase; -import io.agora.base.internal.video.EglBase14; -import io.agora.base.internal.video.RendererCommon; -import io.agora.base.internal.video.YuvConverter; -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; - -//@Example( -// index = 7, -// group = ADVANCED, -// name = R.string.item_pushexternal, -// actionId = R.id.action_mainFragment_to_PushExternalVideo, -// tipsId = R.string.pushexternalvideo -//) - -/** - * The type Push external video. - * - * @deprecated The impletation of custom has been moved to {@link PushExternalVideoYUV}. You can refer to {@link PushExternalVideoYUV} example. - */ -@Deprecated -public class PushExternalVideo extends BaseFragment implements View.OnClickListener, TextureView.SurfaceTextureListener, - SurfaceTexture.OnFrameAvailableListener { - private static final String TAG = PushExternalVideo.class.getSimpleName(); - private final int DEFAULT_CAPTURE_WIDTH = 640; - private final int DEFAULT_CAPTURE_HEIGHT = 480; - - private FrameLayout fl_local, fl_remote; - private Button join; - private EditText et_channel; - private RtcEngine engine; - private int myUid; - private volatile boolean joined = false; - - private YuvConverter mYuvConverter = new YuvConverter(); - private Handler mHandler; - private int mPreviewTexture; - private SurfaceTexture mPreviewSurfaceTexture; - private EglCore mEglCore; - private EGLSurface mDummySurface; - private EGLSurface mDrawSurface; - private ProgramTextureOES mProgram; - private float[] mTransform = new float[16]; - private float[] mMVPMatrix = new float[16]; - private boolean mMVPMatrixInit = false; - private Camera mCamera; - private int mFacing = Camera.CameraInfo.CAMERA_FACING_FRONT; - private boolean mPreviewing = false; - private int mSurfaceWidth; - private int mSurfaceHeight; - private boolean mTextureDestroyed; - private volatile boolean glPrepared; - private volatile TextureBufferHelper textureBufferHelper; - - private boolean prepareGl(EglBase.Context eglContext, final int width, final int height) { - Log.d(TAG, "prepareGl"); - textureBufferHelper = TextureBufferHelper.create("STProcess", eglContext); - if (textureBufferHelper == null) { - return false; - } - Log.d(TAG, "prepareGl completed"); - return true; - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_push_externalvideo, container, false); - return view; - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - join = view.findViewById(R.id.btn_join); - et_channel = view.findViewById(R.id.et_channel); - view.findViewById(R.id.btn_join).setOnClickListener(this); - fl_local = view.findViewById(R.id.fl_local); - fl_remote = view.findViewById(R.id.fl_remote); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - try { - RtcEngineConfig config = new RtcEngineConfig(); - /* - * The context of Android Activity - */ - config.mContext = context.getApplicationContext(); - /* - * The App ID issued to you by Agora. See How to get the App ID - */ - config.mAppId = getString(R.string.agora_app_id); - /* Sets the channel profile of the Agora RtcEngine. - CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. - Use this profile in one-on-one calls or group calls, where all users can talk freely. - CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast - channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; - an audience can only receive streams.*/ - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - /* - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); - config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - /* - * This parameter is for reporting the usages of APIExample to agora background. - * Generally, it is not necessary for you to set this parameter. - */ - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/ - LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - // This api can only be used in the private media server scenario, otherwise some problems may occur. - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - } - catch (Exception e) { - e.printStackTrace(); - getActivity().onBackPressed(); - } - } - - - @Override - public void onDestroy() { - /*leaveChannel and Destroy the RtcEngine instance*/ - if (engine != null) { - /*After joining a channel, the user must call the leaveChannel method to end the - * call before joining another channel. This method returns 0 if the user leaves the - * channel and releases all resources related to the call. This method call is - * asynchronous, and the user has not exited the channel when the method call returns. - * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. - * A successful leaveChannel method call triggers the following callbacks: - * 1:The local client: onLeaveChannel. - * 2:The remote client: onUserOffline, if the user leaving the channel is in the - * Communication channel, or is a BROADCASTER in the Live Broadcast profile. - * @returns 0: Success. - * < 0: Failure. - * PS: - * 1:If you call the destroy method immediately after calling the leaveChannel - * method, the leaveChannel process interrupts, and the SDK does not trigger - * the onLeaveChannel callback. - * 2:If you call the leaveChannel method during CDN live streaming, the SDK - * triggers the removeInjectStreamUrl method.*/ - engine.leaveChannel(); - engine.stopPreview(); - if (textureBufferHelper != null) { - textureBufferHelper.dispose(); - textureBufferHelper = null; - } - } - engine = null; - super.onDestroy(); - handler.post(RtcEngine::destroy); - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.btn_join) { - if (!joined) { - CommonUtil.hideInputBoard(getActivity(), et_channel); - // call when join button hit - String channelId = et_channel.getText().toString(); - // Check permission - checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { - @Override - public void onPermissionsResult(boolean allPermissionsGranted, String[] permissions, int[] grantResults) { - // Permissions Granted - if (allPermissionsGranted) { - joinChannel(channelId); - } - } - }); - } else { - fl_local.setVisibility(View.GONE); - getActivity().onBackPressed(); - } - } - } - - private void joinChannel(String channelId) { -// engine.setParameters("{\"rtc.log_filter\":65535}"); - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - - // Create render view by RtcEngine - TextureView textureView = new TextureView(getContext()); - //add SurfaceTextureListener - textureView.setSurfaceTextureListener(this); - // Add to the local container - fl_local.addView(textureView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); - /*Set up to play remote sound with receiver*/ - engine.setDefaultAudioRoutetoSpeakerphone(true); - - /*In the demo, the default is to enter as the anchor.*/ - engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); - // Enables the video module. - engine.enableVideo(); - // 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()) - )); - /*Configures the external video source. - * @param enable Sets whether or not to use the external video source: - * true: Use the external video source. - * false: Do not use the external video source. - * @param useTexture Sets whether or not to use texture as an input: - * true: Use texture as an input. - * false: (Default) Do not use texture as an input. - * @param pushMode - * VIDEO_FRAME: Use the ENCODED_VIDEO_FRAME. - * ENCODED_VIDEO_FRAME: Use the ENCODED_VIDEO_FRAME*/ - engine.setExternalVideoSource(true, true, Constants.ExternalVideoSourceType.VIDEO_FRAME); - - /*Please configure accessToken in the string_config file. - * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see - * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token - * A token generated at the server. This applies to scenarios with high-security requirements. For details, see - * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ - TokenUtils.gen(requireContext(), channelId, 0, token -> { - /* Allows a user to join a channel. - if you do not specify the uid, we will generate the uid for you*/ - - ChannelMediaOptions option = new ChannelMediaOptions(); - option.autoSubscribeAudio = true; - option.autoSubscribeVideo = true; - int res = engine.joinChannel(token, channelId, 0, option); - if (res != 0) { - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - showAlert(RtcEngine.getErrorDescription(Math.abs(res))); - return; - } - // Prevent repeated entry - join.setEnabled(false); - }); - - } - - @Override - public void onFrameAvailable(SurfaceTexture surfaceTexture) { - if (mTextureDestroyed) { - return; - } - - if (!mEglCore.isCurrent(mDrawSurface)) { - mEglCore.makeCurrent(mDrawSurface); - } - try { - surfaceTexture.updateTexImage(); - surfaceTexture.getTransformMatrix(mTransform); - } catch (Exception e) { - e.printStackTrace(); - } - - /*The rectangle ratio of frames and the screen surface may be different, so cropping may - * happen when display frames to the screen. - * The display transformation matrix does not change for the same camera when the screen - * orientation remains the same.*/ - if (!mMVPMatrixInit) { - /*For simplicity, we only consider the activity as portrait mode. In this case, the captured - * images should be rotated 90 degrees (left or right).Thus the frame width and height - * should be swapped.*/ - float frameRatio = DEFAULT_CAPTURE_HEIGHT / (float) DEFAULT_CAPTURE_WIDTH; - float surfaceRatio = mSurfaceWidth / (float) mSurfaceHeight; - Matrix.setIdentityM(mMVPMatrix, 0); - - if (frameRatio >= surfaceRatio) { - float w = DEFAULT_CAPTURE_WIDTH * surfaceRatio; - float scaleW = DEFAULT_CAPTURE_HEIGHT / w; - Matrix.scaleM(mMVPMatrix, 0, scaleW, 1, 1); - } else { - float h = DEFAULT_CAPTURE_HEIGHT / surfaceRatio; - float scaleH = DEFAULT_CAPTURE_WIDTH / h; - Matrix.scaleM(mMVPMatrix, 0, 1, scaleH, 1); - } - mMVPMatrixInit = true; - } - GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight); - mProgram.drawFrame(mPreviewTexture, mTransform, mMVPMatrix); - mEglCore.swapBuffers(mDrawSurface); - - if (joined) { - VideoFrame.Buffer buffer = textureBufferHelper.invoke(new Callable() { - @Override - public VideoFrame.Buffer call() throws Exception { - return textureBufferHelper.wrapTextureBuffer(DEFAULT_CAPTURE_HEIGHT, - DEFAULT_CAPTURE_WIDTH, VideoFrame.TextureBuffer.Type.OES, mPreviewTexture, - RendererCommon.convertMatrixToAndroidGraphicsMatrix(mTransform)); - } - }); - VideoFrame frame = new VideoFrame(buffer, 0, 0); - /*Pushes the video frame using the AgoraVideoFrame class and passes the video frame to the Agora SDK. - * Call the setExternalVideoSource method and set pushMode as true before calling this - * method. Otherwise, a failure returns after calling this method. - * @param frame AgoraVideoFrame - * @return - * true: The frame is pushed successfully. - * false: Failed to push the frame. - * PS: - * In the Communication profile, the SDK does not support textured video frames.*/ - boolean a = engine.pushExternalVideoFrame(frame); - Log.d(TAG, "pushExternalVideoFrame:" + a); - } - } - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { - Log.i(TAG, "onSurfaceTextureAvailable"); - mTextureDestroyed = false; - mSurfaceWidth = width; - mSurfaceHeight = height; - /* handler associate to the GL thread which creates the texture. - * in some condition SDK need to convert from texture format to YUV format, in this case, - * SDK will use this handler to switch into the GL thread to complete the conversion. - * */ - mHandler = new Handler(Looper.myLooper()); - mEglCore = new EglCore(); - if (!glPrepared) { - // setup egl context - EglBase.Context eglContext = new EglBase14.Context(mEglCore.getEGLContext()); - glPrepared = prepareGl(eglContext, width, height); - } - mDummySurface = mEglCore.createOffscreenSurface(1, 1); - mEglCore.makeCurrent(mDummySurface); - mPreviewTexture = GlUtil.createTextureObject(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); - mPreviewSurfaceTexture = new SurfaceTexture(mPreviewTexture); - mPreviewSurfaceTexture.setOnFrameAvailableListener(this); - mDrawSurface = mEglCore.createWindowSurface(surface); - mProgram = new ProgramTextureOES(); - if (mCamera != null || mPreviewing) { - Log.e(TAG, "Camera preview has been started"); - return; - } - try { - mCamera = Camera.open(mFacing); - /*It is assumed to capture images of resolution 640x480. During development, it should - * be the most suitable supported resolution that best fits the scenario.*/ - Camera.Parameters parameters = mCamera.getParameters(); - parameters.setPreviewSize(DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT); - mCamera.setParameters(parameters); - mCamera.setPreviewTexture(mPreviewSurfaceTexture); - /*The display orientation is 90 for both front and back facing cameras using a surface - * texture for the preview when the screen is in portrait mode.*/ - mCamera.setDisplayOrientation(90); - mCamera.startPreview(); - mPreviewing = true; - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { - - } - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { - Log.i(TAG, "onSurfaceTextureDestroyed"); - mTextureDestroyed = true; - if (mCamera != null && mPreviewing) { - mCamera.stopPreview(); - mPreviewing = false; - mCamera.release(); - mCamera = null; - } - mProgram.release(); - mEglCore.releaseSurface(mDummySurface); - mEglCore.releaseSurface(mDrawSurface); - mEglCore.release(); - return true; - } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surface) { - - } - - /** - * 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); - Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); - showLongToast(String.format("local user %d leaveChannel!", myUid)); - } - - /**Occurs when the local user joins a specified channel. - * The channel name assignment is based on channelName specified in the joinChannel method. - * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. - * @param channel Channel name - * @param uid User ID - * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - myUid = uid; - joined = true; - handler.post(new Runnable() { - @Override - public void run() { - join.setEnabled(true); - join.setText(getString(R.string.leave)); - } - }); - } - - /**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; - } - handler.post(() -> { - /*Display remote video stream*/ - // Create render view by RtcEngine - SurfaceView surfaceView = new SurfaceView(context); - surfaceView.setZOrderMediaOverlay(true); - if (fl_remote.getChildCount() > 0) { - fl_remote.removeAllViews(); - } - // Add to the remote container - fl_remote.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)); - } - }); - } - }; -} 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..183aca4fa 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 @@ -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. @@ -301,7 +301,7 @@ private void joinChannel(String channelId) { fl_local.addView(textureView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); engine.startPreview(Constants.VideoSourceType.VIDEO_SOURCE_CUSTOM); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..5385417e3 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 @@ -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. @@ -240,7 +240,7 @@ private void joinChannel(String channelId) { /*Set up to play remote sound with receiver*/ engine.setDefaultAudioRoutetoSpeakerphone(true); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..9aa4f0ee4 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 @@ -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. @@ -229,7 +229,7 @@ private void joinChannel(String channelId) { engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); engine.enableAudioVolumeIndication(1000, 3, true); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..a4f4cbd87 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 @@ -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. @@ -412,7 +412,7 @@ private void joinChannel() { startScreenSharePreview(); } - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..0311ad237 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 @@ -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. @@ -240,7 +240,7 @@ private void joinChannel(String channelId) { /*Set up to play remote sound with receiver*/ engine.setDefaultAudioRoutetoSpeakerphone(true); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..0a19f3db6 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 @@ -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. @@ -333,7 +333,7 @@ private void joinChannel(String channelId) { engine.setClientRole(CLIENT_ROLE_BROADCASTER); engine.enableAudioVolumeIndication(1000, 3, false); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..025cd6e19 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 @@ -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. @@ -336,7 +336,7 @@ private void joinChannel(String channelId, boolean broadcast) { engine.setSimulcastConfig(simulcastConfig); } - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..616988e25 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 @@ -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; @@ -219,7 +219,7 @@ private void joinChannel() { engine.setClientRole(io.agora.rtc2.Constants.CLIENT_ROLE_BROADCASTER); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..4f9cceefb 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. @@ -350,7 +350,7 @@ private void joinChannel(String channelId) { /*Set up to play remote sound with receiver*/ engine.setDefaultAudioRoutetoSpeakerphone(true); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..b694eae98 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 @@ -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. @@ -248,7 +248,7 @@ private void joinChannel(String channelId) { option.publishCameraTrack = false; option.publishCustomVideoTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..11ae7f4f6 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 @@ -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..892f5c2c9 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 @@ -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. @@ -293,7 +293,7 @@ private void joinChannel(String channelId) { VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) )); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..7bac6cf96 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 @@ -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. @@ -288,7 +288,7 @@ private void joinChannel(String channelId) { /*Set up to play remote sound with receiver*/ engine.setDefaultAudioRoutetoSpeakerphone(true); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..d8b7ef145 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 @@ -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. @@ -397,7 +397,7 @@ private void joinChannel(String channelId) { Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) ); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..09eef328a 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 @@ -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. @@ -233,7 +233,7 @@ private void joinChannel(String channelId) { engine.setExternalAudioSink(true, SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..6443918d0 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 @@ -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. @@ -275,7 +275,7 @@ private void joinChannel(String channelId) { config.enableLocalPlayback = false; customAudioTrack = engine.createCustomAudioTrack(Constants.AudioTrackType.AUDIO_TRACK_MIXABLE, config); - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..aa64576e3 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. @@ -123,7 +123,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { option.publishMicrophoneTrack = true; option.publishCameraTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..c5a15375e 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. @@ -120,7 +120,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { option.publishMicrophoneTrack = true; option.publishCameraTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..e6563ea47 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. @@ -121,7 +121,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { option.publishMicrophoneTrack = true; option.publishCameraTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..848b1b76d 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 @@ -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. @@ -171,7 +171,7 @@ private void joinChannel(String channelId) { option.autoSubscribeVideo = true; option.publishMicrophoneTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..f83c52537 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. @@ -552,7 +552,7 @@ private void joinChannel(String channelId) { option.autoSubscribeAudio = true; option.autoSubscribeVideo = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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..0e1ed1432 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. @@ -239,7 +239,7 @@ private void joinChannel(String channelId) { option.publishMicrophoneTrack = true; option.publishCameraTrack = true; - /*Please configure accessToken in the string_config file. + /* * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token * A token generated at the server. This applies to scenarios with high-security requirements. For details, see 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -