From 45f1f279f01087f7f6b5e657764399f0c1657bdb Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 29 Apr 2026 15:43:05 +0800 Subject: [PATCH 01/49] Polish beauty panel layout and clean up resources --- .../src/main/res/layout/fragment_beauty_faceunity.xml | 11 +++++++---- Android/APIExample/app/src/main/res/values/arrays.xml | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml b/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml index 2aa57b690..424518b90 100644 --- a/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml +++ b/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml @@ -61,17 +61,18 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#99000000" + android:minHeight="56dp" android:orientation="horizontal" - android:paddingVertical="12dp" app:layout_constraintBottom_toBottomOf="parent"> @@ -79,20 +80,22 @@ diff --git a/Android/APIExample/app/src/main/res/values/arrays.xml b/Android/APIExample/app/src/main/res/values/arrays.xml index 740b8015a..94927aeaf 100644 --- a/Android/APIExample/app/src/main/res/values/arrays.xml +++ b/Android/APIExample/app/src/main/res/values/arrays.xml @@ -204,7 +204,6 @@ VD_960x720 VD_1280x720 VD_1920x1080 - VD_1920x1080 VD_2540x1440 VD_3840x2160 From 081563acb6c8d3a4476bf9045524a88354214a4c Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 7 May 2026 16:04:31 +0800 Subject: [PATCH 02/49] chore(android): standardize case skill system across example apps --- .../.agent/skills/query-cases/SKILL.md | 114 ------ .../.agent/skills/review-case/SKILL.md | 57 --- .../.agent/skills/upsert-case/SKILL.md | 231 ------------ .../references/fragment-template.java | 209 ----------- .../.agents/skills/query-cases/SKILL.md | 49 +++ .../.agents/skills/review-case/SKILL.md | 48 +++ .../.agents/skills/upsert-case/SKILL.md | 35 ++ .../references/fragment-template.java | 64 ++++ Android/APIExample-Audio/AGENTS.md | 6 +- .../.agent/skills/query-cases/SKILL.md | 117 ------ .../.agent/skills/review-case/SKILL.md | 48 --- .../.agent/skills/upsert-case/SKILL.md | 185 ---------- .../references/composable-template.kt | 161 --------- .../.agents/skills/query-cases/SKILL.md | 50 +++ .../.agents/skills/review-case/SKILL.md | 46 +++ .../.agents/skills/upsert-case/SKILL.md | 50 +++ .../references/composable-template.kt | 112 ++++++ Android/APIExample-Compose/AGENTS.md | 6 +- Android/APIExample-Compose/ARCHITECTURE.md | 32 +- .../.agent/skills/query-cases/SKILL.md | 110 ------ .../.agent/skills/review-case/SKILL.md | 52 --- .../.agent/skills/upsert-case/SKILL.md | 342 ------------------ .../references/fragment-template.java | 207 ----------- .../.agents/skills/query-cases/SKILL.md | 50 +++ .../.agents/skills/review-case/SKILL.md | 45 +++ .../.agents/skills/upsert-case/SKILL.md | 51 +++ .../references/fragment-template.java | 46 +++ Android/APIExample/AGENTS.md | 6 +- 28 files changed, 674 insertions(+), 1855 deletions(-) delete mode 100644 Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md delete mode 100644 Android/APIExample-Audio/.agent/skills/review-case/SKILL.md delete mode 100644 Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md delete mode 100644 Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java create mode 100644 Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md create mode 100644 Android/APIExample-Audio/.agents/skills/review-case/SKILL.md create mode 100644 Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md create mode 100644 Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java delete mode 100644 Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md delete mode 100644 Android/APIExample-Compose/.agent/skills/review-case/SKILL.md delete mode 100644 Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md delete mode 100644 Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt create mode 100644 Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md create mode 100644 Android/APIExample-Compose/.agents/skills/review-case/SKILL.md create mode 100644 Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md create mode 100644 Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt delete mode 100644 Android/APIExample/.agent/skills/query-cases/SKILL.md delete mode 100644 Android/APIExample/.agent/skills/review-case/SKILL.md delete mode 100644 Android/APIExample/.agent/skills/upsert-case/SKILL.md delete mode 100644 Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java create mode 100644 Android/APIExample/.agents/skills/query-cases/SKILL.md create mode 100644 Android/APIExample/.agents/skills/review-case/SKILL.md create mode 100644 Android/APIExample/.agents/skills/upsert-case/SKILL.md create mode 100644 Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java diff --git a/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md b/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md deleted file mode 100644 index 4636e2b71..000000000 --- a/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -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 deleted file mode 100644 index ba517c3ea..000000000 --- a/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -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 deleted file mode 100644 index 9cb3d65e2..000000000 --- a/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -name: upsert-case -description: > - Add a new audio API example case or modify an existing one in the APIExample-Audio Android demo — - creates or updates Fragment class, XML layout, string resources, and nav_graph registration. - Use when: adding a new Agora audio API demo screen, modifying an existing case's implementation - or registration, implementing a new audio feature example in Java + XML layouts, registering a new - case via @Example annotation, subclassing BaseFragment for a new audio demo screen, or updating - an existing case's strings, layout, or nav entry. - This project uses voice-sdk — no video APIs available. - Keywords: add case, modify case, update case, new fragment, nav_graph, @Example, BaseFragment, - APIExample-Audio, audio case, voice-sdk, new screen, audio demo, upsert case. ---- - -# Upsert Case — APIExample-Audio - -## Adding a New Case - -Touch exactly 4 files (all paths relative to `app/src/main/`): - -| File | What to add | -|---|---| -| `java/.../examples/{basic\|advanced\|audio}/YourCaseName.java` | Fragment class | -| `res/layout/fragment_your_case_name.xml` | XML layout | -| `res/values/strings.xml` | 2 strings | -| `res/navigation/nav_graph.xml` | 1 action + 1 destination | - -Registration is automatic via reflection — no other files needed. - -**voice-sdk constraint**: Do NOT call `enableVideo()`, `setupLocalVideo()`, `VideoCanvas`, or any video API — the module does not exist and will crash at runtime. - ---- - -### Step 1: Clarify before coding - -Before writing a single line, ask: -- **What audio API am I demonstrating?** — determines which existing case is the closest reference to copy patterns from -- **BASIC or ADVANCED group?** — BASIC for fundamental join/leave audio patterns; ADVANCED for feature-specific audio APIs -- **What's the sort index?** — index must be unique within the group. BASIC uses 0–9, ADVANCED starts from 10. Run `query-cases` skill first; a collision causes silent ordering bugs at runtime -- **Any special permissions beyond `RECORD_AUDIO`?** — most audio cases only need `RECORD_AUDIO`; check if the API requires anything else - ---- - -### Step 2: Create the Fragment - -**MANDATORY — READ ENTIRE FILE before writing any code**: -[`references/fragment-template.java`](references/fragment-template.java) - -Do NOT skip — the `setParameters`, `handler.post`, `getPrivateCloudConfig()` null-check, `AudioSeatManager` wiring, and voice-sdk constraints are only fully shown there and are required in every case. - -**Do NOT load** any other reference files for this task. - -Non-obvious points the template highlights: - -- `setParameters(...)` for app scenario reporting — **required in every case**, do not remove -- `handler.post(RtcEngine::destroy)` — NOT `RtcEngine.destroy()` directly; direct call blocks UI thread (ANR) -- `getPrivateCloudConfig()` null-check before `setLocalAccessPoint()` — returns null on non-private-cloud builds (NPE) -- All `IRtcEngineEventHandler` callbacks run on a **background thread** — always `runOnUIThread()` for UI -- `onActivityCreated` → create engine; `onDestroy` → `leaveChannel()` then `handler.post(RtcEngine::destroy)` -- `ChannelMediaOptions` must NOT set `publishCameraTrack` or `autoSubscribeVideo` — voice-sdk has no video module -- Use `AudioSeatManager` (not `VideoReportLayout`) to visualize remote participants - ---- - -### Step 3: Create the XML layout - -Typical audio layout — channel input + join button + audio controls: - -```xml - - - - - - - - - - - - -``` - -For waveform visualization, copy the `WaveformView` pattern from `fragment_join_channel_audio.xml`. - ---- - -### Step 4: Add nav entries - -File: `res/navigation/nav_graph.xml` - -**Action** — inside `` (NOT mainFragment — mainFragment only has one action, to Ready): - -```xml - -``` - -**Destination** — at root `` level: - -```xml - -``` - -`action android:id` must exactly match `actionId` in `@Example`. - ---- - -### Step 5: Update ARCHITECTURE.md - -Add one line to the case list in `ARCHITECTURE.md` under the correct directory section (`basic/`, `advanced/`, or `audio/`): - -``` -├── YourCaseName.java # [index] "Display Name" — key API description -``` - -Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans. - ---- - -## Modifying an Existing Case - -When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating: - -| What changed | Files to touch | -|---|---| -| Implementation logic (API calls, event handling) | `java/.../examples/{basic\|advanced\|audio}/CaseName.java` | -| UI layout (views, controls) | `res/layout/fragment_case_name.xml` | -| Display name or tips text | `res/values/strings.xml` | -| Sort index or group (BASIC ↔ ADVANCED) | `@Example` annotation in the Fragment class | -| Navigation label | `res/navigation/nav_graph.xml` (fragment label attribute) | -| Class rename or package move | Fragment class, `nav_graph.xml` (android:name + destination id), `@Example` annotation (actionId), layout file name, `ARCHITECTURE.md` | - -After making changes: - -1. **Verify `@Example` annotation consistency** — ensure `index`, `group`, `name`, `actionId`, and `tipsId` still match the actual string resources, nav action ID, and intended group/position. A mismatch causes the case to silently disappear from the list or navigate to the wrong screen. -2. **Update `res/values/strings.xml`** if the display name or tips text changed. -3. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed. -4. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description. - ---- - -## Verify - -```bash -./gradlew assembleDebug -``` - -- [ ] Case appears in correct group at expected sort position -- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment) -- [ ] `onJoinChannelSuccess` fires in Logcat -- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy` -- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description -- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries - ---- - -## When to Use a Spec Instead - -If the case meets any of the following criteria, create a Spec rather than using this skill directly: - -1. Involves coordinated calls across two or more Agora API modules -2. Requires a custom UI layout (not one of the standard templates above) -3. Manages multiple channels or multiple engine instances -4. Requires a foreground Service or background thread coordination -5. Involves developing new shared components (widget/utils, etc.) -6. Requires optional module integration (e.g. streamEncrypt) - -If none apply → use this skill directly; no Spec needed. - -### Spec Requirements Document Must Include - -- List of APIs the case demonstrates (audio APIs only) -- User interaction flow description -- Expected RtcEngine lifecycle behavior -- Required permissions (typically only `RECORD_AUDIO`) - -### Spec Design Document Must Include - -- Target project identifier: `APIExample-Audio` -- Class/file structure design -- API call sequence (Mermaid sequence diagram recommended) -- State management approach -- UI layout plan -- Integration points with existing shared components -- Case registration info: class name, display name, group (BASIC/ADVANCED), sort index — finalize during design to avoid conflicts -- Generate `@Example` annotation parameters, `nav_graph.xml` action + destination, `strings.xml` key names (`item_` prefix) -- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing indices -- voice-sdk checks: no video APIs (`enableVideo`, `setupLocalVideo`, `setupRemoteVideo`, `VideoCanvas`, `startScreenCapture`) — violations must be eliminated at design time -- Risk identification and mitigation (API availability, permissions, thread safety, performance) - -### Spec Task List Integration - -- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters -- Mark which sub-tasks require manual coding, and provide target file paths and change summaries -- New shared component creation tasks must come before case implementation tasks - ---- - -## NEVER - -- **NEVER** call any video API (`enableVideo`, `setupLocalVideo`, `VideoCanvas`) — voice-sdk has no video module; crash is immediate. -- **NEVER** put the nav action inside `` — it belongs in ``. mainFragment only routes to Ready; all case actions live in Ready. Wrong placement causes silent navigation failure at runtime. -- **NEVER** call `RtcEngine.destroy()` directly on the main thread — always `handler.post(RtcEngine::destroy)`. Direct call blocks the UI thread and causes ANR. -- **NEVER** call `setLocalAccessPoint()` without null-checking `getPrivateCloudConfig()` first — it returns null on standard builds, causing NPE. -- **NEVER** update UI directly inside `IRtcEngineEventHandler` callbacks — they run on a background thread. Always wrap with `runOnUIThread()`. -- **NEVER** omit `setParameters(...)` — it's required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally. diff --git a/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java b/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java deleted file mode 100644 index b4e9214c7..000000000 --- a/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java +++ /dev/null @@ -1,209 +0,0 @@ -package io.agora.api.example.examples.advanced; - -import static io.agora.api.example.common.model.Examples.ADVANCED; - -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Random; - -import io.agora.api.example.MainApplication; -import io.agora.api.example.R; -import io.agora.api.example.annotation.Example; -import io.agora.api.example.common.BaseFragment; -import io.agora.api.example.common.widget.AudioSeatManager; -import io.agora.api.example.utils.PermissonUtils; -import io.agora.api.example.utils.TokenUtils; -import io.agora.rtc2.ChannelMediaOptions; -import io.agora.rtc2.Constants; -import io.agora.rtc2.IRtcEngineEventHandler; -import io.agora.rtc2.RtcEngine; -import io.agora.rtc2.RtcEngineConfig; -import io.agora.rtc2.proxy.LocalAccessPointConfiguration; - -// NOTE: This project uses voice-sdk. -// Do NOT import or call: enableVideo(), setupLocalVideo(), VideoCanvas, VideoReportLayout, -// setVideoEncoderConfiguration(), or any other video API — the module does not exist. - -/** - * This demo demonstrates how to use [describe the audio feature here]. - * - * Key APIs used: - * - RtcEngine.yourAudioApi() - */ -@Example( - index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+ - group = ADVANCED, // BASIC or ADVANCED - name = R.string.item_your_case_name, - actionId = R.id.action_mainFragment_to_yourCaseName, - tipsId = R.string.your_case_name_tips -) -public class YourCaseName extends BaseFragment implements View.OnClickListener { - private static final String TAG = YourCaseName.class.getSimpleName(); - - private Button join; - private EditText et_channel; - private RtcEngine engine; - private int myUid; - private boolean joined = false; - - // AudioSeatManager visualizes remote audio participants — add seat views in your XML layout - private AudioSeatManager audioSeatManager; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_your_case_name, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - join = view.findViewById(R.id.btn_join); - et_channel = view.findViewById(R.id.et_channel); - join.setOnClickListener(this); - // bind additional feature-specific views here - - // Wire AudioSeatManager to seat views defined in your XML layout - // audioSeatManager = new AudioSeatManager( - // view.findViewById(R.id.audio_place_01), - // view.findViewById(R.id.audio_place_02) - // ); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Context context = getContext(); - if (context == null) return; - try { - RtcEngineConfig config = new RtcEngineConfig(); - config.mContext = context.getApplicationContext(); - config.mAppId = getAgoraAppId(); - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); - config.mAreaCode = ((MainApplication) getActivity().getApplication()) - .getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - // REQUIRED in every case — do not remove - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - // null-check is mandatory — returns null on non-private-cloud builds - LocalAccessPointConfiguration localAccessPointConfiguration = - ((MainApplication) getActivity().getApplication()) - .getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - } catch (Exception e) { - e.printStackTrace(); - getActivity().onBackPressed(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (engine != null) { - engine.leaveChannel(); - } - // MUST use handler.post — do NOT call RtcEngine.destroy() directly on main thread - handler.post(RtcEngine::destroy); - engine = null; - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.btn_join) { - if (!joined) { - String channelId = et_channel.getText().toString(); - checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { - @Override - public void onPermissionsResult(boolean allPermissionsGranted, - String[] permissions, int[] grantResults) { - if (allPermissionsGranted) { - joinChannel(channelId); - } - } - }); - } else { - joined = false; - engine.leaveChannel(); - join.setText(getString(R.string.join)); - } - } - } - - private void joinChannel(String channelId) { - engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); - // --- feature-specific audio setup goes here --- - // e.g. engine.setAudioProfile(...); engine.setVoiceBeautifierPreset(...); - - ChannelMediaOptions options = new ChannelMediaOptions(); - options.autoSubscribeAudio = true; - options.publishMicrophoneTrack = true; - // Do NOT set publishCameraTrack or autoSubscribeVideo — voice-sdk has no video module - - int uid = new Random().nextInt(1000) + 100000; - TokenUtils.gen(requireContext(), channelId, uid, token -> { - int res = engine.joinChannel(token, channelId, uid, options); - if (res != 0) { - showAlert(RtcEngine.getErrorDescription(Math.abs(res))); - return; - } - join.setEnabled(false); - }); - } - - private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - myUid = uid; - joined = true; - // ALL UI updates must go through runOnUIThread — callbacks run on background thread - runOnUIThread(() -> { - join.setEnabled(true); - join.setText(getString(R.string.leave)); - }); - } - - @Override - public void onUserJoined(int uid, int elapsed) { - Log.i(TAG, "onUserJoined -> " + uid); - runOnUIThread(() -> { - // audioSeatManager.addUser(uid); - }); - } - - @Override - public void onUserOffline(int uid, int reason) { - Log.i(TAG, String.format("user %d offline, reason %d", uid, reason)); - runOnUIThread(() -> { - // audioSeatManager.removeUser(uid); - }); - } - - @Override - public void onError(int err) { - showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err)); - } - }; -} diff --git a/Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md b/Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md new file mode 100644 index 000000000..796ae664b --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md @@ -0,0 +1,49 @@ +--- +name: query-cases +description: > + Query and browse existing API example cases in the APIExample-Audio Android demo. + Use when: someone asks what audio cases exist, which case demonstrates a specific + Agora voice API, whether a BASIC or ADVANCED slot is free, or whether a case is + fully registered. This project uses voice-sdk only and does not support video APIs. + Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED, + existing cases, available cases, audio case, voice API. +--- + +# Query Cases — APIExample-Audio + +## What this skill is for + +Use this skill when the question is about which audio cases already exist in `APIExample-Audio/`, which case demonstrates a specific voice API, whether a BASIC or ADVANCED slot is free, or whether an audio case is fully registered. + +## Source of truth + +1. `APIExample-Audio/ARCHITECTURE.md` +2. `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**` +3. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml` +4. `APIExample-Audio/app/src/main/res/values/strings.xml` + +## Procedure + +1. Start with `ARCHITECTURE.md` to answer ordinary "what exists?" or "which case uses API X?" questions quickly. +2. Read the relevant case source under `examples/basic/`, `examples/advanced/`, or `examples/audio/` when the answer depends on live `@Example` values, method usage, or whether the architecture doc is stale. +3. For API-to-case mapping, prefer the class Javadoc and then confirm by scanning for the API call in the case implementation. +4. For registration checks, confirm the case has a live `@Example` annotation, a matching destination and action in `nav_graph.xml`, and corresponding string resources in `strings.xml`. +5. For free-index or collision checks, scan active `@Example` annotations from source immediately before answering; treat `examples/audio/` as part of the `ADVANCED` index namespace. +6. Present results with explicit group, index, case name, file path, and key API when applicable, and call out any stale or inconsistent registration artifacts. + +## Verify + +- Re-scan source before reporting any free BASIC or ADVANCED index +- Confirm commented-out annotations such as `//@Example` are excluded from active-case results +- Confirm any claimed registration status matches `@Example`, `nav_graph.xml`, and `strings.xml` + +## Out of scope + +- Editing or registering cases +- Recommending video APIs +- Approving implementation quality + +## Never + +- Never suggest `enableVideo()`, `setupLocalVideo()`, or `VideoCanvas` +- Never report a free ADVANCED index without scanning both `advanced/` and `audio/` diff --git a/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md new file mode 100644 index 000000000..3b3e95c23 --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md @@ -0,0 +1,48 @@ +--- +name: review-case +description: > + Review an APIExample-Audio case after it has been created or modified. + Use when: checking lifecycle, permission, registration, and voice-SDK-only + constraints before considering a case ready. Always run the minimum build + verification command as part of the review. + Keywords: review, audit, check, lifecycle, threading, permissions, compliance. +--- + +# Review Case — APIExample-Audio + +## What this skill is for + +Use this skill after a case has been created or modified in `APIExample-Audio/`. It checks lifecycle, permission, registration, and voice-SDK-only constraints, then runs the minimum build verification command. + +## Source of truth + +1. `APIExample-Audio/AGENTS.md` +2. `APIExample-Audio/ARCHITECTURE.md` +3. Target case source under `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**` +4. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml` +5. `APIExample-Audio/app/src/main/res/values/strings.xml` + +## Procedure + +1. Open the target case and verify teardown order: `leaveChannel()` before `RtcEngine.destroy()`, aligned with the audio project's documented lifecycle rule. +2. Verify all UI updates triggered from `IRtcEngineEventHandler` callbacks are dispatched back to the UI thread. +3. For cases that call `joinChannel()`, verify microphone permission is requested through the project's permission helper before join, and that the case remains audio-only. +4. Check for required engine-init safeguards used in this project, including backend reporting setup and private-cloud null-guarding where applicable. +5. Confirm the case registration still aligns across `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md`. +6. Run the minimum build verification command from `APIExample-Audio/` before finalizing the review. + +## Verify + +- Run `./gradlew assembleDebug` from `APIExample-Audio/` +- Confirm the target case does not call `enableVideo()`, `setupLocalVideo()`, or `VideoCanvas` +- Confirm `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md` still align + +## Out of scope + +- Fixing the case during review by default +- Treating compile success as proof of runtime correctness + +## Never + +- Never approve any video API usage in the audio-only project +- Never skip the build command diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..7048e6920 --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,35 @@ +## What this skill is for + +Use this skill to add or update a case in `APIExample-Audio/`. It owns the full change closure: case source, XML layout, strings, `nav_graph.xml`, and `ARCHITECTURE.md`, while preserving the voice-SDK-only constraint. + +## Source of truth + +1. `APIExample-Audio/AGENTS.md` +2. `APIExample-Audio/ARCHITECTURE.md` +3. `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**` +4. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml` +5. `APIExample-Audio/app/src/main/res/values/strings.xml` +6. `APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java` + +## Procedure + +1. Run `query-cases` first when the case index, group placement, or nearby examples are unknown. +2. Create or update the Fragment, layout, strings, nav entries, and `ARCHITECTURE.md` together so the case stays fully registered. +3. Use `APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java` as the reference for engine lifecycle, usage reporting, private-cloud setup, and voice-only guardrails. +4. Keep `@Example`, action ID, destination ID, string resources, and audio-only constraints aligned across the implementation. + +## Verify + +- Run `./gradlew assembleDebug` from `APIExample-Audio/` +- Confirm the edited case avoids video APIs and keeps `@Example`, layout, strings, nav, and docs aligned + +## Out of scope + +- Reviewing the finished implementation as an independent reviewer +- Omitting docs or registration updates because the fragment compiles + +## Never + +- Never add `publishCameraTrack` +- Never call `enableVideo()` +- Never skip `ARCHITECTURE.md` diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java new file mode 100644 index 000000000..65191135f --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java @@ -0,0 +1,64 @@ +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.widget.AudioSeatManager; +import io.agora.rtc2.ChannelMediaOptions; +import io.agora.rtc2.IRtcEngineEventHandler; +import io.agora.rtc2.RtcEngine; +import io.agora.rtc2.RtcEngineConfig; +import io.agora.rtc2.proxy.LocalAccessPointConfiguration; + +/** + * Reference skeleton only. Adapt this to a real case Fragment with its own UI, join flow, + * and permission handling before compiling it into the project. + */ +public abstract class AudioExampleCaseTemplate extends BaseFragment { + protected RtcEngine engine; + protected AudioSeatManager audioSeatManager; + protected final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + }; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Context context = getContext(); + if (context == null || engine != null) { + return; + } + RtcEngineConfig config = new RtcEngineConfig(); + config.mContext = context.getApplicationContext(); + config.mAppId = getAgoraAppId(); + config.mEventHandler = iRtcEngineEventHandler; + config.mAreaCode = + ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); + engine = RtcEngine.create(config); + engine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\"" + + RtcEngine.getSdkVersion() + "\"}}"); + LocalAccessPointConfiguration privateCloud = + ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); + if (privateCloud != null) { + engine.setLocalAccessPoint(privateCloud); + } + } + + protected ChannelMediaOptions buildAudioOnlyOptions() { + ChannelMediaOptions options = new ChannelMediaOptions(); + options.autoSubscribeAudio = true; + options.publishMicrophoneTrack = true; + return options; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (engine != null) { + engine.leaveChannel(); + RtcEngine.destroy(); + engine = null; + } + } +} diff --git a/Android/APIExample-Audio/AGENTS.md b/Android/APIExample-Audio/AGENTS.md index 092f45f59..a7a31663e 100644 --- a/Android/APIExample-Audio/AGENTS.md +++ b/Android/APIExample-Audio/AGENTS.md @@ -29,9 +29,9 @@ See [README.md — Obtain an App Id](README.md#obtain-an-app-id). | 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 | +| upsert-case | `.agents/skills/upsert-case/` | Add a new audio case or modify an existing one | +| query-cases | `.agents/skills/query-cases/` | Query and browse existing audio cases | +| review-case | `.agents/skills/review-case/` | Review a case against project red lines | ## Further Reading diff --git a/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md b/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md deleted file mode 100644 index e4bb11024..000000000 --- a/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -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 deleted file mode 100644 index fd0b84bad..000000000 --- a/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -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 deleted file mode 100644 index 3faecf254..000000000 --- a/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -name: upsert-case -description: > - Add a new API example case or modify an existing one in the APIExample-Compose Android demo — - creates or updates a Kotlin Composable file, registers or updates it in Examples.kt, and manages - string resources. Use when: adding a new Agora RTC API demo screen in Jetpack Compose, modifying - an existing case's implementation or registration, porting an existing APIExample case to Compose, - implementing a new feature example in Kotlin + Compose UI, registering a new entry in - BasicExampleList or AdvanceExampleList, or updating an existing case's strings or Examples.kt entry. - Kotlin only — no XML layouts, no Fragments. Keywords: add case, modify case, update case, - new composable, Examples.kt, BasicExampleList, AdvanceExampleList, APIExample-Compose, Compose case, - new screen, Jetpack Compose, RTC API example, upsert case. ---- - -# Upsert Case — APIExample-Compose - -## Adding a New Case - -Touch exactly 3 files (all paths relative to `app/src/main/`): - -| File | What to add | -|---|---| -| `java/.../compose/samples/YourCaseName.kt` | Composable file | -| `java/.../compose/model/Examples.kt` | 1 list entry | -| `res/values/strings.xml` | 1 string | - -No `nav_graph.xml` changes — navigation routes by list position automatically. - ---- - -### Step 1: Clarify before coding - -Before writing a single line, ask: -- **What API am I demonstrating?** — determines which existing case is the closest reference (`JoinChannelVideo.kt` for video, `JoinChannelAudio.kt` for audio) -- **Video or audio-only?** — determines permissions (`CAMERA` + `RECORD_AUDIO` vs `RECORD_AUDIO` only), whether `enableVideo()` and `VideoGrid` are needed -- **BasicExampleList or AdvanceExampleList?** — Basic for fundamental join/leave patterns; Advance for feature-specific APIs -- **List position?** — run `query-cases` skill to see current entries; list order is display order - ---- - -### Step 2: Create the Composable file - -**MANDATORY — READ ENTIRE FILE before writing any code**: -[`references/composable-template.kt`](references/composable-template.kt) - -Do NOT skip — the `SettingPreferences.getArea()`, `DisposableEffect` key, `rememberSaveable` vs `remember` rules, and `@Preview` placement are only fully shown there and are required in every case. - -**Do NOT load** any other reference files for this task. - -Non-obvious points the template highlights: - -- `mAreaCode = SettingPreferences.getArea()` — **required**, do not hardcode or omit -- `DisposableEffect(lifecycleOwner)` — key must be `lifecycleOwner`, not `Unit`; wrong key means cleanup never fires on back navigation -- `rememberSaveable` for channelName, isJoined, uid, videoIdList — survives rotation -- `remember` for RtcEngine — must NOT be `rememberSaveable` (engine is not serializable) -- `IRtcEngineEventHandler` callbacks can mutate Compose state directly — snapshot system is thread-safe, no `runOnUIThread()` needed -- `Toast`/`Dialog`/`AlertDialog` inside callbacks still need main thread — use `coroutineScope.launch(Dispatchers.Main) { }` -- `@Preview` goes on the **private** `*View` function only — never on the public stateful entry - ---- - -### Step 3: Register in Examples.kt - -File: `app/src/main/java/io/agora/api/example/compose/model/Examples.kt` - -```kotlin -val AdvanceExampleList = listOf( - // … existing entries … - Example(R.string.example_your_case_name) { YourCaseName() } -) -``` - -List order is display order — position determines where the case appears in the UI. - ---- - -### Step 4: Add string resource - -File: `app/src/main/res/values/strings.xml` - -```xml -Your Case Name -``` - -String key must use the `example_` prefix. No separate tips string needed (unlike APIExample). - ---- - -### Step 5: Update ARCHITECTURE.md - -Add one line to the case list in `ARCHITECTURE.md` under the correct directory section: - -``` -├── YourCaseName.kt # "Display Name" — key API description -``` - -Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans. - ---- - -## Modifying an Existing Case - -When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating: - -| What changed | Files to touch | -|---|---| -| Implementation logic (API calls, event handling, Compose state) | `java/.../compose/samples/CaseName.kt` | -| Display name | `res/values/strings.xml` | -| List group (Basic ↔ Advance) or position | `java/.../compose/model/Examples.kt` (move entry between lists or reorder) | -| Composable function rename | `CaseName.kt` (file + function name), `Examples.kt` (lambda reference), `ARCHITECTURE.md` | - -After making changes: - -1. **Verify `Examples.kt` entry consistency** — ensure the string resource reference, composable lambda, and list placement (`BasicExampleList` or `AdvanceExampleList`) still match the actual case. A mismatch causes the case to silently disappear from the list or render the wrong screen. -2. **Update `res/values/strings.xml`** if the display name changed. -3. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description. - ---- - -## Verify - -```bash -./gradlew assembleDebug -``` - -- [ ] Case appears in the correct group at the expected list position -- [ ] Tap navigates to the case screen -- [ ] Channel join succeeds and `isJoined` flips to `true` -- [ ] Press back — check Logcat for `RtcEngine.destroy` within ~2 seconds; if missing, `DisposableEffect` key is wrong or `onDispose` is incomplete -- [ ] Rotate screen — `channelName` and `isJoined` survive (`rememberSaveable` working) -- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description -- [ ] `Examples.kt` entry is consistent — string resource, composable lambda, and list placement match the actual case - ---- - -## When to Use a Spec Instead - -If the case meets any of the following criteria, create a Spec rather than using this skill directly: - -1. Involves coordinated calls across two or more Agora API modules -2. Requires a custom Composable layout not covered by the standard template above -3. Manages multiple channels or multiple engine instances -4. Requires a foreground Service or background coroutine coordination -5. Involves developing new shared components (shared Composables / utils) -6. Requires optional module integration (simpleFilter / streamEncrypt) - -If none apply → use this skill directly; no Spec needed. - -### Spec Requirements Document Must Include - -- List of APIs the case demonstrates -- User interaction flow description -- Expected RtcEngine lifecycle behavior -- Required permissions list - -### Spec Design Document Must Include - -- Target project identifier: `APIExample-Compose` -- Composable function structure design -- API call sequence (Mermaid sequence diagram recommended) -- State management plan (`remember` vs `rememberSaveable` boundaries) -- UI layout plan -- Integration points with existing shared components -- Case registration info: `Examples.kt` list entry, `strings.xml` key (`example_` prefix) — finalize during design to avoid conflicts -- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing entries -- Compose-specific checks: `DisposableEffect(lifecycleOwner)`, `rememberSaveable` vs `remember`, main-thread dispatch for Toast/Dialog -- Risk identification and mitigation (API compatibility, performance, permissions, thread safety, rotation/config changes) - -### Spec Task List Integration - -- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters -- Mark which sub-tasks require manual coding, and provide target file paths and change summaries -- Tasks for creating new shared Composables must come before case implementation tasks - ---- - -## NEVER - -- **NEVER** use XML layouts, `Fragment`, or `ViewBinding` — Compose only. -- **NEVER** use `remember` for channelName, isJoined, or uid — they must be `rememberSaveable` to survive rotation. -- **NEVER** use `rememberSaveable` for `RtcEngine` — it is not serializable and will crash on rotation. -- **NEVER** use `Unit` as the `DisposableEffect` key — it fires only once and won't clean up on back navigation. Always use `lifecycleOwner`. -- **NEVER** put `@Preview` on the public stateful function — it will crash because `LocalContext` and `LocalLifecycleOwner` are unavailable in preview. Only preview the private `*View` function. -- **NEVER** call `Toast`/`Dialog`/`AlertDialog` directly inside `IRtcEngineEventHandler` callbacks — they require the main thread. Use `coroutineScope.launch(Dispatchers.Main) { }`. -- **NEVER** hardcode `mAreaCode` — always use `SettingPreferences.getArea()`. diff --git a/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt deleted file mode 100644 index 29d8c0da3..000000000 --- a/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt +++ /dev/null @@ -1,161 +0,0 @@ -package io.agora.api.example.compose.samples - -import android.Manifest -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.tooling.preview.Preview -import io.agora.api.example.compose.data.SettingPreferences -import io.agora.api.example.compose.ui.common.ChannelNameInput -import io.agora.api.example.compose.utils.AgoraConfig -import io.agora.api.example.compose.utils.TokenUtils -import io.agora.rtc2.ChannelMediaOptions -import io.agora.rtc2.Constants -import io.agora.rtc2.IRtcEngineEventHandler -import io.agora.rtc2.RtcEngine -import io.agora.rtc2.RtcEngineConfig - -// For video cases, also import: -// import io.agora.api.example.compose.ui.common.VideoGrid -// import io.agora.rtc2.video.VideoCanvas -// import io.agora.rtc2.video.VideoEncoderConfiguration - -/** - * Demonstrates how to use [describe the feature here]. - * - * Key APIs used: - * - RtcEngine.yourApi() - */ -// PUBLIC stateful entry point — no @Preview here -@Composable -fun YourCaseName() { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - - // rememberSaveable: survives rotation — use for channelName, isJoined, uid, videoIdList - var isJoined by rememberSaveable { mutableStateOf(false) } - var channelName by rememberSaveable { mutableStateOf("") } - var localUid by rememberSaveable { mutableIntStateOf(0) } - - // remember: survives recomposition but NOT rotation — use for RtcEngine, collections - val rtcEngine = remember { - RtcEngine.create(RtcEngineConfig().apply { - mAreaCode = SettingPreferences.getArea() // REQUIRED — do not hardcode - mContext = context - mAppId = AgoraConfig.getAppId() - mEventHandler = object : IRtcEngineEventHandler() { - // IRtcEngineEventHandler callbacks are safe to mutate Compose state directly — - // the snapshot system is thread-safe. No runOnUIThread() needed. - override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { - super.onJoinChannelSuccess(channel, uid, elapsed) - isJoined = true - localUid = uid - } - - override fun onLeaveChannel(stats: RtcStats?) { - super.onLeaveChannel(stats) - isJoined = false - } - - override fun onUserJoined(uid: Int, elapsed: Int) { - super.onUserJoined(uid, elapsed) - // add uid to videoIdList for video cases - } - - override fun onUserOffline(uid: Int, reason: Int) { - super.onUserOffline(uid, reason) - // remove uid from videoIdList for video cases - } - } - }).apply { - // feature-specific engine setup goes here - // e.g. enableVideo(); setVideoEncoderConfiguration(...) - } - } - - // MUST use lifecycleOwner as key — ensures cleanup fires when screen leaves composition - DisposableEffect(lifecycleOwner) { - onDispose { - if (isJoined) rtcEngine.leaveChannel() - RtcEngine.destroy() - // NOTE: Toast/Dialog/AlertDialog MUST be called on main thread. - // Inside onDispose this is fine. Inside IRtcEngineEventHandler callbacks, - // wrap with: coroutineScope.launch(Dispatchers.Main) { ... } - } - } - - val permissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { grantedMap -> - // Permission callbacks run on main thread — Toast is safe here - if (grantedMap.values.all { it }) { - TokenUtils.gen(channelName, 0) { token -> - val options = ChannelMediaOptions().apply { - channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING - clientRoleType = Constants.CLIENT_ROLE_BROADCASTER - publishMicrophoneTrack = true - // publishCameraTrack = true // add for video cases - } - rtcEngine.joinChannel(token, channelName, 0, options) - } - } - } - - // Delegate all UI to the private stateless View function - YourCaseNameView( - channelName = channelName, - isJoined = isJoined, - onJoinClick = { name -> - channelName = name - permissionLauncher.launch( - arrayOf(Manifest.permission.RECORD_AUDIO) - // add Manifest.permission.CAMERA for video cases - ) - }, - onLeaveClick = { rtcEngine.leaveChannel() } - ) -} - -// @Preview goes here on the PRIVATE stateless function — never on the stateful entry above -@Preview -@Composable -private fun YourCaseNamePreview() { - YourCaseNameView( - channelName = "test", - isJoined = false, - onJoinClick = {}, - onLeaveClick = {} - ) -} - -// PRIVATE stateless View — receives only plain data and lambdas, no engine/state -@Composable -private fun YourCaseNameView( - channelName: String, - isJoined: Boolean, - onJoinClick: (String) -> Unit, - onLeaveClick: () -> Unit -) { - Column(Modifier.fillMaxSize()) { - // feature-specific UI here - // For video cases: VideoGrid(videoIdList, setupVideo, ...) - ChannelNameInput( - channelName = channelName, - isJoined = isJoined, - onJoinClick = onJoinClick, - onLeaveClick = onLeaveClick - ) - } -} diff --git a/Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md b/Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md new file mode 100644 index 000000000..391adba55 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md @@ -0,0 +1,50 @@ +--- +name: query-cases +description: > + Query and browse existing API example cases in the APIExample-Compose Android demo — + lists cases by group, finds which Composables demonstrate a specific Agora API, + checks append 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, 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. +--- + +## What this skill is for + +Use this skill when the question is about which Compose cases already exist in `APIExample-Compose/`, which Composables demonstrate a specific RTC API, where a case is registered in `Examples.kt`, or whether a new list position should be appended. + +## Source of truth + +1. `APIExample-Compose/ARCHITECTURE.md` +2. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt` +3. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/**` +4. `APIExample-Compose/app/src/main/res/values/strings.xml` + +## Procedure + +1. Read `ARCHITECTURE.md` first for the fast case index and registration model. +2. Re-scan live registrations from `Examples.kt` before claiming append position, registration presence, or list order. +3. Resolve display names from `strings.xml` for any `R.string.example_*` entry referenced by `Examples.kt`. +4. If the question is API-to-case mapping and `ARCHITECTURE.md` is insufficient, search across `samples/**` and use direct RTC API calls in the Composable source as the primary signal. Return all relevant matching cases, or the best-scoped matches if the API is shared by many samples. +5. Return direct factual conclusions only. For list position questions, report the current list length and the next append position for the target list (`BasicExampleList` or `AdvanceExampleList`). If the target list is not explicit, infer it from the requested feature or call out that the answer depends on which list the case belongs in. + +## Verify + +- Re-check any append-position or registration answer against the current contents of `Examples.kt` before answering +- Confirm any reported case name still matches the `strings.xml` resource referenced by the registered `Example(...)` entry +- Confirm any API-to-case answer is backed by `ARCHITECTURE.md` or the actual Composable source under `samples/**` + +## Out of scope + +- Editing or registering cases +- Looking for `@Example` annotations +- Claiming `nav_graph.xml` registration exists for Compose cases + +## Never + +- Never use `item_` string keys for Compose examples +- Never claim list position is an `@Example` index diff --git a/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md new file mode 100644 index 000000000..f2e2498e3 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md @@ -0,0 +1,46 @@ +--- +name: review-case +description: > + Review an existing Compose 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. +--- + +## What this skill is for + +Use this skill after a Compose case has been created or modified in `APIExample-Compose/`. It checks lifecycle, state, registration, and build verification before the case is treated as review-ready. + +## Source of truth + +1. `APIExample-Compose/AGENTS.md` +2. `APIExample-Compose/ARCHITECTURE.md` +3. The target case source file +4. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt` +5. `APIExample-Compose/app/src/main/res/values/strings.xml` + +## Procedure + +1. Audit lifecycle, permission, state, and threading-sensitive UI rules in the target Composable source. +2. Check `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` for registration and documentation closure. +3. Run the minimum build verification command from the Compose project. +4. Report findings first, then verification results, then explicit unverified items if any required verification could not be completed. + +## Verify + +- Run `./gradlew assembleDebug` from `APIExample-Compose/` +- Confirm `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` are aligned +- Confirm engine lifecycle and cleanup are owned by `DisposableEffect(lifecycleOwner)`, and `rememberSaveable` / `remember` boundaries still match current project practice + +## Out of scope + +- Rewriting the case during review by default +- Treating snapshot-safe state mutation as permission to show Toast or Dialog off the main thread + +## Never + +- Never approve `DisposableEffect(Unit)` for case cleanup +- Never approve `RtcEngine` stored in `rememberSaveable` +- Never skip the build command diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..7224860c6 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,50 @@ +--- +name: upsert-case +description: > + Add a new Compose API example case or modify an existing one in the + APIExample-Compose Android demo — updates Composable source, Examples.kt + registration, string resources, and architecture docs. Use when: adding a new + Agora RTC API demo screen in Jetpack Compose, modifying an existing case's + implementation or registration, or porting an APIExample case to Compose while + preserving Compose-only lifecycle and state rules. +--- + +## What this skill is for + +Use this skill to add or update a case in `APIExample-Compose/`. It owns the full change closure: Composable source, `Examples.kt` registration, localized `strings.xml` updates, and `ARCHITECTURE.md`. + +## Source of truth + +1. `APIExample-Compose/AGENTS.md` +2. `APIExample-Compose/ARCHITECTURE.md` +3. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt` +4. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/**` +5. `APIExample-Compose/app/src/main/res/values/strings.xml` +6. `APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt` + +## Procedure + +1. Run `query-cases` first when the target list placement or the closest nearby examples are unclear. +2. Create or update the Composable source, `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` together as one change set. +3. When the case title is user-facing, update both `res/values/strings.xml` and `res/values-zh/strings.xml` to keep locale coverage aligned with current project practice. +4. Use `APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt` for Compose state, lifecycle, permission, and registration patterns. +5. Keep the public stateful Composable, the private preview/view split, and the `Examples.kt` plus string registration alignment consistent. +6. Treat this skill as the current source of truth for case-creation closure even if older sections in `ARCHITECTURE.md` still describe a smaller file set; update `ARCHITECTURE.md` as part of the same change. + +## Verify + +- Run `./gradlew assembleDebug` from `APIExample-Compose/` +- Confirm the edited case updates `Examples.kt`, `strings.xml`, `values-zh/strings.xml` when needed, and `ARCHITECTURE.md` +- Confirm the state model matches current project practice: `rememberSaveable` for UI/session state shown in the canonical Compose samples, and `remember` for `RtcEngine` and other non-serializable objects +- Confirm the public Composable stays stateful and the preview stays on the private view function + +## Out of scope + +- Adding XML, Fragment, or ViewBinding code +- Skipping docs or registration because the Composable compiles + +## Never + +- Never use `rememberSaveable` for `RtcEngine` +- Never use `DisposableEffect(Unit)` for teardown +- Never hardcode `mAreaCode` diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt new file mode 100644 index 000000000..613c9c251 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt @@ -0,0 +1,112 @@ +package io.agora.api.example.compose.samples + +import android.Manifest +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.tooling.preview.Preview +import io.agora.api.example.compose.BuildConfig +import io.agora.api.example.compose.data.SettingPreferences +import io.agora.api.example.compose.utils.TokenUtils +import io.agora.rtc2.ChannelMediaOptions +import io.agora.rtc2.IRtcEngineEventHandler +import io.agora.rtc2.RtcEngine +import io.agora.rtc2.RtcEngineConfig +import io.agora.rtc2.RtcStats + +@Composable +fun ExampleCase() { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var channelName by rememberSaveable { mutableStateOf("") } + var isJoined by rememberSaveable { mutableStateOf(false) } + var localUid by rememberSaveable { mutableIntStateOf(0) } + + val rtcEngine = remember { + RtcEngine.create(RtcEngineConfig().apply { + mAreaCode = SettingPreferences.getArea() + mContext = context + mAppId = BuildConfig.AGORA_APP_ID + mEventHandler = object : IRtcEngineEventHandler() { + override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { + super.onJoinChannelSuccess(channel, uid, elapsed) + isJoined = true + localUid = uid + } + + override fun onLeaveChannel(stats: RtcStats?) { + super.onLeaveChannel(stats) + isJoined = false + localUid = 0 + } + + // Add case-specific callbacks here for remote users, stats, or custom media flows. + } + }) + } + + DisposableEffect(lifecycleOwner) { + onDispose { + if (isJoined) { + rtcEngine.leaveChannel() + } + RtcEngine.destroy() + } + } + + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { grantedMap -> + if (grantedMap.values.all { it }) { + TokenUtils.gen(channelName, 0) { token -> + rtcEngine.joinChannel(token, channelName, 0, ChannelMediaOptions()) + } + } + } + val requiredPermissions = arrayOf( + Manifest.permission.RECORD_AUDIO, + // Add Manifest.permission.CAMERA for video-capable cases. + ) + + ExampleCaseView( + channelName = channelName, + isJoined = isJoined, + onJoinClick = { newChannelName -> + channelName = newChannelName + permissionLauncher.launch(requiredPermissions) + }, + onLeaveClick = { + rtcEngine.leaveChannel() + } + ) +} + +@Preview +@Composable +private fun ExampleCasePreview() { + ExampleCaseView( + channelName = "Channel Name", + isJoined = false, + onJoinClick = {}, + onLeaveClick = {} + ) +} + +@Composable +private fun ExampleCaseView( + channelName: String, + isJoined: Boolean, + onJoinClick: (String) -> Unit, + onLeaveClick: () -> Unit +) { + // Replace with case-specific UI while keeping preview on this private view function. +} diff --git a/Android/APIExample-Compose/AGENTS.md b/Android/APIExample-Compose/AGENTS.md index 3e63c9283..39b131843 100644 --- a/Android/APIExample-Compose/AGENTS.md +++ b/Android/APIExample-Compose/AGENTS.md @@ -29,9 +29,9 @@ See [README.md — Obtain an App Id](README.md#obtain-an-app-id). | 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 | +| upsert-case | `.agents/skills/upsert-case/` | Add a new Compose case or modify an existing one | +| query-cases | `.agents/skills/query-cases/` | Query and browse existing Compose cases | +| review-case | `.agents/skills/review-case/` | Review a case against project red lines | ## Further Reading diff --git a/Android/APIExample-Compose/ARCHITECTURE.md b/Android/APIExample-Compose/ARCHITECTURE.md index 3b70c3991..bf757f933 100644 --- a/Android/APIExample-Compose/ARCHITECTURE.md +++ b/Android/APIExample-Compose/ARCHITECTURE.md @@ -7,16 +7,16 @@ 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) +├── .agents/ +│ └── skills/ +│ ├── upsert-case/ +│ │ ├── SKILL.md # Add or update a Compose case +│ │ └── references/ +│ │ └── composable-template.kt # Compose lifecycle/state reference skeleton +│ ├── query-cases/ +│ │ └── SKILL.md # Query existing cases by API, group, or list position +│ └── review-case/ +│ └── SKILL.md # Review a Compose case against project red lines └── app/src/main/ ├── AndroidManifest.xml ├── assets/ # Audio/video sample files @@ -127,7 +127,7 @@ APIExample-Compose/ Registration is **manual** — no reflection, no annotation scanning. -**To add a case, edit exactly two files:** +**To add a case, update at least four project-local artifacts:** **1. `model/Examples.kt`** — append to `BasicExampleList` or `AdvanceExampleList`: ```kotlin @@ -143,8 +143,14 @@ val AdvanceExampleList = listOf( 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. +**3. `res/values/strings.xml`** — add the user-facing example title. + +**4. `ARCHITECTURE.md`** — update the case index so discovery tooling stays current. + +Update `res/values-zh/strings.xml` too when the case title should remain localized alongside the existing examples. + +No `nav_graph.xml`, no `@Example` annotation, and no action ID. `NavGraph.kt` routes to cases by their +position in the list — the order in `Examples.kt` is the display order inside the target list. ## Composable Case Pattern diff --git a/Android/APIExample/.agent/skills/query-cases/SKILL.md b/Android/APIExample/.agent/skills/query-cases/SKILL.md deleted file mode 100644 index ee1944957..000000000 --- a/Android/APIExample/.agent/skills/query-cases/SKILL.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -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 deleted file mode 100644 index 6caabc6ce..000000000 --- a/Android/APIExample/.agent/skills/review-case/SKILL.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -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 deleted file mode 100644 index 25211787d..000000000 --- a/Android/APIExample/.agent/skills/upsert-case/SKILL.md +++ /dev/null @@ -1,342 +0,0 @@ ---- -name: upsert-case -description: > - Add a new API example case or modify an existing one in the APIExample Android demo — - creates or updates Fragment class, XML layout, string resources, and nav_graph registration. - Use when: adding a new Agora RTC API demo screen, modifying an existing case's implementation - or registration, implementing a new feature example in Java + XML layouts, registering a new - case via @Example annotation, subclassing BaseFragment for a new demo screen, or updating - an existing case's strings, layout, or nav entry. Keywords: add case, modify case, update case, - new fragment, nav_graph, @Example, BaseFragment, APIExample, new screen, demo case, RTC API example. ---- - -# Upsert Case — APIExample - -## Adding a New Case - -Touch exactly 4 files (all paths relative to `app/src/main/`): - -| File | What to add | -|---|---| -| `java/.../examples/{basic\|advanced\|audio}/YourCaseName.java` | Fragment class | -| `res/layout/fragment_your_case_name.xml` | XML layout | -| `res/values/strings.xml` | 2 strings | -| `res/navigation/nav_graph.xml` | 1 action + 1 destination | - -Registration is automatic via reflection — no other files needed. - ---- - -### Step 1: Clarify before coding - -Before writing a single line, ask: -- **What API am I demonstrating?** — determines which existing case is the closest reference to copy patterns from -- **Video or audio-only?** — determines permissions (`CAMERA` + `RECORD_AUDIO` vs `RECORD_AUDIO` only), layout complexity, and whether `VideoReportLayout` is needed -- **BASIC or ADVANCED group?** — BASIC for fundamental channel join/leave patterns; ADVANCED for feature-specific APIs -- **What's the sort index?** — index must be unique within the group. BASIC uses 0–9, ADVANCED starts from 10. Run `query-cases` skill first; a collision causes silent ordering bugs at runtime - ---- - -### Step 2: Create the Fragment - -**MANDATORY — READ ENTIRE FILE before writing any code**: -[`references/fragment-template.java`](references/fragment-template.java) - -Do NOT skip — the `setParameters`, `handler.post`, and `getPrivateCloudConfig()` null-check patterns are only fully shown there and are required in every case. - -**Do NOT load** any other reference files for this task. - -Non-obvious points the template highlights: - -- `setParameters(...)` for app scenario reporting — **required in every case**, do not remove -- `handler.post(RtcEngine::destroy)` — NOT `RtcEngine.destroy()` directly; direct call blocks UI thread (ANR) -- `getPrivateCloudConfig()` null-check before `setLocalAccessPoint()` — returns null on non-private-cloud builds (NPE) -- All `IRtcEngineEventHandler` callbacks run on a **background thread** — always `runOnUIThread()` for UI -- `onActivityCreated` → create engine; `onDestroy` → `leaveChannel()` then `handler.post(RtcEngine::destroy)` - -For video cases, add `VideoReportLayout` fields and wire `setupRemoteVideo` in `onUserJoined`/`onUserOffline`. - ---- - -### Step 3: Create the XML layout - -Minimum structure — channel input + join button at bottom: - -```xml - - - - - - - - - - - - -``` - -For video cases, use `VideoReportLayout` for each video slot. Pick one of the four standard layouts below — they cover the vast majority of cases. - -**General rules (apply to all layouts):** -- Video containers must sit **above** the bottom control bar. In `RelativeLayout` use `android:layout_above="@id/ll_join"`; in `ConstraintLayout` use `app:layout_constraintBottom_toTopOf="@id/ll_join"`. -- Each `VideoReportLayout` needs a unique `android:id` (`fl_local`, `fl_remote`, `fl_remote2`, …). - ---- - -**Layout A — Single broadcaster (local fullscreen)** -Use when: broadcaster-only demo, no remote video needed. - -```xml - - -``` - ---- - -**Layout B — 1v1 (local left, remote right, side by side)** -Use when: two-party call, equal-weight split. - -```xml - - - - - - - -``` - ---- - -**Layout C — Audience co-hosting (remote fullscreen background + local PiP top-right)** -Use when: live streaming where audience co-hosts; remote/host fills screen, local is a small overlay. - -```xml - - - - -``` - ---- - -**Layout D — 2×2 grid (up to 4 participants)** -Use when: multi-party call with up to 4 streams. - -```xml - - - - - - - - - - - - - -``` - ---- - -### Step 4: Add nav entries - -File: `res/navigation/nav_graph.xml` - -**Action** — inside `` (NOT mainFragment — mainFragment only has one action, to Ready): - -```xml - -``` - -**Destination** — at root `` level: - -```xml - -``` - -`action android:id` must exactly match `actionId` in `@Example`. - ---- - -### Step 5: Update ARCHITECTURE.md - -Add one line to the case list in `ARCHITECTURE.md` under the correct directory section (`basic/`, `advanced/`, or `audio/`): - -``` -├── YourCaseName.java # [index] "Display Name" — key API description -``` - -Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans. - ---- - - -## Modifying an Existing Case - -When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating: - -| What changed | Files to touch | -|---|---| -| Implementation logic (API calls, event handling) | `java/.../examples/{basic\|advanced\|audio}/CaseName.java` | -| UI layout (views, controls, video containers) | `res/layout/fragment_case_name.xml` | -| Display name or tips text | `res/values/strings.xml` | -| Sort index or group (BASIC ↔ ADVANCED) | `@Example` annotation in the Fragment class | -| Navigation label | `res/navigation/nav_graph.xml` (fragment label attribute) | -| Class rename or package move | Fragment class, `nav_graph.xml` (android:name + destination id), `@Example` annotation (actionId), layout file name, `ARCHITECTURE.md` | - -After making changes: - -1. **Verify `@Example` annotation consistency** — ensure `index`, `group`, `name`, `actionId`, and `tipsId` still match the actual string resources, nav action ID, and intended group/position. A mismatch causes the case to silently disappear from the list or navigate to the wrong screen. -2. **Update `res/values/strings.xml`** if the display name or tips text changed. -3. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed. -4. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description. - ---- - -## Verify - -```bash -./gradlew assembleDebug -``` - -- [ ] Case appears in correct group at expected sort position -- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment) -- [ ] `onJoinChannelSuccess` fires in Logcat -- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy` -- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description -- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries - ---- - -## When to Use a Spec Instead - -If the case meets any of the following criteria, create a Spec rather than using this skill directly: - -1. Involves coordinated calls across two or more Agora API modules -2. Requires a custom UI layout (not one of the standard Layout A/B/C/D templates above) -3. Involves multi-channel or multi-engine instance management -4. Requires a foreground Service or background thread coordination -5. Involves developing new shared components (widgets/utils, etc.) -6. Requires optional module integration (simpleFilter/streamEncrypt) - -If none apply → use this skill directly; no Spec needed. - -### Spec Requirements Document Must Include - -- List of APIs the case demonstrates -- User interaction flow description -- Expected RtcEngine lifecycle behavior -- Required permissions list - -### Spec Design Document Must Include - -- Target project identifier: `APIExample` -- Class/file structure design -- API call sequence (Mermaid sequence diagram recommended) -- State management approach -- UI layout plan -- Integration points with existing shared components -- Case registration info: class name, display name, group (BASIC/ADVANCED), sort index — finalize during design to avoid conflicts -- Generate `@Example` annotation parameters, `nav_graph.xml` action + destination, `strings.xml` key names (`item_` prefix) -- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing indices -- Risk identification and mitigation (API compatibility, performance, permissions, thread safety) - -### Spec Task List Integration - -- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters -- Mark which sub-tasks require manual coding, and provide target file paths and change summaries -- Tasks for creating new shared components must come before case implementation tasks - ---- - -## NEVER - -- **NEVER** put the nav action inside `` — it belongs in ``. mainFragment only routes to Ready; all case actions live in Ready. Wrong placement causes silent navigation failure at runtime. -- **NEVER** call `RtcEngine.destroy()` directly on the main thread — always `handler.post(RtcEngine::destroy)`. Direct call blocks the UI thread and causes ANR. -- **NEVER** call `setLocalAccessPoint()` without null-checking `getPrivateCloudConfig()` first — it returns null on standard builds, causing NPE. -- **NEVER** update UI directly inside `IRtcEngineEventHandler` callbacks — they run on a background thread. Always wrap with `runOnUIThread()`. -- **NEVER** omit `setParameters(...)` — it's required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally. diff --git a/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java b/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java deleted file mode 100644 index a96649654..000000000 --- a/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java +++ /dev/null @@ -1,207 +0,0 @@ -package io.agora.api.example.examples.advanced; - -import static io.agora.api.example.common.model.Examples.ADVANCED; -import static io.agora.rtc2.Constants.RENDER_MODE_HIDDEN; - -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Map; -import java.util.Random; -import java.util.concurrent.ConcurrentHashMap; - -import io.agora.api.example.MainApplication; -import io.agora.api.example.R; -import io.agora.api.example.annotation.Example; -import io.agora.api.example.common.BaseFragment; -import io.agora.api.example.common.widget.VideoReportLayout; -import io.agora.api.example.utils.PermissonUtils; -import io.agora.api.example.utils.TokenUtils; -import io.agora.rtc2.ChannelMediaOptions; -import io.agora.rtc2.Constants; -import io.agora.rtc2.IRtcEngineEventHandler; -import io.agora.rtc2.RtcEngine; -import io.agora.rtc2.RtcEngineConfig; -import io.agora.rtc2.proxy.LocalAccessPointConfiguration; -import io.agora.rtc2.video.VideoCanvas; - -/** - * This demo demonstrates how to use [describe the feature here]. - * - * Key APIs used: - * - RtcEngine.yourApi() - */ -@Example( - index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+ - group = ADVANCED, // BASIC or ADVANCED - name = R.string.item_your_case_name, - actionId = R.id.action_mainFragment_to_yourCaseName, - tipsId = R.string.your_case_name_tips -) -public class YourCaseName extends BaseFragment implements View.OnClickListener { - private static final String TAG = YourCaseName.class.getSimpleName(); - - // For video cases: add VideoReportLayout fields here - // private VideoReportLayout fl_local, fl_remote; - // private Map remoteViews = new ConcurrentHashMap<>(); - - private Button join; - private EditText et_channel; - private RtcEngine engine; - private int myUid; - private boolean joined = false; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_your_case_name, container, false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - join = view.findViewById(R.id.btn_join); - et_channel = view.findViewById(R.id.et_channel); - join.setOnClickListener(this); - // bind additional views here - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Context context = getContext(); - if (context == null) return; - try { - RtcEngineConfig config = new RtcEngineConfig(); - config.mContext = context.getApplicationContext(); - config.mAppId = getAgoraAppId(); - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); - config.mAreaCode = ((MainApplication) getActivity().getApplication()) - .getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - // REQUIRED in every case — do not remove - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - // null-check is mandatory — returns null on non-private-cloud builds - LocalAccessPointConfiguration localAccessPointConfiguration = - ((MainApplication) getActivity().getApplication()) - .getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - } catch (Exception e) { - e.printStackTrace(); - getActivity().onBackPressed(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (engine != null) { - engine.leaveChannel(); - } - // MUST use handler.post — do NOT call RtcEngine.destroy() directly on main thread - handler.post(RtcEngine::destroy); - engine = null; - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.btn_join) { - if (!joined) { - String channelId = et_channel.getText().toString(); - checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() { - @Override - public void onPermissionsResult(boolean allPermissionsGranted, - String[] permissions, int[] grantResults) { - if (allPermissionsGranted) { - joinChannel(channelId); - } - } - }); - } else { - joined = false; - engine.leaveChannel(); - join.setText(getString(R.string.join)); - } - } - } - - private void joinChannel(String channelId) { - engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); - // --- feature-specific setup goes here --- - // e.g. engine.enableVideo(); engine.setVideoEncoderConfiguration(...); - - ChannelMediaOptions options = new ChannelMediaOptions(); - options.autoSubscribeAudio = true; - options.autoSubscribeVideo = true; - options.publishMicrophoneTrack = true; - options.publishCameraTrack = true; // remove for audio-only cases - - int uid = new Random().nextInt(1000) + 100000; - TokenUtils.gen(requireContext(), channelId, uid, token -> { - int res = engine.joinChannel(token, channelId, uid, options); - if (res != 0) { - showAlert(RtcEngine.getErrorDescription(Math.abs(res))); - return; - } - join.setEnabled(false); - }); - } - - private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - myUid = uid; - joined = true; - // ALL UI updates must go through runOnUIThread — callbacks run on background thread - runOnUIThread(() -> { - join.setEnabled(true); - join.setText(getString(R.string.leave)); - }); - } - - @Override - public void onUserJoined(int uid, int elapsed) { - Log.i(TAG, "onUserJoined -> " + uid); - runOnUIThread(() -> { - // For video cases: create SurfaceView, call engine.setupRemoteVideo(...) - }); - } - - @Override - public void onUserOffline(int uid, int reason) { - Log.i(TAG, String.format("user %d offline, reason %d", uid, reason)); - runOnUIThread(() -> { - // For video cases: removeAllViews(), call engine.setupRemoteVideo(null, ...) - }); - } - - @Override - public void onError(int err) { - showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err)); - } - }; -} diff --git a/Android/APIExample/.agents/skills/query-cases/SKILL.md b/Android/APIExample/.agents/skills/query-cases/SKILL.md new file mode 100644 index 000000000..76682b221 --- /dev/null +++ b/Android/APIExample/.agents/skills/query-cases/SKILL.md @@ -0,0 +1,50 @@ +--- +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. +--- + +## What this skill is for + +Use this skill when the question is about what cases already exist in `APIExample/`, which case demonstrates a specific RTC API, whether a BASIC or ADVANCED slot is free, or whether a case is fully registered. This skill answers inventory and registration questions only; it does not edit files. + +## Source of truth + +1. `APIExample/ARCHITECTURE.md` +2. `APIExample/app/src/main/java/io/agora/api/example/examples/**` +3. `APIExample/app/src/main/res/navigation/nav_graph.xml` +4. `APIExample/app/src/main/res/values/strings.xml` + +## Procedure + +1. Read `ARCHITECTURE.md` first for the fast case index. +2. Re-scan live `@Example` registrations from source before claiming index availability or whether a case is fully registered. +3. For any registration answer, cross-check the active `@Example` entry against matching `actionId`, nav action, destination in `nav_graph.xml`, and a valid display-name string resource in `strings.xml`. +4. If `ARCHITECTURE.md` is stale or insufficient for API-to-case lookup, fall back to class Javadoc or direct API calls under `app/src/main/java/io/agora/api/example/examples/**`. +5. Resolve display names from `strings.xml`. +6. Return tables or direct factual conclusions only. + +## Verify + +- Re-check any free-slot or registration claim against live active `@Example` annotations before answering +- Confirm any registration answer is backed by matching `@Example`, nav action or destination, and display-name string resource +- Confirm any reported case name still matches the `strings.xml` resource used by the annotated entry + +## Out of scope + +- Editing or registering cases +- Reviewing lifecycle quality +- Approving implementation correctness + +## Never + +- Never report a free ADVANCED index without scanning both `advanced/` and `audio/` +- Never count a commented-out `//@Example` entry as an active registration +- Never rely on filename alone when a subdirectory case may hide the annotated entry point diff --git a/Android/APIExample/.agents/skills/review-case/SKILL.md b/Android/APIExample/.agents/skills/review-case/SKILL.md new file mode 100644 index 000000000..f851a1e74 --- /dev/null +++ b/Android/APIExample/.agents/skills/review-case/SKILL.md @@ -0,0 +1,45 @@ +--- +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. +--- + +## What this skill is for + +Use this skill after a case has been created or modified in `APIExample/`. It checks project red lines, registration closure, and minimum executable verification before the case is treated as review-ready. + +## Source of truth + +1. `APIExample/AGENTS.md` +2. `APIExample/ARCHITECTURE.md` +3. The target case source file +4. `APIExample/app/src/main/res/navigation/nav_graph.xml` +5. `APIExample/app/src/main/res/values/strings.xml` + +## Procedure + +1. Audit lifecycle, permission, threading, and registration rules. +2. Check `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md` for closure. +3. Run the minimum build verification command. +4. Report findings first, then verification results, then explicit unverified items. + +## Verify + +- Run `./gradlew assembleDebug` from `APIExample/` +- Confirm the target case still appears to satisfy `@Example` + action + destination + string alignment + +## Out of scope + +- Re-implementing the case from scratch +- Making silent code fixes during review +- Claiming runtime behavior was validated if only compile checks ran + +## Never + +- Never approve direct `RtcEngine.destroy()` on the main thread +- Never approve missing `leaveChannel()` before destroy +- Never skip the build command diff --git a/Android/APIExample/.agents/skills/upsert-case/SKILL.md b/Android/APIExample/.agents/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..255a6e65b --- /dev/null +++ b/Android/APIExample/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,51 @@ +--- +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, nav_graph registration, + and architecture docs. 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, nav + entry, or architecture docs. Keywords: add case, modify case, update case, new fragment, + nav_graph, @Example, BaseFragment, APIExample, new screen, demo case, RTC API example. +--- + +## What this skill is for + +Use this skill to add a new case or update an existing case in `APIExample/`. It owns the full change closure: case source, XML layout, strings, `nav_graph.xml`, and `ARCHITECTURE.md`. + +## Source of truth + +1. `APIExample/AGENTS.md` +2. `APIExample/ARCHITECTURE.md` +3. `APIExample/app/src/main/java/io/agora/api/example/examples/**` +4. `APIExample/app/src/main/res/navigation/nav_graph.xml` +5. `APIExample/app/src/main/res/values/strings.xml` +6. `APIExample/.agents/skills/upsert-case/references/fragment-template.java` + +## Procedure + +1. Run `query-cases` first when index or placement is unknown. +2. Create or update the Fragment, layout, strings, navigation entries, and `ARCHITECTURE.md`. +3. Use the reference template for lifecycle, reporting, and private-cloud guards. +4. Keep `@Example`, action ID, destination ID, and string resources aligned. +5. Treat the reference template as a pattern source, not a drop-in class. + +## Verify + +- Run `./gradlew assembleDebug` from `APIExample/` +- Re-open the edited file list and confirm the case closure touched every required file category + +## Out of scope + +- Auditing the final implementation as a reviewer +- Audio-only cases that belong in `APIExample-Audio/` +- Compose-based cases that belong in `APIExample-Compose/` + +## Never + +- Never omit `setParameters(...)` +- Never skip the `getPrivateCloudConfig()` null-check +- Never call `RtcEngine.destroy()` directly on the main thread +- Never stop at “it compiles” without updating registration and `ARCHITECTURE.md` diff --git a/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java new file mode 100644 index 000000000..2d029e12d --- /dev/null +++ b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java @@ -0,0 +1,46 @@ +// Reference skeleton only. +// This file demonstrates the required engine lifecycle, reporting, and private-cloud patterns. +// It is not a drop-in compiled class; each real case must supply its own imports, annotation, +// event handler, engine config fields, and UI logic. +public abstract class ExampleCaseTemplate extends BaseFragment { + // Supply case-specific handler/config fields in the real Fragment. + protected IRtcEngineEventHandler iRtcEngineEventHandler; + protected RtcEngine engine; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Context context = getContext(); + if (context == null) { + return; + } + try { + RtcEngineConfig config = new RtcEngineConfig(); + config.mContext = context.getApplicationContext(); + config.mAppId = getAgoraAppId(); + config.mEventHandler = iRtcEngineEventHandler; + // Add case-specific config here, for example channel profile or area code. + engine = RtcEngine.create(config); + engine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"}}"); + LocalAccessPointConfiguration privateCloud = + ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); + if (privateCloud != null) { + engine.setLocalAccessPoint(privateCloud); + } + } catch (Exception e) { + e.printStackTrace(); + // Match the seeded case rollback pattern when engine init fails. + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } +} diff --git a/Android/APIExample/AGENTS.md b/Android/APIExample/AGENTS.md index b5c9e8bd7..e142a46a1 100644 --- a/Android/APIExample/AGENTS.md +++ b/Android/APIExample/AGENTS.md @@ -40,9 +40,9 @@ Both are `false` by default. Do not enable unless the feature explicitly require | 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 | +| upsert-case | `.agents/skills/upsert-case/` | Add a new case or modify an existing one | +| query-cases | `.agents/skills/query-cases/` | Query and browse existing cases | +| review-case | `.agents/skills/review-case/` | Review a case against project red lines | ## Further Reading From e68da2850a2e6fecb417f1d12d462053ac8839a2 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 26 May 2026 14:02:57 +0800 Subject: [PATCH 03/49] Update LiveStreaming watermark API --- .../api/example/examples/advanced/LiveStreaming.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 e8de67458..faa526ca5 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 @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.Locale; import java.util.Random; +import java.util.UUID; import io.agora.api.example.MainApplication; import io.agora.api.example.R; @@ -58,6 +59,7 @@ import io.agora.rtc2.video.SnapshotConfig; import io.agora.rtc2.video.VideoCanvas; import io.agora.rtc2.video.VideoEncoderConfiguration; +import io.agora.rtc2.video.WatermarkConfig; import io.agora.rtc2.video.WatermarkOptions; /** @@ -591,7 +593,13 @@ private void enableWatermark(boolean enable) { watermarkOptions.positionInPortraitMode = new WatermarkOptions.Rectangle(10, height / 2, size, size); watermarkOptions.positionInLandscapeMode = new WatermarkOptions.Rectangle(10, height / 2, size, size); watermarkOptions.visibleInPreview = true; - int ret = engine.addVideoWatermark(Constant.WATER_MARK_FILE_PATH, watermarkOptions); + + WatermarkConfig watermarkConfig = new WatermarkConfig(); + watermarkConfig.id = UUID.randomUUID().toString().replace("-", ""); + watermarkConfig.type = WatermarkConfig.WATERMARK_TYPE_IMAGE; + watermarkConfig.imageUrl = Constant.WATER_MARK_FILE_PATH; + watermarkConfig.options = watermarkOptions; + int ret = engine.addVideoWatermark(watermarkConfig); if (ret != Constants.ERR_OK) { Log.e(TAG, "addVideoWatermark error=" + ret + ", msg=" + RtcEngine.getErrorDescription(ret)); } From 2bc250311440f1009f513f4e01f920910b194cac Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 26 May 2026 14:03:15 +0800 Subject: [PATCH 04/49] Add ScreenSharing permission callback --- .../api/example/examples/advanced/ScreenSharing.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 d385c5dc8..1bc7016d4 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 @@ -513,6 +513,18 @@ public void onLocalVideoEvent(Constants.VideoSourceType source, int event) { } } + @Override + public void onPermissionGranted(int permission) { + super.onPermissionGranted(permission); + if (permission == Constants.RECORD_AUDIO) { + showLongToast("Recording permission granted"); + } else if (permission == Constants.CAMERA) { + showLongToast("Camera permission granted"); + } else if (permission == Constants.SCREEN_CAPTURE) { + showLongToast("Screen capture permission granted"); + } + } + /**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 From d5868bc70dc135b566d1a26540e0f88dbb9005db Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 26 May 2026 14:41:39 +0800 Subject: [PATCH 05/49] Update audio scenario options --- .../examples/advanced/PlayAudioFiles.java | 15 +++++++++++++-- .../example/examples/advanced/VoiceEffects.java | 17 ++++++++++++++--- .../examples/basic/JoinChannelAudio.java | 15 +++++++++++++-- .../app/src/main/res/values/arrays.xml | 9 ++++++--- 4 files changed, 46 insertions(+), 10 deletions(-) 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 7cf455f82..e4e84a7cd 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 @@ -231,7 +231,7 @@ public void onDestroy() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (parent == audioScenario) { - engine.setAudioScenario(Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString()))); + engine.setAudioScenario(getAudioScenarioValue(audioScenario.getSelectedItem().toString())); } } @@ -330,7 +330,7 @@ private void joinChannel(String channelId) { engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); engine.setAudioProfile( Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString())), - Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) + getAudioScenarioValue(audioScenario.getSelectedItem().toString()) ); /* @@ -359,6 +359,17 @@ private void joinChannel(String channelId) { }); } + private int getAudioScenarioValue(String label) { + return switch (label) { + case "AUDIO_SCENARIO_GAME_STREAMING" -> Constants.AUDIO_SCENARIO_GAME_STREAMING; + case "AUDIO_SCENARIO_CHATROOM" -> Constants.AUDIO_SCENARIO_CHATROOM; + case "AUDIO_SCENARIO_CHORUS" -> Constants.AUDIO_SCENARIO_CHORUS; + case "AUDIO_SCENARIO_MEETING" -> Constants.AUDIO_SCENARIO_MEETING; + case "AUDIO_SCENARIO_AI_CLIENT" -> Constants.AUDIO_SCENARIO_AI_CLIENT; + default -> Constants.AUDIO_SCENARIO_DEFAULT; + }; + } + /** * IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events. 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 bb68f7dff..408a8f3ad 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 @@ -394,10 +394,10 @@ private void joinChannel(String channelId) { // audio config engine.setAudioProfile( Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString())), - Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) + getAudioScenarioValue(audioScenario.getSelectedItem().toString()) ); - /* + /* * 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 @@ -575,7 +575,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long } if (parent == audioScenario) { - engine.setAudioScenario(Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString()))); + engine.setAudioScenario(getAudioScenarioValue(audioScenario.getSelectedItem().toString())); return; } @@ -652,6 +652,17 @@ public void onItemSelected(AdapterView parent, View view, int position, long } } + private int getAudioScenarioValue(String label) { + return switch (label) { + case "AUDIO_SCENARIO_GAME_STREAMING" -> Constants.AUDIO_SCENARIO_GAME_STREAMING; + case "AUDIO_SCENARIO_CHATROOM" -> Constants.AUDIO_SCENARIO_CHATROOM; + case "AUDIO_SCENARIO_CHORUS" -> Constants.AUDIO_SCENARIO_CHORUS; + case "AUDIO_SCENARIO_MEETING" -> Constants.AUDIO_SCENARIO_MEETING; + case "AUDIO_SCENARIO_AI_CLIENT" -> Constants.AUDIO_SCENARIO_AI_CLIENT; + default -> Constants.AUDIO_SCENARIO_DEFAULT; + }; + } + private int getVoiceConversionValue(String label) { switch (label) { case "VOICE_CHANGER_NEUTRAL": 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 f83c52537..49d067657 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 @@ -155,7 +155,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (joined) { - int scenario = Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString())); + int scenario = getAudioScenarioValue(audioScenarioInput.getSelectedItem().toString()); engine.setAudioScenario(scenario); } } @@ -545,7 +545,7 @@ private void joinChannel(String channelId) { int audioProfile = Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfileInput.getSelectedItem().toString())); engine.setAudioProfile(audioProfile); - int scenario = Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString())); + int scenario = getAudioScenarioValue(audioScenarioInput.getSelectedItem().toString()); engine.setAudioScenario(scenario); ChannelMediaOptions option = new ChannelMediaOptions(); @@ -577,6 +577,17 @@ private void joinChannel(String channelId) { } + private int getAudioScenarioValue(String label) { + return switch (label) { + case "AUDIO_SCENARIO_GAME_STREAMING" -> Constants.AUDIO_SCENARIO_GAME_STREAMING; + case "AUDIO_SCENARIO_CHATROOM" -> Constants.AUDIO_SCENARIO_CHATROOM; + case "AUDIO_SCENARIO_CHORUS" -> Constants.AUDIO_SCENARIO_CHORUS; + case "AUDIO_SCENARIO_MEETING" -> Constants.AUDIO_SCENARIO_MEETING; + case "AUDIO_SCENARIO_AI_CLIENT" -> Constants.AUDIO_SCENARIO_AI_CLIENT; + default -> Constants.AUDIO_SCENARIO_DEFAULT; + }; + } + private final IRtcEngineEventHandler secondHandler = new IRtcEngineEventHandler() { public static final String LABLE = "secondHandler"; diff --git a/Android/APIExample/app/src/main/res/values/arrays.xml b/Android/APIExample/app/src/main/res/values/arrays.xml index 94927aeaf..9bd4f53a2 100644 --- a/Android/APIExample/app/src/main/res/values/arrays.xml +++ b/Android/APIExample/app/src/main/res/values/arrays.xml @@ -16,9 +16,12 @@ Channel1 - DEFAULT - GAME_STREAMING - CHATROOM + AUDIO_SCENARIO_DEFAULT + AUDIO_SCENARIO_GAME_STREAMING + AUDIO_SCENARIO_CHATROOM + AUDIO_SCENARIO_CHORUS + AUDIO_SCENARIO_MEETING + AUDIO_SCENARIO_AI_CLIENT OFF From 25b2768afc41018afc772f45c4adf67c57be4725 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 26 May 2026 15:41:10 +0800 Subject: [PATCH 06/49] Expose audio route options --- .../examples/basic/JoinChannelAudio.java | 32 ++++++++++++++++--- .../app/src/main/res/values/arrays.xml | 8 +++-- .../app/src/main/res/values/strings.xml | 8 +++-- 3 files changed, 38 insertions(+), 10 deletions(-) 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 49d067657..abf3f734e 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 @@ -173,15 +173,23 @@ public void onItemSelected(AdapterView parent, View view, int position, long } boolean isCommunication = getString(R.string.channel_profile_communication).equals(channelProfileInput.getSelectedItem()); if (isCommunication) { - int route = Constants.AUDIO_ROUTE_EARPIECE; - if (getString(R.string.audio_route_earpiece).equals(parent.getSelectedItem())) { + int route = Constants.AUDIO_ROUTE_DEFAULT; + if (getString(R.string.audio_route_default).equals(parent.getSelectedItem())) { + route = Constants.AUDIO_ROUTE_DEFAULT; + } else if (getString(R.string.audio_route_headset).equals(parent.getSelectedItem())) { + route = Constants.AUDIO_ROUTE_HEADSET; + } else if (getString(R.string.audio_route_earpiece).equals(parent.getSelectedItem())) { route = Constants.AUDIO_ROUTE_EARPIECE; + } else if (getString(R.string.audio_route_headset_no_mic).equals(parent.getSelectedItem())) { + route = Constants.AUDIO_ROUTE_HEADSETNOMIC; } else if (getString(R.string.audio_route_speakerphone).equals(parent.getSelectedItem())) { route = Constants.AUDIO_ROUTE_SPEAKERPHONE; - } else if (getString(R.string.audio_route_headset).equals(parent.getSelectedItem())) { - route = Constants.AUDIO_ROUTE_HEADSET; + } else if (getString(R.string.audio_route_loudspeaker).equals(parent.getSelectedItem())) { + route = Constants.AUDIO_ROUTE_LOUDSPEAKER; } else if (getString(R.string.audio_route_headset_bluetooth).equals(parent.getSelectedItem())) { route = Constants.AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP; + } else if (getString(R.string.audio_route_headset_typec).equals(parent.getSelectedItem())) { + route = Constants.AUDIO_ROUTE_USBDEVICE; } int ret = engine.setRouteInCommunicationMode(route); showShortToast("setRouteInCommunicationMode route=" + route + ", ret=" + ret); @@ -673,10 +681,24 @@ public void onRemoteAudioStats(RemoteAudioStats stats) { @Override public void onAudioRouteChanged(int routing) { super.onAudioRouteChanged(routing); - Log.d(TAG, "secondHandler onAudioRouteChanged : " + routing); + Log.d(TAG, "secondHandler onAudioRouteChanged : " + getAudioRouteKey(routing)); } }; + private String getAudioRouteKey(int routing) { + return switch (routing) { + case Constants.AUDIO_ROUTE_DEFAULT -> "AUDIO_ROUTE_DEFAULT"; + case Constants.AUDIO_ROUTE_HEADSET -> "AUDIO_ROUTE_HEADSET"; + case Constants.AUDIO_ROUTE_EARPIECE -> "AUDIO_ROUTE_EARPIECE"; + case Constants.AUDIO_ROUTE_HEADSETNOMIC -> "AUDIO_ROUTE_HEADSETNOMIC"; + case Constants.AUDIO_ROUTE_SPEAKERPHONE -> "AUDIO_ROUTE_SPEAKERPHONE"; + case Constants.AUDIO_ROUTE_LOUDSPEAKER -> "AUDIO_ROUTE_LOUDSPEAKER"; + case Constants.AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP -> "AUDIO_ROUTE_BLUETOOTH_DEVICE_HFP"; + case Constants.AUDIO_ROUTE_BLUETOOTH_DEVICE_A2DP -> "AUDIO_ROUTE_BLUETOOTH_DEVICE_A2DP"; + default -> String.valueOf(routing); + }; + } + /** * IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events. diff --git a/Android/APIExample/app/src/main/res/values/arrays.xml b/Android/APIExample/app/src/main/res/values/arrays.xml index 9bd4f53a2..f11813c38 100644 --- a/Android/APIExample/app/src/main/res/values/arrays.xml +++ b/Android/APIExample/app/src/main/res/values/arrays.xml @@ -261,10 +261,14 @@ @string/channel_profile_communication_1v1 - @string/audio_route_speakerphone - @string/audio_route_earpiece + @string/audio_route_default @string/audio_route_headset + @string/audio_route_earpiece + @string/audio_route_headset_no_mic + @string/audio_route_speakerphone + @string/audio_route_loudspeaker @string/audio_route_headset_bluetooth + @string/audio_route_headset_typec diff --git a/Android/APIExample/app/src/main/res/values/strings.xml b/Android/APIExample/app/src/main/res/values/strings.xml index ad756046a..f7a7fd3db 100644 --- a/Android/APIExample/app/src/main/res/values/strings.xml +++ b/Android/APIExample/app/src/main/res/values/strings.xml @@ -403,11 +403,13 @@ CLOUD_GAMING COMMUNICATION_1v1 default - speakerphone - earpiece headset - headset(TypeC) + earpiece + headset(no mic) + speakerphone + external speaker(unsupported) bluetooth headset + USB device(Type-C) Third Party Player IjkPlayer ExoPlayer From df7d225f4d5d0ad9dcae9f06c93e1d781006aa70 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 26 May 2026 16:16:29 +0800 Subject: [PATCH 07/49] Expose video module position option --- Android/APIExample/app/src/main/res/values/arrays.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/Android/APIExample/app/src/main/res/values/arrays.xml b/Android/APIExample/app/src/main/res/values/arrays.xml index f11813c38..9810f60b1 100644 --- a/Android/APIExample/app/src/main/res/values/arrays.xml +++ b/Android/APIExample/app/src/main/res/values/arrays.xml @@ -344,6 +344,7 @@ APPLICATION_SCENARIO_1V1 + VIDEO_MODULE_POSITION_POST_CAPTURER VIDEO_MODULE_POSITION_PRE_RENDERER VIDEO_MODULE_POSITION_PRE_ENCODER VIDEO_MODULE_POSITION_POST_CAPTURER_ORIGIN From c6616b7ac8cbb38698b66c363c9a5747b46684f7 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 27 May 2026 12:01:49 +0800 Subject: [PATCH 08/49] Handle token state changes in JoinChannelVideoByToken --- .../basic/JoinChannelVideoByToken.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java index 9d8507b1e..9763e53d2 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java @@ -238,15 +238,25 @@ private void joinChannel(String channelId, String token) { @Override public void onError(int err) { super.onError(err); - showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err)); - if (err == Constants.ERR_INVALID_TOKEN || err == Constants.ERR_TOKEN_EXPIRED) { +// showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err)); + } + + /** + * Occurs when the network connection state changes. + * @param state The current connection state. + * @param reason Reason for connection status change + */ + @Override + public void onConnectionStateChanged(int state, int reason) { + super.onConnectionStateChanged(state, reason); + if (reason == Constants.CONNECTION_CHANGED_INVALID_TOKEN || reason == Constants.CONNECTION_CHANGED_TOKEN_EXPIRED) { engine.leaveChannel(); runOnUIThread(() -> join.setEnabled(true)); - if (Constants.ERR_INVALID_TOKEN == err) { + if (Constants.CONNECTION_CHANGED_INVALID_TOKEN == reason) { showAlert(getString(R.string.token_invalid)); } - if (Constants.ERR_TOKEN_EXPIRED == err) { + if (Constants.CONNECTION_CHANGED_TOKEN_EXPIRED == reason) { showAlert(getString(R.string.token_expired)); } } From 414e1a99540cc61cfdec474cff0a5631a9e53cad Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 27 May 2026 16:50:59 +0800 Subject: [PATCH 09/49] Hide deprecated RhythmPlayer from UI list --- .../io/agora/api/example/examples/advanced/RhythmPlayer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 194fdde17..0cd8ff5a0 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 @@ -36,13 +36,13 @@ /** * This demo demonstrates how to make a VideoProcessExtension */ -@Example( +/*@Example( index = 34, group = ADVANCED, name = R.string.item_rhythmplayer, actionId = R.id.action_mainFragment_rhythm_player, tipsId = R.string.rhythmplayer -) +)*/ public class RhythmPlayer extends BaseFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener { private static final String TAG = RhythmPlayer.class.getSimpleName(); From 6aa166176b011032eac92cc93b8ecb2d5e8536e8 Mon Sep 17 00:00:00 2001 From: alienzh Date: Tue, 28 Jul 2026 17:30:01 +0800 Subject: [PATCH 10/49] feat(ai-engineering): add multi-platform API Example iteration workflow (#475) * feat(ai-engineering): add multi-platform API Example workflow Add repository-local Contract, platform implementation, independent verification, and cross-platform acceptance tooling. Use an Agora repository profile for SDK version sources and keep coverage and release knowledge specific to the overseas repository. * refactor(ai-engineering): support per-platform SDK targets * ci(ios): add signing preflight and unsigned build validation - validate Jenkins signing profiles and certificate expiration - support legacy and modern distribution certificate names - switch Azure iOS jobs to unsigned compile checks --------- Co-authored-by: zhangwei --- .../api-example-release-iteration/SKILL.md | 124 ++ .github/ci/build/build_ios.sh | 4 + .github/ci/build/check_ios_signing_assets.py | 343 ++++ .../tests/test_check_ios_signing_assets.py | 205 +++ AGENTS.md | 11 + cicd/build-template/build-ios.yml | 20 +- cicd/scripts/ios_build.sh | 95 +- .../ai-engineering/case-maintenance-matrix.md | 112 ++ docs/ai-engineering/knowledge-index.md | 226 +++ docs/ai-engineering/pilot-runs/README.md | 27 + docs/ai-engineering/release-iteration-gate.md | 161 ++ docs/ai-engineering/release-known-issues.md | 20 + docs/ai-engineering/repository-profile.json | 59 + docs/ai-engineering/role-routing.json | 36 + docs/ai-engineering/templates/README.md | 16 + .../acceptance-manifest-template.json | 283 +++ .../templates/release-dry-run-template.md | 131 ++ docs/ai-engineering/tools/README.md | 106 ++ .../tools/generate_case_backlog.py | 222 +++ .../tools/generate_case_backlog_test.py | 124 ++ .../tools/orchestrate_case_execution.py | 1594 +++++++++++++++++ .../tools/orchestrate_case_execution_test.py | 1176 ++++++++++++ .../tools/prepare_case_execution.py | 457 +++++ .../tools/prepare_case_execution_test.py | 292 +++ .../tools/validate_acceptance_manifest.py | 1239 +++++++++++++ .../validate_acceptance_manifest_test.py | 727 ++++++++ 26 files changed, 7720 insertions(+), 90 deletions(-) create mode 100644 .agent/skills/api-example-release-iteration/SKILL.md create mode 100644 .github/ci/build/check_ios_signing_assets.py create mode 100644 .github/ci/build/tests/test_check_ios_signing_assets.py create mode 100644 docs/ai-engineering/case-maintenance-matrix.md create mode 100644 docs/ai-engineering/knowledge-index.md create mode 100644 docs/ai-engineering/pilot-runs/README.md create mode 100644 docs/ai-engineering/release-iteration-gate.md create mode 100644 docs/ai-engineering/release-known-issues.md create mode 100644 docs/ai-engineering/repository-profile.json create mode 100644 docs/ai-engineering/role-routing.json create mode 100644 docs/ai-engineering/templates/README.md create mode 100644 docs/ai-engineering/templates/acceptance-manifest-template.json create mode 100644 docs/ai-engineering/templates/release-dry-run-template.md create mode 100644 docs/ai-engineering/tools/README.md create mode 100644 docs/ai-engineering/tools/generate_case_backlog.py create mode 100644 docs/ai-engineering/tools/generate_case_backlog_test.py create mode 100644 docs/ai-engineering/tools/orchestrate_case_execution.py create mode 100644 docs/ai-engineering/tools/orchestrate_case_execution_test.py create mode 100644 docs/ai-engineering/tools/prepare_case_execution.py create mode 100644 docs/ai-engineering/tools/prepare_case_execution_test.py create mode 100644 docs/ai-engineering/tools/validate_acceptance_manifest.py create mode 100644 docs/ai-engineering/tools/validate_acceptance_manifest_test.py diff --git a/.agent/skills/api-example-release-iteration/SKILL.md b/.agent/skills/api-example-release-iteration/SKILL.md new file mode 100644 index 000000000..9213cbd36 --- /dev/null +++ b/.agent/skills/api-example-release-iteration/SKILL.md @@ -0,0 +1,124 @@ +--- +name: api-example-release-iteration +description: Use when turning an RTC API Examples product requirement into aligned Android, iOS, macOS, and Windows changes with attributed implementation and independent repository acceptance +--- + +# API Example Release Iteration + +Use this skill for one product requirement across all official platform families. The Lead coordinates a shared Contract, attributed platform delivery, independent platform Verification, and final cross-platform acceptance. + +## Required Inputs + +Identify before editing: + +- Product scenario and key RTC SDK APIs. +- New case, existing case update, docs-only, or SDK-version work. +- Reference case for behavior and parity. +- Verification budget and available platform hosts. +- Target RTC SDK version for each platform. Platforms may remain on different release lines. + +Android, iOS, macOS, and Windows are required by default. Contract may choose the appropriate full/audio/Compose/UIKit/Objective-C project for each platform; marking a platform not required needs an explicit waiver reason. + +## Source Route + +Read root `AGENTS.md`, the repository profile, this knowledge index and gate, then each platform/project `AGENTS.md`, `ARCHITECTURE.md`, and available `query-cases`, `upsert-case`, and `review-case` skills. Project skills may use either `.agent/skills/` or `.agents/skills/`. Read `release-known-issues.md` only for release, SDK version, CI, signing, packaging, license, or third-party asset work. + +## Agent Topology + +```text +Lead + Contract + Android Implementation -> Android Verification + iOS Implementation -> iOS Verification + macOS Implementation -> macOS Verification + Windows Implementation -> Windows Verification + Cross-platform acceptance +``` + +This is one Lead plus nine independent `codex exec` runs. They are replayable requirement-workflow runs, not parent-managed Codex subagent threads. Contract executes once. In a shared checkout, platform Implementation agents run one at a time so each repository delta belongs to one run; platform Verification agents run concurrently after their corresponding Implementation passes. + +| Role Type | Responsibility | Profile | +| --- | --- | --- | +| Contract | Shared behavior, APIs, non-goals, reference, platform targets and allowed files | `standard` | +| Implementation | Query and modify exactly one platform target | `deep` | +| Verification | Findings-first review, parity, UX, and strongest valid platform checks | `review` | + +Implementation cannot approve its own platform. Every accepted artifact needs a distinct `agent_id`, Codex run provenance, model/profile, and input snapshot hash. + +## Execution + +Initialize one requirement: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ + --matrix docs/ai-engineering/case-maintenance-matrix.md \ + --feature "" \ + --target-sdk-version "4.6.2" \ + --platform-sdk-version "android=4.6.3" \ + --run-dir /tmp/api-example-requirement +``` + +`--target-sdk-version` supplies the baseline for all four platforms. Repeat `--platform-sdk-version platform=x.y.z` only for platforms whose target differs. The execution package and manifest store the resolved four-platform mapping, so staggered platform releases never require script or repository-profile changes. + +The default `docs/ai-engineering/repository-profile.json` supplies repository-specific SDK package names and version sources. Use `--repository-profile` only for a checked-in alternative profile. The execution package records its repository-relative path and SHA-256. + +For a new requirement not yet actionable in the matrix, also pass `--sdk-family ""` and repeat `--key-api ""` as needed. + +Dispatch the shared Contract: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement \ + --phase contract \ + --model "" +``` + +Dispatch all platform agents by omitting `--platform`. Implementation runs are serialized in the shared checkout for delta attribution; Verification runs fan out concurrently: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement \ + --phase implementation \ + --model "" + +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement \ + --phase verification \ + --model "" +``` + +Use `--platform android|ios|macos|windows` for a single platform and `--retry` to replace a prior `FAIL` or `BLOCKED` artifact. Retrying Implementation invalidates that platform's old Verification and carries all attempts into one cumulative net repository delta. Each run has an independent 900-second default timeout, JSONL command log, host provenance, and target-project working directory so nested `AGENTS.md` files load automatically. + +Assemble only after reviewing all required platform artifacts: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py assemble \ + --run-dir /tmp/api-example-requirement \ + --matrix docs/ai-engineering/case-maintenance-matrix.md \ + --final-status "" \ + --cross-platform-result "" \ + --cross-platform-evidence "" +``` + +## Platform Rules + +- Contract selects the target project and allowed files for each platform. +- The dispatcher runs platform roles from that target project. In a shared checkout it refreshes the package/dependency snapshot, completes and reconciles one Implementation before starting the next, derives that role's cumulative net repository delta, and rejects changes outside Contract paths. +- Each Implementation agent follows only its platform/project instructions. +- Platform source files, dependencies, and build scripts are never shared across roots. +- Verification covers code review, reference parity, discoverability, UX, and valid checks on the current verification host. Executed PASS/FAIL commands must bind to the Codex JSONL command event and exit code, Contract working directory, and recorded host platform. +- Contract and Verification runs may create ignored build output but must not modify tracked or untracked repository content. +- A required platform `FAIL` or `BLOCKED` forces the whole requirement to `BLOCKED`. +- Final non-`BLOCKED` acceptance requires all required platform review, parity, and build results to pass, plus cross-platform acceptance `PASS`. + +## Host Mismatch + +On macOS, Windows Verification may perform repository-local static review only. It must not download Windows SDK archives, emulate Windows, cross-compile, or use a substitute compiler as build evidence. Keep Windows Verification and the requirement `BLOCKED` until the Verification role is retried on a real Windows host with MSBuild evidence. + +## Matrix And Release + +Each platform Implementation may propose zero or more matrix updates. `DONE` requires final `PASS`, platform parity/build `PASS`, and no skipped checks. Release data is limited to repository SDK-version consistency. Jenkins packaging, QA validation, artifact distribution, and external website publication are outside this workflow and must not be recorded as manifest gates. + +## Final Output + +Report the shared Contract, each platform's files and Verification result, declared platform differences, SDK-version checks, manifest validation, and final status. Do not describe the workflow as fully autonomous: the Lead still advances phases, handles retries, and owns final cross-platform judgment. diff --git a/.github/ci/build/build_ios.sh b/.github/ci/build/build_ios.sh index 96b54690d..234d96c30 100644 --- a/.github/ci/build/build_ios.sh +++ b/.github/ci/build/build_ios.sh @@ -152,6 +152,10 @@ if [ $compress_apiexample = true ]; then fi if [ $compile_project = true ]; then + if ! python3 ./.github/ci/build/check_ios_signing_assets.py \ + --export-options "./iOS/${ios_direction}/ExportOptions.plist"; then + echo "WARNING: iOS signing asset preflight could not complete; continuing packaging" + fi cd ./$unzip_name/samples/${ios_direction} ./cloud_build.sh || exit 1 cd - diff --git a/.github/ci/build/check_ios_signing_assets.py b/.github/ci/build/check_ios_signing_assets.py new file mode 100644 index 000000000..ede69a1e8 --- /dev/null +++ b/.github/ci/build/check_ios_signing_assets.py @@ -0,0 +1,343 @@ +#!/usr/bin/env python3 +"""Validate the iOS signing assets that Jenkins will use for IPA export.""" + +from __future__ import annotations + +import argparse +import hashlib +import math +import os +import plistlib +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime, timezone +from email.utils import parsedate_to_datetime +from pathlib import Path +from typing import Dict, Iterable, List, Optional, Set, Tuple + + +IDENTITY_PATTERN = re.compile( + r'^\s*\d+\)\s+([0-9A-Fa-f]{40})\s+"([^"]+)"' +) +DISTRIBUTION_CERTIFICATE_ALIASES = frozenset( + { + "apple distribution", + "ios distribution", + "iphone distribution", + } +) + + +@dataclass(frozen=True) +class ProvisioningProfile: + path: Path + name: str + team_ids: Tuple[str, ...] + expires_at: datetime + certificates: Dict[str, bytes] + + +def normalize_datetime(value: datetime) -> datetime: + if value.tzinfo is None: + return value.replace(tzinfo=timezone.utc) + return value.astimezone(timezone.utc) + + +def parse_identities(output: str) -> Dict[str, str]: + identities: Dict[str, str] = {} + for line in output.splitlines(): + match = IDENTITY_PATTERN.match(line) + if match: + identities[match.group(1).upper()] = match.group(2) + return identities + + +def certificate_selector_matches_identity(selector: str, identity_name: str) -> bool: + normalized_selector = selector.strip().casefold() + normalized_identity = identity_name.casefold() + if normalized_selector in normalized_identity: + return True + return normalized_selector in DISTRIBUTION_CERTIFICATE_ALIASES and any( + alias in normalized_identity for alias in DISTRIBUTION_CERTIFICATE_ALIASES + ) + + +def parse_certificate_expiration(output: str) -> datetime: + for line in output.splitlines(): + if line.startswith("notAfter="): + return normalize_datetime(parsedate_to_datetime(line.split("=", 1)[1])) + raise ValueError("openssl output does not contain notAfter") + + +def expiration_status( + expires_at: datetime, + now: datetime, + critical_days: int, + warning_days: int, +) -> Tuple[str, int]: + remaining_seconds = (normalize_datetime(expires_at) - normalize_datetime(now)).total_seconds() + days_left = math.floor(remaining_seconds / 86400) + if remaining_seconds <= 0: + return "expired", days_left + if remaining_seconds < critical_days * 86400: + return "critical", days_left + if remaining_seconds < warning_days * 86400: + return "warning", days_left + return "ok", days_left + + +def load_export_options(path: Path) -> Tuple[str, str, List[str]]: + with path.open("rb") as stream: + options = plistlib.load(stream) + + team_id = options.get("teamID") + certificate_selector = options.get("signingCertificate") + provisioning_profiles = options.get("provisioningProfiles") + if not isinstance(team_id, str) or not team_id: + raise ValueError("ExportOptions.plist does not define teamID") + if not isinstance(certificate_selector, str) or not certificate_selector: + raise ValueError("ExportOptions.plist does not define signingCertificate") + if not isinstance(provisioning_profiles, dict) or not provisioning_profiles: + raise ValueError("ExportOptions.plist does not define provisioningProfiles") + + profile_names = sorted( + {value for value in provisioning_profiles.values() if isinstance(value, str) and value} + ) + if not profile_names: + raise ValueError("ExportOptions.plist has no provisioning profile names") + return team_id, certificate_selector, profile_names + + +def profile_directories(explicit_directories: List[Path]) -> List[Path]: + if explicit_directories: + return explicit_directories + + configured = os.environ.get("IOS_PROVISIONING_PROFILE_DIRS") + if configured: + return [Path(value).expanduser() for value in configured.split(os.pathsep) if value] + + home = Path.home() + return [ + home / "Library/MobileDevice/Provisioning Profiles", + home / "Library/Developer/Xcode/UserData/Provisioning Profiles", + ] + + +def profile_files(directories: Iterable[Path]) -> List[Path]: + files: Set[Path] = set() + for directory in directories: + if not directory.is_dir(): + continue + files.update(directory.rglob("*.mobileprovision")) + files.update(directory.rglob("*.provisionprofile")) + return sorted(files) + + +def decode_profile(path: Path, security: str) -> Optional[ProvisioningProfile]: + result = subprocess.run( + [security, "cms", "-D", "-i", str(path)], + check=False, + capture_output=True, + ) + if result.returncode != 0: + return None + + try: + payload = plistlib.loads(result.stdout) + name = payload["Name"] + expires_at = normalize_datetime(payload["ExpirationDate"]) + except (KeyError, TypeError, ValueError, plistlib.InvalidFileException): + return None + + team_ids = payload.get("TeamIdentifier", ()) + if isinstance(team_ids, str): + team_ids = (team_ids,) + elif isinstance(team_ids, list): + team_ids = tuple(value for value in team_ids if isinstance(value, str)) + else: + team_ids = () + + certificates: Dict[str, bytes] = {} + for certificate in payload.get("DeveloperCertificates", ()): + if isinstance(certificate, bytes): + fingerprint = hashlib.sha1(certificate).hexdigest().upper() + certificates[fingerprint] = certificate + + if not isinstance(name, str) or not certificates: + return None + return ProvisioningProfile(path, name, team_ids, expires_at, certificates) + + +def installed_identities(security: str) -> Dict[str, str]: + result = subprocess.run( + [security, "find-identity", "-v", "-p", "codesigning"], + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or "security find-identity failed") + return parse_identities(result.stdout) + + +def certificate_expiration(certificate: bytes, openssl: str) -> datetime: + result = subprocess.run( + [openssl, "x509", "-inform", "DER", "-noout", "-enddate"], + input=certificate, + check=False, + capture_output=True, + ) + if result.returncode != 0: + message = result.stderr.decode(errors="replace").strip() + raise RuntimeError(message or "openssl could not read certificate") + return parse_certificate_expiration(result.stdout.decode(errors="replace")) + + +def report_expiration( + label: str, + expires_at: datetime, + now: datetime, + critical_days: int, + warning_days: int, +) -> None: + status, days_left = expiration_status( + expires_at, now, critical_days, warning_days + ) + print(f"{label} expires: {expires_at.isoformat()} ({days_left} days remaining)") + if status == "expired": + print(f"CRITICAL: {label} has expired", file=sys.stderr) + elif status == "critical": + print( + f"CRITICAL: {label} expires in less than {critical_days} days", + file=sys.stderr, + ) + elif status == "warning": + print(f"WARNING: {label} expires in less than {warning_days} days") + + +def validate(args: argparse.Namespace) -> int: + if args.critical_days < 0: + raise ValueError("critical-days must not be negative") + if args.warning_days < args.critical_days: + raise ValueError("warning-days must be greater than or equal to critical-days") + + security = shutil.which("security") + openssl = shutil.which("openssl") + if not security: + raise RuntimeError("macOS security command was not found") + if not openssl: + raise RuntimeError("openssl command was not found") + + team_id, certificate_selector, profile_names = load_export_options( + args.export_options + ) + directories = profile_directories(args.profile_directory) + profiles = [] + for path in profile_files(directories): + profile = decode_profile(path, security) + if profile is not None: + profiles.append(profile) + identities = installed_identities(security) + now = datetime.now(timezone.utc) + + print("iOS signing asset preflight") + print(f"Export options: {args.export_options}") + print(f"Team ID: {team_id}") + print(f"Certificate selector: {certificate_selector}") + + for profile_name in profile_names: + candidates = [ + profile + for profile in profiles + if profile.name == profile_name and team_id in profile.team_ids + ] + viable: List[Tuple[ProvisioningProfile, Set[str]]] = [] + for profile in candidates: + matching_fingerprints = { + fingerprint + for fingerprint in profile.certificates + if fingerprint in identities + and certificate_selector_matches_identity( + certificate_selector, identities[fingerprint] + ) + } + if matching_fingerprints: + viable.append((profile, matching_fingerprints)) + + if not viable: + searched = ", ".join(str(path) for path in directories) + print( + f"WARNING: no installed signing identity matches profile '{profile_name}' " + f"for team {team_id}; searched {searched}", + file=sys.stderr, + ) + continue + + profile, fingerprints = max(viable, key=lambda item: item[0].expires_at) + print(f"Provisioning profile: {profile.name} ({profile.path})") + report_expiration( + f"Provisioning profile '{profile.name}'", + profile.expires_at, + now, + args.critical_days, + args.warning_days, + ) + + certificate_options = [ + ( + certificate_expiration(profile.certificates[fingerprint], openssl), + fingerprint, + ) + for fingerprint in fingerprints + ] + expires_at, fingerprint = max(certificate_options) + print(f"Signing identity: {identities[fingerprint]}") + print(f"Certificate SHA-1: {fingerprint}") + report_expiration( + "Signing certificate", + expires_at, + now, + args.critical_days, + args.warning_days, + ) + + print("iOS signing asset preflight completed") + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--export-options", required=True, type=Path) + parser.add_argument( + "--profile-directory", + action="append", + default=[], + type=Path, + help="Provisioning profile directory; may be supplied more than once", + ) + parser.add_argument( + "--critical-days", + type=int, + default=int(os.environ.get("IOS_SIGNING_CRITICAL_DAYS", "30")), + ) + parser.add_argument( + "--warning-days", + type=int, + default=int(os.environ.get("IOS_SIGNING_WARNING_DAYS", "90")), + ) + return parser.parse_args() + + +def main() -> int: + try: + return validate(parse_args()) + except (OSError, RuntimeError, ValueError, plistlib.InvalidFileException) as error: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/ci/build/tests/test_check_ios_signing_assets.py b/.github/ci/build/tests/test_check_ios_signing_assets.py new file mode 100644 index 000000000..d9304e0a5 --- /dev/null +++ b/.github/ci/build/tests/test_check_ios_signing_assets.py @@ -0,0 +1,205 @@ +import hashlib +import importlib.util +import plistlib +import sys +import tempfile +import unittest +from datetime import datetime, timezone +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + + +MODULE_PATH = Path(__file__).parents[1] / "check_ios_signing_assets.py" +SPEC = importlib.util.spec_from_file_location("check_ios_signing_assets", MODULE_PATH) +MODULE = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +sys.modules[SPEC.name] = MODULE +SPEC.loader.exec_module(MODULE) + + +class SigningAssetCheckTests(unittest.TestCase): + def test_load_export_options_deduplicates_profile_names(self): + options = { + "teamID": "TEAM123", + "signingCertificate": "Apple Distribution", + "provisioningProfiles": { + "example.app": "shared-profile", + "example.app.extension": "shared-profile", + }, + } + with tempfile.NamedTemporaryFile() as stream: + plistlib.dump(options, stream) + stream.flush() + team_id, selector, profiles = MODULE.load_export_options( + Path(stream.name) + ) + + self.assertEqual(team_id, "TEAM123") + self.assertEqual(selector, "Apple Distribution") + self.assertEqual(profiles, ["shared-profile"]) + + def test_parse_identities_returns_sha1_to_name_mapping(self): + output = ( + ' 1) 553B538240B4CEF55467E6515514766F6767692C ' + '"iPhone Distribution: Example (TEAM123)"\n' + " 1 valid identities found\n" + ) + + self.assertEqual( + MODULE.parse_identities(output), + { + "553B538240B4CEF55467E6515514766F6767692C": ( + "iPhone Distribution: Example (TEAM123)" + ) + }, + ) + + def test_certificate_selector_matches_distribution_aliases(self): + for identity_name in ( + "Apple Distribution: Example (TEAM123)", + "iOS Distribution: Example (TEAM123)", + "iPhone Distribution: Example (TEAM123)", + ): + with self.subTest(identity_name=identity_name): + self.assertTrue( + MODULE.certificate_selector_matches_identity( + "iPhone Distribution", identity_name + ) + ) + + self.assertFalse( + MODULE.certificate_selector_matches_identity( + "iPhone Distribution", "Apple Development: Example (TEAM123)" + ) + ) + + def test_decode_profile_records_embedded_certificate_fingerprint(self): + certificate = b"test certificate bytes" + payload = plistlib.dumps( + { + "Name": "shared-profile", + "TeamIdentifier": ["TEAM123"], + "ExpirationDate": datetime(2030, 1, 1), + "DeveloperCertificates": [certificate], + } + ) + completed = SimpleNamespace(returncode=0, stdout=payload, stderr=b"") + + with mock.patch.object(MODULE.subprocess, "run", return_value=completed): + profile = MODULE.decode_profile(Path("profile.mobileprovision"), "security") + + self.assertIsNotNone(profile) + fingerprint = hashlib.sha1(certificate).hexdigest().upper() + self.assertEqual(profile.certificates, {fingerprint: certificate}) + self.assertEqual(profile.team_ids, ("TEAM123",)) + + def test_expiration_status_is_critical_inside_critical_window(self): + now = datetime(2026, 1, 1, tzinfo=timezone.utc) + + self.assertEqual( + MODULE.expiration_status( + datetime(2026, 1, 21, tzinfo=timezone.utc), now, 30, 90 + )[0], + "critical", + ) + self.assertEqual( + MODULE.expiration_status( + datetime(2026, 3, 2, tzinfo=timezone.utc), now, 30, 90 + )[0], + "warning", + ) + self.assertEqual( + MODULE.expiration_status( + datetime(2026, 6, 1, tzinfo=timezone.utc), now, 30, 90 + )[0], + "ok", + ) + + def test_parse_certificate_expiration_reads_openssl_date(self): + expires_at = MODULE.parse_certificate_expiration( + "notAfter=Dec 11 02:30:30 2026 GMT\n" + ) + + self.assertEqual( + expires_at, + datetime(2026, 12, 11, 2, 30, 30, tzinfo=timezone.utc), + ) + + def test_validate_matches_profile_certificate_to_installed_identity(self): + certificate = b"matching distribution certificate" + fingerprint = hashlib.sha1(certificate).hexdigest().upper() + profile = MODULE.ProvisioningProfile( + path=Path("profile.mobileprovision"), + name="shared-profile", + team_ids=("TEAM123",), + expires_at=datetime(2030, 1, 1, tzinfo=timezone.utc), + certificates={fingerprint: certificate}, + ) + options = { + "teamID": "TEAM123", + "signingCertificate": "iPhone Distribution", + "provisioningProfiles": {"example.app": "shared-profile"}, + } + + with tempfile.NamedTemporaryFile() as stream: + plistlib.dump(options, stream) + stream.flush() + args = SimpleNamespace( + export_options=Path(stream.name), + profile_directory=[Path("profiles")], + critical_days=30, + warning_days=90, + ) + with mock.patch.object( + MODULE.shutil, "which", side_effect=lambda value: value + ), mock.patch.object( + MODULE, "profile_files", return_value=[profile.path] + ), mock.patch.object( + MODULE, "decode_profile", return_value=profile + ), mock.patch.object( + MODULE, + "installed_identities", + return_value={ + fingerprint: "Apple Distribution: Example (TEAM123)" + }, + ), mock.patch.object( + MODULE, + "certificate_expiration", + return_value=datetime(2030, 1, 1, tzinfo=timezone.utc), + ) as certificate_expiration: + result = MODULE.validate(args) + + self.assertEqual(result, 0) + certificate_expiration.assert_called_once_with(certificate, "openssl") + + def test_validate_missing_identity_only_warns(self): + options = { + "teamID": "TEAM123", + "signingCertificate": "Apple Distribution", + "provisioningProfiles": {"example.app": "shared-profile"}, + } + + with tempfile.NamedTemporaryFile() as stream: + plistlib.dump(options, stream) + stream.flush() + args = SimpleNamespace( + export_options=Path(stream.name), + profile_directory=[Path("profiles")], + critical_days=30, + warning_days=90, + ) + with mock.patch.object( + MODULE.shutil, "which", side_effect=lambda value: value + ), mock.patch.object( + MODULE, "profile_files", return_value=[] + ), mock.patch.object( + MODULE, "installed_identities", return_value={} + ): + result = MODULE.validate(args) + + self.assertEqual(result, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/AGENTS.md b/AGENTS.md index af39f90f7..3249c06d4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,17 @@ Each platform directory contains its own `AGENTS.md` with platform-specific rule | macOS | `macOS/AGENTS.md` | | Windows | `windows/AGENTS.md` | +## AI Engineering Workflow + +For AI-assisted version iteration, product-to-implementation routing, multi-agent acceptance, or automatic API example generation, use the repository workflow in `.agent/skills/api-example-release-iteration/SKILL.md`. + +Supporting documents: +- `docs/ai-engineering/knowledge-index.md` - repository knowledge map for agents. +- `docs/ai-engineering/release-iteration-gate.md` - product, architecture, review, test, and UX acceptance gates. +- `docs/ai-engineering/role-routing.json` - shared Contract and independent platform role types with logical Codex profiles. +- `docs/ai-engineering/repository-profile.json` - repository-specific SDK package names and version sources used by the shared tools. +- `docs/ai-engineering/release-known-issues.md` - repository release-risk reference. + ## Cross-Platform Rules 1. Never share source files, build scripts, or SDK dependencies between platforms. diff --git a/cicd/build-template/build-ios.yml b/cicd/build-template/build-ios.yml index 89fd342a0..9a9ab7a62 100644 --- a/cicd/build-template/build-ios.yml +++ b/cicd/build-template/build-ios.yml @@ -16,26 +16,10 @@ jobs: steps: - script: cd 'cicd/scripts' && ls && python keycenter.py && ls + displayName: 'Generate KeyCenter' env: AGORA_APP_ID: $(agora.appId) File_Directory: '../../${{ parameters.workingDirectory }}/${{ parameters.project }}/Common' - - - task: InstallAppleCertificate@2 - inputs: - certSecureFile: 'App.p12' - certPwd: $(agora.password) - - - task: InstallAppleProvisioningProfile@1 - inputs: - provProfileSecureFile: 'App.mobileprovision' - script: cd 'cicd/scripts' && chmod +x ios_build.sh && ./ios_build.sh ../../${{ parameters.workingDirectory }} ${{ parameters.project }} ${{ parameters.scheme }} - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: ${{ parameters.workingDirectory }}/app - ArtifactName: ${{ parameters.displayName }} - - - template: github-release.yml - parameters: - displayName: ${{ parameters.displayName }} + displayName: 'Build without code signing' diff --git a/cicd/scripts/ios_build.sh b/cicd/scripts/ios_build.sh index a32c91747..56d939c10 100755 --- a/cicd/scripts/ios_build.sh +++ b/cicd/scripts/ios_build.sh @@ -1,81 +1,32 @@ -WORKING_PATH=$1 -APP_Project=$2 -APP_TARGET=$3 +#!/usr/bin/env bash +set -euo pipefail + +WORKING_PATH="$1" +APP_PROJECT="$2" +APP_TARGET="$3" MODE=Release echo "WORKING_PATH: ${WORKING_PATH}" echo "APP_TARGET: ${APP_TARGET}" -cd ${WORKING_PATH} -echo `pwd` - -rm -f *.ipa -rm -rf *.app -rm -f *.zip -rm -rf dSYMs -rm -rf *.dSYM -rm -f *dSYMs.zip -rm -rf *.xcarchive - -Export_Plist_File=ExportOptions.plist - -BUILD_DATE=`date +%Y-%m-%d-%H.%M.%S` -ArchivePath=${APP_TARGET}-${BUILD_DATE}.xcarchive -PBXPROJ_PATH="${APP_Project}.xcodeproj/project.pbxproj" -echo PBXPROJ_PATH: $PBXPROJ_PATH - -# 主项目工程配置 -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH - -# 屏幕共享Extension -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH +cd "${WORKING_PATH}" +pwd -# SimpleFilter -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH - -#修改build number -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH - - - - -TARGET_FILE="" -if [ ! -f "Podfile" ];then -TARGET_FILE="${APP_Project}.xcodeproj" -xcodebuild clean -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -xcodebuild CODE_SIGN_STYLE="Manual" -project ${TARGET_FILE} -scheme "${APP_TARGET}" clean CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -configuration ${MODE} -archivePath ${ArchivePath} archive +if [[ -f Podfile ]]; then + pod install + project_args=(-workspace "${APP_PROJECT}.xcworkspace") else -pod install -TARGET_FILE="${APP_Project}.xcworkspace" -xcodebuild clean -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -xcodebuild CODE_SIGN_STYLE="Manual" archive -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" clean CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -configuration ${MODE} -archivePath ${ArchivePath} -destination 'generic/platform=iOS' + project_args=(-project "${APP_PROJECT}.xcodeproj") fi -xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . - -mkdir app -mv *.ipa app && mv *.xcarchive app +xcodebuild \ + "${project_args[@]}" \ + -scheme "${APP_TARGET}" \ + -configuration "${MODE}" \ + -destination 'generic/platform=iOS' \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY= \ + DEVELOPMENT_TEAM= \ + PROVISIONING_PROFILE_SPECIFIER= \ + clean build diff --git a/docs/ai-engineering/case-maintenance-matrix.md b/docs/ai-engineering/case-maintenance-matrix.md new file mode 100644 index 000000000..f32a3a89f --- /dev/null +++ b/docs/ai-engineering/case-maintenance-matrix.md @@ -0,0 +1,112 @@ +# API Examples Case Maintenance Matrix + +This matrix is the planning artifact for cross-platform API Example parity work. It does not replace platform `AGENTS.md`, project `ARCHITECTURE.md`, or project `.agent/skills/*` / `.agents/skills/*`; every row must be verified against the target project before implementation. + +## How To Use + +1. Start from the product request and identify the feature name, SDK family, and key SDK APIs. +2. Find the matching row in this matrix. +3. For each target platform unit, verify the path with the project `query-cases` skill when present. +4. Treat each required platform project as an independent delivery/verification unit inside one requirement-level acceptance manifest. +5. Update this matrix when a case is added, moved, renamed, or confirmed not applicable. + +## Status Values + +| Status | Meaning | +| --- | --- | +| `DONE(path)` | Inventory says the case is implemented and expected to be registered at the listed path. Verify before editing. For new or changed parity work, use `DONE` only after the required reference, review, and build gates pass. | +| `PARTIAL(note)` | Exists but parity, registration, docs, verification, runtime smoke, or API coverage needs review. | +| `MISSING` | Required for parity but no implementation is currently recorded here. | +| `N/A(reason)` | Not applicable because the SDK family, platform, or project variant does not support it. | +| `UNKNOWN` | Not inventoried yet; run the project case query before making decisions. | + +## Active Parity Acceptance Rule + +For new or changed parity work, do not move a cell to `DONE` from compilation alone. + +The platform execution unit must record: + +- Reference Contract: source implementation, UI resources, registration, SDK overloads, enum/option values, defaults, limits, and expected success signals. +- Parity Checklist: final implementation matches the reference contract; similar cases are used only for framework patterns. +- Build Result: the target project compile/build command passes, or the exact blocker is recorded. + +If runtime/device behavior is required but unavailable, use `PARTIAL(...)` and note the pending smoke check instead of `DONE(...)`. + +## Platform Units + +| Unit | Project | Primary Source | +| --- | --- | --- | +| Android full | `Android/APIExample/` | `Android/APIExample/ARCHITECTURE.md` | +| Android audio | `Android/APIExample-Audio/` | `Android/APIExample-Audio/ARCHITECTURE.md` | +| Android Compose | `Android/APIExample-Compose/` | `Android/APIExample-Compose/ARCHITECTURE.md` | +| iOS UIKit | `iOS/APIExample/` | `iOS/APIExample/ARCHITECTURE.md` | +| iOS SwiftUI | `iOS/APIExample-SwiftUI/` | `iOS/APIExample-SwiftUI/ARCHITECTURE.md` | +| iOS Objective-C | `iOS/APIExample-OC/` | `iOS/APIExample-OC/ARCHITECTURE.md` | +| iOS audio | `iOS/APIExample-Audio/` | `iOS/APIExample-Audio/ARCHITECTURE.md` | +| macOS | `macOS/` | `macOS/ARCHITECTURE.md` | +| Windows | `windows/` | `windows/ARCHITECTURE.md` | + +## Pilot Matrix + +Seeded from the overseas `dev/4.6.4` project `ARCHITECTURE.md` indexes and source tree. The inventory audit resolved 191 `DONE` candidates against their target project roots; paths are relative to the projects listed in Platform Units. + +| Feature | SDK Family | Key APIs | Android full | Android audio | Android Compose | iOS UIKit | iOS SwiftUI | iOS Objective-C | iOS audio | macOS | Windows | Notes | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Join channel video | Full RTC | `joinChannel`, `setupLocalVideo`, `setupRemoteVideo` | `DONE(basic/JoinChannelVideo.java)` | `N/A(audio SDK)` | `DONE(samples/JoinChannelVideo.kt)` | `DONE(Basic/JoinChannelVideo/)` | `DONE(Basic/JoinChannelVideo/)` | `DONE(Basic/JoinChannelVideo/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo/)` | `PARTIAL(Basic/JoinChannelVideoByToken)` | Windows records token variant only; non-token parity gap. | +| Join channel video with token | Full RTC | `joinChannel` with token, local and remote video setup | `DONE(basic/JoinChannelVideoByToken.java)` | `N/A(audio SDK)` | `DONE(samples/JoinChannelVideoToken.kt)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideoByToken/)` | Verify token-generation behavior per platform before changing auth flow. | +| Join channel audio | Full RTC or audio SDK | `joinChannel`, `setAudioProfile`, audio routing or volume APIs | `DONE(basic/JoinChannelAudio.java)` | `DONE(basic/JoinChannelAudio.java)` | `DONE(samples/JoinChannelAudio.kt)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | +| Live streaming role switch | Full RTC | `setClientRole`, `joinChannel`, streaming role configuration | `DONE(advanced/LiveStreaming.java)` | `N/A(video/live)` | `DONE(samples/LiveStreaming.kt)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Advanced/LiveStreaming/)` | `N/A(video/live)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Basic/LiveBroadcasting/)` | Names differ by platform; keep product scenario wording stable. | +| RTMP or CDN streaming | Full RTC | `startRtmpStreamWithTranscoding`, `startRtmpStreamWithoutTranscoding`, `stopRtmpStream` | `DONE(advanced/RTMPStreaming.java)` | `N/A(video/live)` | `DONE(samples/RTMPStreaming.kt)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStream/)` | `DONE(Advanced/RTMPStreaming/)` | `N/A(video/live)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStream/)` | Windows also has `RTMPinject`; verify requested API before routing. | +| Media metadata | Full RTC | `registerMediaMetadataObserver` or platform metadata delegate APIs | `DONE(advanced/MediaMetadata.java)` | `N/A(audio SDK)` | `DONE(samples/MediaMetadata.kt)` | `DONE(Advanced/VideoMetadata/)` | `DONE(Advanced/VideoMetadata/)` | `DONE(Advanced/VideoMetadata/)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/Metadata/)` | macOS has no metadata case. iOS names this VideoMetadata. | +| Data stream messaging | Full RTC | `createDataStream`, `sendStreamMessage` | `DONE(advanced/SendDataStream.java)` | `MISSING` | `DONE(samples/SendDataStream.kt)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | Windows, Android audio, and iOS audio have no data stream case. | +| Custom audio source | Full RTC or audio SDK | `createCustomAudioTrack`, `setExternalAudioSource`, `pushExternalAudioFrame` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(samples/CustomAudioSource.kt)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomPCMAudioSource/)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioCapture/)` | Platform APIs differ; compare demonstrated behavior, not only method names. | +| Custom audio render | Full RTC or audio SDK | `setExternalAudioSink`, pull playback audio frame APIs | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(samples/CustomAudioRender.kt)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `MISSING` | Windows has no custom audio render case. | +| Custom video source | Full RTC | `setExternalVideoSource`, `pushExternalVideoFrame`, custom video track APIs | `DONE(advanced/PushExternalVideoYUV.java)` | `N/A(audio SDK)` | `DONE(samples/CustomVideoSource.kt)` | `DONE(Advanced/CustomVideoSourcePush/)` | `MISSING` | `DONE(Advanced/CustomVideoSourcePush/)` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoCapture/)` | SwiftUI has CustomVideoRender but no source-push case. Multi-source variants are separate rows. | +| Raw audio data | Full RTC or audio SDK | `registerAudioFrameObserver`, audio frame delegate APIs | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(samples/OriginAudioData.kt)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/OriginalAudio/)` | Check callback threading in every platform review. | +| Raw video data | Full RTC | `registerVideoFrameObserver`, video frame delegate APIs | `DONE(advanced/ProcessRawData.java)` | `N/A(audio SDK)` | `DONE(samples/OriginVideoData.kt)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `N/A(audio SDK)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/OriginalVideo/)` | Raw media combined samples are tracked separately. | +| Screen sharing | Full RTC | `startScreenCapture`, `updateScreenCaptureParameters`, `stopScreenCapture` | `DONE(advanced/ScreenSharing.java)` | `N/A(audio SDK)` | `DONE(samples/ScreenSharing.kt)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `N/A(audio SDK)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | iOS requires ReplayKit extension; Android requires foreground service. | +| Media player | Full RTC | `createMediaPlayer`, `open`, `play`, publish/update channel options | `DONE(advanced/MediaPlayer.java)` | `MISSING` | `DONE(samples/MediaPlayer.kt)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | `MISSING` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | Android audio and iOS audio have no standalone media player case. | +| Spatial audio | Full RTC or audio SDK | spatial audio engine, remote position, media player spatialization APIs | `DONE(advanced/SpatialSound.java)` | `DONE(advanced/SpatialSound.java)` | `DONE(samples/SpatialSound.kt)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | iOS and Android architecture docs record different key APIs; verify scenario first. | +| Stream encryption | Full RTC | `enableEncryption`, encryption configuration APIs | `DONE(advanced/ChannelEncryption.java)` | `MISSING` | `DONE(samples/ChannelEncryption.kt)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/StreamEncryption/)` | `MISSING` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/CustomEncrypt/ + MediaEncrypt/)` | Windows has multiple encryption rows; route by requested API. Audio projects lack encryption cases. | +| Pre-call test | Full RTC or audio SDK | `startEchoTest`, `stopEchoTest`, last-mile probe or network test APIs | `DONE(advanced/PreCallTest.java)` | `DONE(advanced/PreCallTest.java)` | `DONE(samples/PreCallTest.kt)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PrecallTest/)` | `MISSING` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PreCallTest/)` | iOS Objective-C has no pre-call test case. | +| Third-party beauty | Full RTC | extension enablement, third-party SDK initialization, video frame processing | `DONE(advanced/ThirdPartyBeauty.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/ThirdBeautify/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `MISSING` | `MISSING` | Only Android full and iOS UIKit have third-party beauty. Release work must check license expiration. | +| Extension sample | Full RTC extension | `enableExtension`, extension property APIs, matching native headers | `DONE(advanced/SimpleExtension.java)` | `N/A(audio SDK)` | `PARTIAL(VideoProcessExtension covers enableExtension)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | `DONE(Advanced/SimpleFilter/)` | `N/A(audio SDK)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | During SDK bumps, verify Android extension include files and iOS/macOS native assets. | +| Audio mixing | Full RTC or audio SDK | `startAudioMixing`, `stopAudioMixing`, `adjustAudioMixingVolume`, effect APIs | `DONE(advanced/PlayAudioFiles.java)` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(samples/PlayAudioFiles.kt)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage across all projects. | +| Rhythm player | Full RTC or audio SDK | `startRhythmPlayer`, `stopRhythmPlayer` | `DONE(advanced/RhythmPlayer.java)` | `DONE(advanced/RhythmPlayer.java)` | `DONE(samples/RhythmPlayer.kt)` | `DONE(Advanced/RhythmPlayer/)` | `DONE(Advanced/RhythmPlayer/)` | `DONE(Advanced/RhythmPlayer/)` | `DONE(Advanced/RhythmPlayer/)` | `MISSING` | `MISSING` | macOS and Windows have no rhythm player case. | +| Media recorder | Full RTC | `createMediaRecorder`, `startRecording`, `stopRecording` | `DONE(advanced/MediaRecorder.java)` | `N/A(audio SDK)` | `DONE(samples/MediaRecorder.kt)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Advanced/MediaRecorder/)` | iOS/macOS embed recorder in a join-channel variant; Android/Compose/Windows have standalone cases. | +| Local video transcoding | Full RTC | `startLocalVideoTranscoder`, `startCameraCapture`, compositing APIs | `DONE(advanced/LocalVideoTranscoding.java)` | `N/A(audio SDK)` | `DONE(samples/LocalVideoTranscoding.kt)` | `DONE(Advanced/LocalCompositeGraph/)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalCompositeGraph/)` | `N/A(audio SDK)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalVideoTranscoding/)` | iOS UIKit and OC name this LocalCompositeGraph; SwiftUI splits transcoding and composition. | +| Multi-channel join | Full RTC | `joinChannelEx`, `leaveChannelEx`, multi-channel connection APIs | `DONE(advanced/JoinMultipleChannel.java)` | `N/A(audio SDK)` | `DONE(samples/JoinMultiChannel.kt)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `N/A(audio SDK)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/MultiChannel/)` | Full coverage across full-RTC projects. | +| Multipath | Full RTC | `enableMultipath`, `setMultipathConfig`, or `updateChannel` with multipath options | `DONE(advanced/Multipath.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | `N/A(audio SDK)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | Android Compose has no multipath case. | +| Content inspect | Full RTC | `enableContentInspect` | `DONE(advanced/ContentInspect.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `N/A(audio SDK)` | `DONE(Advanced/ContentInspect/)` | `MISSING` | Android Compose and Windows have no content inspect case. | +| Channel media relay | Full RTC | `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay`, pause/resume APIs | `DONE(advanced/HostAcrossChannel.java)` | `N/A(audio SDK)` | `DONE(samples/HostAcrossChannel.kt)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/MediaChannelRelay/)` | `N/A(audio SDK)` | `DONE(Advanced/ChannelMediaRelay/)` | `DONE(Advanced/CrossChannel/)` | Full coverage across full-RTC projects. | +| Picture in picture | Full RTC | `enterPictureInPictureMode` or `AVPictureInPictureController` | `DONE(advanced/PictureInPicture.java)` | `N/A(audio SDK)` | `DONE(samples/PictureInPicture.kt)` | `DONE(Advanced/PictureInPicture/)` | `DONE(Advanced/PictureInPicture/)` | `DONE(Advanced/PictureInPicture/)` | `N/A(audio SDK)` | `MISSING` | `MISSING` | macOS and Windows have no PiP case. | + +## Confirmed Gaps + +These gaps are derived from the pilot matrix above and should be prioritized when parity work is requested. + +| Gap | Affected Units | Severity | +| --- | --- | --- | +| Basic audio-only join channel | Windows | High — missing foundational case | +| Media metadata | macOS | Medium — full-RTC platform gap | +| Data stream messaging | Android audio, iOS audio, Windows | Medium — audio and Windows gap | +| Custom audio render | Windows | Medium — full-RTC platform gap | +| Media player (standalone) | Android audio, iOS audio | Low — audio projects may not need standalone player | +| Stream encryption | Android audio, iOS audio | Low — audio projects may not need encryption | +| Pre-call test | iOS Objective-C | Medium — full-RTC variant gap | +| Third-party beauty | Android Compose, iOS SwiftUI, iOS OC, macOS, Windows | Low — third-party SDK and license overhead | +| Extension sample | iOS SwiftUI, Windows | Medium — extension enablement gap | +| Rhythm player | macOS, Windows | Low — audio utility gap | +| Multipath | Android Compose | Low — Compose variant gap | +| Content inspect | Android Compose, Windows | Low — moderation feature gap | +| Picture in picture | macOS, Windows | Low — platform feature gap | +| Custom video source (push) | iOS SwiftUI | Medium — SwiftUI variant gap | + +## Maintenance Rules + +- Do not change a `UNKNOWN` cell to `MISSING` until the target project `query-cases` skill or architecture file has been checked. +- Do not change a `PARTIAL` cell to `DONE` unless registration, discoverability, reference parity, review, and build evidence are confirmed for the active work. +- Prefer feature names based on user-visible scenario rather than platform-specific class names. +- Keep API names in the matrix broad enough to route work, then validate exact signatures in the target SDK version. +- Keep release-risk notes in this matrix short; detailed release checks belong in `docs/ai-engineering/release-known-issues.md`. +- When a new case is added to any project, update the matching row in the same edit or follow-up. diff --git a/docs/ai-engineering/knowledge-index.md b/docs/ai-engineering/knowledge-index.md new file mode 100644 index 000000000..9f393d275 --- /dev/null +++ b/docs/ai-engineering/knowledge-index.md @@ -0,0 +1,226 @@ +# API Examples AI Engineering Knowledge Index + +This index is the first file to read when an agent turns a product request into API Example changes. It links the existing repository knowledge into one route so the agent does not rediscover platform rules, case patterns, build commands, and known failure modes from scratch. + +## Source Priority + +Use sources in this order: + +1. Product request or issue text provided by the user. +2. `AGENTS.md` at the repository root for cross-platform boundaries. +3. Platform `AGENTS.md` for platform selection and red lines. +4. Project `AGENTS.md` for concrete commands and project-level skills. +5. Project `ARCHITECTURE.md` for case index, registration rules, and canonical file layout. +6. `docs/ai-engineering/repository-profile.json` for repository-specific SDK package names and version sources. +7. `docs/ai-engineering/case-maintenance-matrix.md` for cross-platform parity planning and known gaps. +8. Existing project `.agent/skills/*/SKILL.md` or `.agents/skills/*/SKILL.md` for case query, creation, and review procedures. +9. `docs/ai-engineering/release-known-issues.md` for release packaging and pipeline risks. +10. Repository hooks and platform build scripts for final verification. + +Do not use memory or sibling repositories as the source of truth when the current checkout has conflicting instructions. Treat external knowledge as a hint, then verify it against this repository. + +## Repository Boundary + +The repository has four independent platform families: + +| Platform | Root | Primary Rules | +| --- | --- | --- | +| Android | `Android/` | `Android/AGENTS.md` | +| iOS | `iOS/` | `iOS/AGENTS.md` | +| macOS | `macOS/` | `macOS/AGENTS.md` | +| Windows | `windows/` | `windows/AGENTS.md` | + +Never share source files, build scripts, SDK dependencies, or generated project metadata across platform roots unless a platform rule explicitly says to do so. + +## Project Selection + +| Request Type | Default Target | +| --- | --- | +| Android full RTC, video, screen sharing, beauty, extensions | `Android/APIExample/` | +| Android voice-only or audio-only SDK behavior | `Android/APIExample-Audio/` | +| Android Compose case or Compose parity work | `Android/APIExample-Compose/` | +| iOS UIKit Swift full RTC | `iOS/APIExample/` | +| iOS SwiftUI parity work | `iOS/APIExample-SwiftUI/` | +| iOS Objective-C parity work | `iOS/APIExample-OC/` | +| iOS audio-only SDK behavior | `iOS/APIExample-Audio/` | +| macOS Swift Cocoa sample | `macOS/` | +| Windows C++ MFC sample | `windows/` | + +If the product request does not name a platform, default to Android, iOS, macOS, and Windows. Narrow the scope only when the user or Contract records an explicit waiver reason. Treat each required platform as a separate implementation and verification unit. + +## Existing Local Skills + +| Scope | Skills | +| --- | --- | +| `Android/APIExample/` | `query-cases`, `upsert-case`, `review-case` | +| `Android/APIExample-Audio/` | `query-cases`, `upsert-case`, `review-case` | +| `Android/APIExample-Compose/` | `query-cases`, `upsert-case`, `review-case` | +| `iOS/APIExample/` | `query-cases`, `upsert-case`, `review-case` | +| `iOS/APIExample-Audio/` | `query-cases`, `upsert-case`, `review-case` | +| `iOS/APIExample-OC/` | `query-cases`, `upsert-case`, `review-case` | +| `iOS/APIExample-SwiftUI/` | `query-cases`, `upsert-case`, `review-case` | +| `macOS/` | `upsert-case`, `review-case` | +| `windows/` | `upsert-case`, `review-case` | + +The repository-level orchestration skill is `.agent/skills/api-example-release-iteration/SKILL.md`. + +## Repository Profile + +`docs/ai-engineering/repository-profile.json` contains the SDK version sources that differ between API Examples distributions. Shared Python tools parse source kinds such as Gradle properties, CocoaPods packages, and SDK archive names without embedding distribution package names in code. + +The orchestrator stores the profile path and SHA-256 in the execution package and input snapshots. Dispatch and assembly stop if the checked-in profile changes after initialization. Coverage state and historical knowledge do not belong in this profile; keep them in the matrix and durable knowledge documents. + +## Codex Role Routing + +`docs/ai-engineering/role-routing.json` defines three role types and logical profiles. Provider model names remain runtime inputs through `--model` or `CODEX_MODEL_STANDARD`, `CODEX_MODEL_DEEP`, and `CODEX_MODEL_REVIEW`. The orchestrator starts replayable independent `codex exec` sessions; do not describe them as parent-managed Codex subagents. + +The phase order is: + +1. `contract`: one shared Contract for product behavior, reference, cross-platform invariants, and platform targets. +2. `implementation`: Android, iOS, macOS, and Windows agents run independently after Contract passes. A shared checkout serializes them and reconciles each delta before the next run. +3. `verification`: independent Android, iOS, macOS, and Windows agents run concurrently after their matching implementation passes. + +Release checks are mandatory manifest data, not an agent phase. Every platform dispatch has its own timeout, hashed JSONL command log, run identity, host platform, input snapshot, and Contract-selected working directory. Use `--platform` for a focused retry; otherwise the workflow covers all four official roots. + +## Agent Acceptance Artifacts + +Use `docs/ai-engineering/templates/acceptance-manifest-template.json` when an iteration changes source, workflow gates, matrix state, SDK-version logic, or docs that affect agent behavior. + +Validate the filled manifest with: + +```bash +python3 docs/ai-engineering/tools/validate_acceptance_manifest.py +``` + +The filled manifest is normally an execution artifact, not a repository document. Commit it only when the user explicitly asks for an evidence snapshot or when it is selected as a curated pilot-run example. + +For case backfill work, generate candidate platform execution units from the matrix: + +```bash +python3 docs/ai-engineering/tools/generate_case_backlog.py +``` + +Use the generated priority to select the next product requirement, then deliver that requirement across all four official platform families. Contract selects the applicable project variant inside each platform. + +## Case Implementation Knowledge + +For cross-platform parity requests: + +- Start with `docs/ai-engineering/case-maintenance-matrix.md`. +- Verify every relevant matrix cell against the target project before editing. +- Treat `UNKNOWN` as "not checked yet", not as a confirmed gap. +- Treat each required platform as an independent delivery/verification unit inside one requirement-level acceptance manifest. + +Before adding a case: + +- Use the target project's `query-cases` skill when available. +- Read the target project's `ARCHITECTURE.md` case index. +- Confirm whether the case already exists by feature name and SDK API name. +- Confirm the target group, registration mechanism, display name, and index rules. +- Extract the reference contract for parity work before implementation. The closest existing target-project case may guide lifecycle, permissions, UI framework, and registration patterns, but product semantics must come from the source reference contract. + +While adding or modifying a case: + +- Follow the target project's `upsert-case` skill. +- Keep edits inside the selected project unless the project-level instructions say otherwise. +- Update `ARCHITECTURE.md` when the case list, path, or key APIs change. +- Keep sensitive configuration placeholders intact. Do not commit real App IDs, certificates, tokens, or credentials. + +After implementation: + +- Run the target project's `review-case` skill. +- Run the applicable build or static check from the project `AGENTS.md`. +- Record which checks passed, failed, or were skipped with a reason. PASS/FAIL commands must match a JSONL `command_execution` event and exit code. +- Fill and validate an acceptance manifest before claiming final acceptance. + +## Knowledge Maintenance Protocol + +When an iteration uncovers a reusable failure pattern, record it in the right durable document instead of leaving it only in a chat summary. + +Use this structure: + +- Source: review finding, CI failure, release blocker, customer report, or manual smoke. +- Impact platform/project. +- Symptom. +- Root cause. +- Guardrail or rule. +- Verification command or evidence. +- Updated date. + +Where to write it: + +- Cross-platform routing or implementation traps: `docs/ai-engineering/knowledge-index.md`. +- Release, signing, CI, packaging, license, or SDK-version risks: `docs/ai-engineering/release-known-issues.md`. +- Case coverage state: `docs/ai-engineering/case-maintenance-matrix.md`. +- Project-specific implementation traps: the target project `ARCHITECTURE.md`, `.agent/skills/*/SKILL.md`, or `.agents/skills/*/SKILL.md`. + +Do not duplicate the same rule in every file. Put the durable rule at the lowest scope that future agents must read. + +When an acceptance manifest includes `knowledge_updates`, at least one platform Implementation must list the durable document or skill change in `files_changed`. The validator enforces this so new failure patterns cannot remain only in the execution summary. + +## Cross-Cutting Red Lines + +- Every case owns its RTC engine lifecycle. +- Leave the channel before destroying or releasing the engine. +- SDK callbacks can arrive on background threads; dispatch UI updates to the platform main thread. +- Do not hardcode real credentials. +- Do not mix full SDK and audio-only SDK APIs. +- Do not move common sample code across platform roots to reduce duplication. +- Do not edit packaging or CI scripts unless the product request or failure requires it. + +## Verification Entrypoints + +| Area | Entrypoint | +| --- | --- | +| Sensitive info and commit-message hooks | `HOOKS-GUIDE.md`, `.git-hooks/`, `.pre-commit-config.yaml`, `.gitleaks.toml` | +| Android build/test | project `AGENTS.md`, project Gradle files | +| iOS build | project `AGENTS.md`, project `Podfile`, `.github/ci/build/build_ios*.sh` | +| macOS build | `macOS/AGENTS.md`, `macOS/Podfile`, `.github/ci/build/build_mac*.sh` | +| Windows build | `windows/AGENTS.md`, `.github/ci/build/build_windows.*` | +| CI/CD | `azure-pipelines.yml`, `.github/ci/`, `.github/workflows/`, `cicd/` | +| Release known issues | `docs/ai-engineering/release-known-issues.md` | + +Prefer the smallest check that validates the changed surface. For docs-only changes, use static validation and link checks instead of full platform builds. + +## Pilot Run Reports + +`docs/ai-engineering/pilot-runs/` contains curated workflow validation examples, not a log of every agent execution. + +Use these reports to understand how a gate was exercised at a point in time. Do not use them as the source of truth for current SDK versions, case coverage, CI status, or release readiness. Re-check the live repository and current CI before reusing any conclusion. + +Only add a new pilot-run report when it is intentionally selected as a representative workflow example or when the user explicitly asks for a repository evidence snapshot. Routine run results should stay in the response, PR discussion, CI artifact, or task tracker. + +Use `docs/ai-engineering/templates/release-dry-run-template.md` as the starting point when such a repository evidence snapshot is needed. + +## Known Failure Patterns To Guard Against + +| Pattern | Guardrail | +| --- | --- | +| Wrong platform touched during a focused fix | Re-read root and platform `AGENTS.md`; list intended files before editing. | +| Case added but not visible in app | Verify project registration mechanism and case index. | +| Android case ordering collision | Use `query-cases` and scan source annotations before choosing a sort index. | +| SDK callback updates UI directly | Run `review-case`; check every callback that touches UI. | +| Engine leak after leaving screen | Check leave-channel before destroy/release in the real screen-close path. | +| Audio-only project receives video API | Re-check selected project SDK type before implementation. | +| CI diagnosis starts in the wrong layer | Identify the first failing log line before changing scripts or signing settings. | +| Third-party beauty license expires silently | Track expiration and confirm renewal before release branches are packaged. | +| Android Extension SDK headers drift from SDK version | Check extension `include` files during SDK version bumps. | +| iOS/macOS certificates expire on build machines | Inspect or print certificate expiration during release preparation. | +| SDK dependency version is not bumped on release branch | Verify platform SDK version files against the release target. | +| Model-declared build result is not backed by command telemetry | Bind each executed verification command to the hashed Codex JSONL log and exit code, and accept build status only for recognized platform build tools. | +| Platform run misses nested instructions | Start `codex exec` in the Contract-selected target so root/platform/project `AGENTS.md` files load automatically. | +| External packaging metadata leaks into repository acceptance | Keep Jenkins jobs, artifact URLs, QA evidence, and website publication outside the acceptance manifest. | +| Windows packaging fails from path length or permissions | Run Windows script preflight after path or packaging changes. | + +## Output Contract + +Every completed AI-assisted iteration should end with an acceptance summary: + +- Product request interpreted. +- Target platform/project. +- Files changed. +- Skills or docs used. +- Verification run and results. +- Open risks or skipped checks. + +Use `docs/ai-engineering/release-iteration-gate.md` for the full gate. diff --git a/docs/ai-engineering/pilot-runs/README.md b/docs/ai-engineering/pilot-runs/README.md new file mode 100644 index 000000000..13253ac77 --- /dev/null +++ b/docs/ai-engineering/pilot-runs/README.md @@ -0,0 +1,27 @@ +# Pilot Run Reports + +This directory stores curated workflow validation examples for the API Examples AI engineering process. + +Pilot-run reports are historical evidence. They show how a gate or workflow behaved at a specific branch, commit, and date. They are not the source of truth for current SDK versions, case coverage, CI status, signing state, or release readiness. + +Use `docs/ai-engineering/templates/release-dry-run-template.md` when a new release dry-run evidence snapshot is explicitly needed. + +## What Belongs Here + +- First dry run for a new repository gate or workflow. +- Representative cross-platform rehearsal that future agents can learn from. +- Explicitly requested evidence snapshot for a release or workflow decision. + +## What Does Not Belong Here + +- Every routine agent execution result. +- Repeated acceptance summaries for normal feature work. +- CI logs or build output that already belongs in CI artifacts. +- Mutable release status that must be checked live. + +## Maintenance Rules + +- Use dated filenames. +- Keep reviewed reports immutable; create a new report for a new run. +- Include branch, commit, scope, commands, skipped checks, and final status. +- Re-check live files and CI before using an old report as evidence. diff --git a/docs/ai-engineering/release-iteration-gate.md b/docs/ai-engineering/release-iteration-gate.md new file mode 100644 index 000000000..6b0101ab1 --- /dev/null +++ b/docs/ai-engineering/release-iteration-gate.md @@ -0,0 +1,161 @@ +# API Examples Release Iteration Gate + +One RTC product requirement is accepted across Android, iOS, macOS, and Windows. The Lead coordinates shared scope and cross-platform closure; platform implementation and verification remain independently owned. + +## Topology + +| Agent | Count | Pass Evidence | +| --- | --- | --- | +| Contract | 1 | Shared behavior, APIs, non-goals, reference, platform targets and constraints | +| Platform Implementation | 4 | Query/upsert result, scoped files, matrix proposals | +| Platform Verification | 4 | Findings, parity, entry/UX, commands, build result, skipped reasons | +| Lead | 1 | Cross-platform differences, final status, manifest assembly | + +Contract runs once. In a shared checkout, Android/iOS/macOS/Windows Implementation runs are serialized so every repository delta is attributable to one agent. Each Verification depends on Contract and its matching Implementation, runs concurrently with other platform Verification roles, and cannot reuse the Implementation agent identity. + +## Intake Gate + +Before Contract starts, identify feature behavior, SDK family, key APIs, target SDK version for each platform, reference case, expected user flow, repository scope, and available target verification hosts. Platform targets may differ when SDK release lines are staggered. Confirm that the checked-in repository profile describes the current SDK package names and version sources. All four top-level platforms are required by default. Contract may choose the appropriate project variant per platform. + +## Contract Gate + +Contract passes only when: + +- Scenario, inputs, success signals, APIs, and non-goals are observable. +- A reference source and result are recorded when parity is required. +- Cross-platform invariant behavior is explicit. +- Android, iOS, macOS, and Windows each have `required`, `target_project`, constraints, and allowed files. +- Lifecycle, callback threading, registration, SDK family, and credential boundaries are explicit. + +Nearby cases guide framework patterns; product behavior comes from the shared Contract/reference. + +## Platform Implementation Gate + +Each platform Implementation passes only when it: + +- Reads that platform and target-project instructions. +- Runs from the Contract-selected target project so nested `AGENTS.md` instructions are loaded by Codex. +- Queries existing cases before editing. +- Uses the selected project skill and stays inside Contract's allowed paths. +- Completes registration, localization, lifecycle cleanup, and architecture index updates. +- Lets the orchestrator derive its real platform file delta and records matrix proposals. + +Platform source roots are independent, but Implementation runs sharing one checkout are serialized so cross-root edits cannot be credited to the wrong agent. Each run refreshes an input snapshot bound to the execution package and dependency artifacts; retries retain attempt provenance and produce one cumulative net delta. Separate workspaces may execute them concurrently only when each produces an independently attributable patch. + +## Platform Verification Gate + +Each Verification independently checks: + +- Contract/reference parity, SDK types/defaults, lifecycle, threading, errors, and cleanup. +- Registration, display name, inputs, feedback, layout, and audio/video affordances. +- The strongest valid build or static command on the current verification host. +- Each executed PASS/FAIL command is bound to the hashed Codex JSONL command event and exit code; build results use recognized platform build actions from the Contract working directory. +- Dispatch provenance records the host platform. Windows build `PASS` requires a Windows host. +- The Verification run does not modify tracked or untracked repository content. +- Concrete reasons for every skipped check. + +For non-`BLOCKED` acceptance, every required platform needs role status, review result, parity result, and build result `PASS`. Final `PASS` permits no skipped checks. + +## Host Mismatch + +Host mismatch never authorizes SDK downloads, emulation, cross-compilation, or substitute compilers as evidence. On macOS, Windows Verification may report static findings but must keep `build_result=BLOCKED` and role status `BLOCKED`. A retry on a real Windows host can replace that artifact using `--platform windows --retry`. + +## Cross-Platform Gate + +After platform Verification, the Lead records: + +- `result`: `PASS`, `FAIL`, or `BLOCKED`. +- Evidence that all required platform contracts and behavior were compared. +- Intentional platform differences and their product/API justification. + +Any required platform blocker or cross-platform result other than `PASS` forces final `BLOCKED`. + +## Release Checklist + +Release is mandatory manifest data, not an agent. `requirement.target_sdk_versions` and `release.target_sdk_versions` must contain the same Android, iOS, macOS, and Windows mapping. The orchestrator compares each platform only with its own target while refreshing dependency versions from live repository files during assembly. Non-`BLOCKED` acceptance requires all SDK-version checks to pass and permits no skipped repository release checks. A platform remaining on an older SDK is valid and does not require changing the script or repository profile. + +Jenkins packaging, QA validation, artifact URLs, and external website publication are downstream processes outside this repository workflow. A final `PASS` means the API Example source and repository checks are ready for that external handoff; the manifest does not represent packaging or QA completion. + +## Manifest V4 + +The single state tree is: + +```text +requirement +contract +platforms.android.implementation / verification +platforms.ios.implementation / verification +platforms.macos.implementation / verification +platforms.windows.implementation / verification +cross_platform_acceptance +release +knowledge_updates +final_status +``` + +Validate it with: + +```bash +python3 docs/ai-engineering/tools/validate_acceptance_manifest.py +``` + +The validator enforces platform completeness, unique agent/run provenance, Contract target boundaries, platform result closure, cross-platform closure, matrix rules, SDK-version checks, repository paths, evidence-file hashes, and knowledge writeback. + +## Orchestration + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ + --matrix docs/ai-engineering/case-maintenance-matrix.md \ + --feature "" \ + --target-sdk-version "4.6.2" \ + --platform-sdk-version "android=4.6.3" \ + --run-dir /tmp/api-example-requirement + +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement --phase contract --model "" + +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement --phase implementation --model "" + +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement --phase verification --model "" +``` + +`--target-sdk-version` is the baseline for all four platforms. Repeat `--platform-sdk-version platform=x.y.z` for each differing platform; omit overrides when all four use one version. If the product requirement is not yet an actionable matrix row, initialize it directly with `--sdk-family` and one or more `--key-api` arguments. + +Omitting `--platform` dispatches all four platform roles. Implementation runs are serialized in the shared checkout and reconciled after each run; Verification runs execute concurrently. Each independent `codex exec` run has a timeout, host provenance, a target-project working directory, and a hashed JSONL log. The execution package and input snapshots bind the checked-in repository profile by path and SHA-256. A prior `FAIL`/`BLOCKED` platform artifact can be replaced with `--retry`. + +## Automation Boundary + +The tools prepare platform agents, serialize shared-checkout implementation, parallelize immutable verification, enforce dependencies/timeouts, validate v4, and gate matrix changes. The Lead still approves phase advancement, retries, intentional differences, and final status. + +## Final Summary + +```markdown +## Acceptance Summary + +Requirement: +- Scenario/APIs: +- Contract/reference: + +Platforms: +- Android: files, verification, build +- iOS: files, verification, build +- macOS: files, verification, build +- Windows: files, verification, build + +Cross-platform: +- Result/evidence: +- Intentional differences: + +Release: +- SDK-version checks: +- External handoff required: Jenkins packaging and QA + +Manifest: +- Path/validation: + +Final status: PASS | PASS WITH RISKS | BLOCKED +``` + +Routine manifests remain execution artifacts unless explicitly selected as repository evidence. diff --git a/docs/ai-engineering/release-known-issues.md b/docs/ai-engineering/release-known-issues.md new file mode 100644 index 000000000..2fc609d5c --- /dev/null +++ b/docs/ai-engineering/release-known-issues.md @@ -0,0 +1,20 @@ +# API Examples Release Known Issues + +This file records repository-specific risks that can affect API Example source acceptance. Keep Jenkins packaging, QA validation, artifact distribution, and website publication outside the acceptance manifest. + +## Repository Acceptance Risks + +| Area | Risk | Repository Gate | +| --- | --- | --- | +| SDK versions | Android, iOS, macOS, and Windows dependency declarations can drift across release branches. | Validate every source declared in `repository-profile.json` against the target SDK version. | +| Android extensions | Extension samples can retain headers or native assets from an older SDK. | Review extension `include` files and matching assets whenever the SDK dependency changes. | +| Third-party beauty | Sample licenses and bundled assets can expire or become incompatible. | Record the current license or asset limitation when the related example changes. | +| iOS/macOS signing | Local verification may not cover external package-signing configuration. | Treat signing and packaging as an external handoff; do not report repository acceptance as package completion. | +| Windows verification | macOS cannot provide MSBuild evidence. | Keep Windows build verification blocked until the role runs on a real Windows host. | + +## Maintenance Rules + +- Keep target SDK package names and version sources in `repository-profile.json`, not Python code. +- Add a reusable failure pattern to this file only when it is verified against this repository. +- Keep case coverage in `case-maintenance-matrix.md`; do not use this file as a second coverage tracker. +- Do not add Jenkins URLs, package artifact URLs, QA ownership, or website publication state to the acceptance manifest. diff --git a/docs/ai-engineering/repository-profile.json b/docs/ai-engineering/repository-profile.json new file mode 100644 index 000000000..009841cc0 --- /dev/null +++ b/docs/ai-engineering/repository-profile.json @@ -0,0 +1,59 @@ +{ + "version": 1, + "sdk_version_sources": { + "android": [ + { + "path": "Android/APIExample/gradle.properties", + "kind": "gradle-property", + "key": "rtc_sdk_version" + }, + { + "path": "Android/APIExample-Audio/gradle.properties", + "kind": "gradle-property", + "key": "rtc_sdk_version" + }, + { + "path": "Android/APIExample-Compose/gradle.properties", + "kind": "gradle-property", + "key": "rtc_sdk_version" + } + ], + "ios": [ + { + "path": "iOS/APIExample/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_iOS" + }, + { + "path": "iOS/APIExample-Audio/Podfile", + "kind": "cocoapods", + "package": "AgoraAudio_iOS" + }, + { + "path": "iOS/APIExample-OC/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_iOS" + }, + { + "path": "iOS/APIExample-SwiftUI/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_iOS" + } + ], + "macos": [ + { + "path": "macOS/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_macOS" + } + ], + "windows": [ + { + "path": "windows/APIExample/install.ps1", + "kind": "archive-name", + "prefix": "Agora_Native_SDK_for_Windows_v", + "suffix": "_FULL.zip" + } + ] + } +} diff --git a/docs/ai-engineering/role-routing.json b/docs/ai-engineering/role-routing.json new file mode 100644 index 000000000..8e9d67cf0 --- /dev/null +++ b/docs/ai-engineering/role-routing.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "profiles": { + "standard": { + "reasoning_effort": "medium" + }, + "deep": { + "reasoning_effort": "high" + }, + "review": { + "reasoning_effort": "high" + } + }, + "phases": [ + "contract", + "implementation", + "verification" + ], + "roles": { + "contract": { + "profile": "standard", + "phase": "contract", + "sandbox": "read-only" + }, + "implementation": { + "profile": "deep", + "phase": "implementation", + "sandbox": "workspace-write" + }, + "verification": { + "profile": "review", + "phase": "verification", + "sandbox": "workspace-write" + } + } +} diff --git a/docs/ai-engineering/templates/README.md b/docs/ai-engineering/templates/README.md new file mode 100644 index 000000000..ab62636a2 --- /dev/null +++ b/docs/ai-engineering/templates/README.md @@ -0,0 +1,16 @@ +# AI Engineering Templates + +This directory stores reusable templates for repository-level AI engineering workflows. + +Templates are not completed evidence. Before using a template, replace every placeholder with live repository data and current command output. + +Templates may intentionally fail validation until their placeholders are replaced. The version 4 manifest stores one shared Contract and one Implementation/Verification pair for each official platform; use the orchestrator to add model, run, host, prompt/artifact, input-snapshot, working-directory, repository-delta, repository-profile, and hashed command-log provenance. Release data is limited to per-platform target SDK versions and repository checks. + +Do not commit routine execution output from these templates unless the user explicitly asks for a repository evidence snapshot or the run is selected as a representative workflow validation example. + +## Templates + +| Template | Purpose | +| --- | --- | +| `acceptance-manifest-template.json` | Requirement-level Contract, four platform delivery units, cross-platform acceptance, release, and knowledge evidence. | +| `release-dry-run-template.md` | Human-readable release dry-run evidence snapshot. | diff --git a/docs/ai-engineering/templates/acceptance-manifest-template.json b/docs/ai-engineering/templates/acceptance-manifest-template.json new file mode 100644 index 000000000..d92e990a0 --- /dev/null +++ b/docs/ai-engineering/templates/acceptance-manifest-template.json @@ -0,0 +1,283 @@ +{ + "version": 4, + "final_status": "BLOCKED", + "requirement": { + "feature": "", + "sdk_family": "", + "key_apis": [""], + "target_sdk_versions": { + "android": "", + "ios": "", + "macos": "", + "windows": "" + } + }, + "contract": { + "agent_id": "contract-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/contract.md", + "artifact": "role-artifacts/contract.json" + }, + "status": "BLOCKED", + "evidence": "Pending shared Contract.", + "summary": "Pending shared behavior, reference, and platform targets.", + "output": { + "scenario": "", + "key_apis": [""], + "non_goals": [], + "reference": { + "required": false, + "source_case": "", + "contract_result": "SKIPPED" + }, + "cross_platform_requirements": [""], + "platform_targets": { + "android": { + "required": true, + "target_project": "Android/APIExample/", + "key_constraints": [], + "files_allowed": ["Android/APIExample/"], + "waiver_reason": "" + }, + "ios": { + "required": true, + "target_project": "iOS/APIExample/", + "key_constraints": [], + "files_allowed": ["iOS/APIExample/"], + "waiver_reason": "" + }, + "macos": { + "required": true, + "target_project": "macOS/", + "key_constraints": [], + "files_allowed": ["macOS/"], + "waiver_reason": "" + }, + "windows": { + "required": true, + "target_project": "windows/", + "key_constraints": [], + "files_allowed": ["windows/"], + "waiver_reason": "" + } + } + } + }, + "platforms": { + "android": { + "implementation": { + "agent_id": "android-implementation-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/android-implementation.md", + "artifact": "role-artifacts/android-implementation.json" + }, + "status": "BLOCKED", + "evidence": "Pending Android implementation.", + "summary": "Pending Android changes.", + "output": { + "target_project": "Android/APIExample/", + "query_cases": "Pending.", + "upsert_case": "Pending.", + "files_changed": [], + "matrix_updates": [] + } + }, + "verification": { + "agent_id": "android-verification-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/android-verification.md", + "artifact": "role-artifacts/android-verification.json" + }, + "status": "BLOCKED", + "evidence": "Pending Android verification.", + "summary": "Pending Android review and build.", + "output": { + "result": "BLOCKED", + "findings": [], + "parity_result": "BLOCKED", + "entry_point": "", + "ux_notes": "", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [] + } + } + }, + "ios": { + "implementation": { + "agent_id": "ios-implementation-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/ios-implementation.md", + "artifact": "role-artifacts/ios-implementation.json" + }, + "status": "BLOCKED", + "evidence": "Pending iOS implementation.", + "summary": "Pending iOS changes.", + "output": { + "target_project": "iOS/APIExample/", + "query_cases": "Pending.", + "upsert_case": "Pending.", + "files_changed": [], + "matrix_updates": [] + } + }, + "verification": { + "agent_id": "ios-verification-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/ios-verification.md", + "artifact": "role-artifacts/ios-verification.json" + }, + "status": "BLOCKED", + "evidence": "Pending iOS verification.", + "summary": "Pending iOS review and build.", + "output": { + "result": "BLOCKED", + "findings": [], + "parity_result": "BLOCKED", + "entry_point": "", + "ux_notes": "", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [] + } + } + }, + "macos": { + "implementation": { + "agent_id": "macos-implementation-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/macos-implementation.md", + "artifact": "role-artifacts/macos-implementation.json" + }, + "status": "BLOCKED", + "evidence": "Pending macOS implementation.", + "summary": "Pending macOS changes.", + "output": { + "target_project": "macOS/", + "query_cases": "Pending.", + "upsert_case": "Pending.", + "files_changed": [], + "matrix_updates": [] + } + }, + "verification": { + "agent_id": "macos-verification-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/macos-verification.md", + "artifact": "role-artifacts/macos-verification.json" + }, + "status": "BLOCKED", + "evidence": "Pending macOS verification.", + "summary": "Pending macOS review and build.", + "output": { + "result": "BLOCKED", + "findings": [], + "parity_result": "BLOCKED", + "entry_point": "", + "ux_notes": "", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [] + } + } + }, + "windows": { + "implementation": { + "agent_id": "windows-implementation-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/windows-implementation.md", + "artifact": "role-artifacts/windows-implementation.json" + }, + "status": "BLOCKED", + "evidence": "Pending Windows implementation.", + "summary": "Pending Windows changes.", + "output": { + "target_project": "windows/", + "query_cases": "Pending.", + "upsert_case": "Pending.", + "files_changed": [], + "matrix_updates": [] + } + }, + "verification": { + "agent_id": "windows-verification-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/windows-verification.md", + "artifact": "role-artifacts/windows-verification.json" + }, + "status": "BLOCKED", + "evidence": "Pending Windows verification.", + "summary": "Pending Windows review and MSBuild evidence.", + "output": { + "result": "BLOCKED", + "findings": [], + "parity_result": "BLOCKED", + "entry_point": "", + "ux_notes": "", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [] + } + } + } + }, + "cross_platform_acceptance": { + "result": "BLOCKED", + "evidence": "Pending all required platform verification.", + "differences": [] + }, + "release": { + "required": true, + "target_sdk_versions": { + "android": "", + "ios": "", + "macos": "", + "windows": "" + }, + "checks": [ + { + "name": "sdk-version-android", + "result": "BLOCKED", + "expected_version": "", + "actual_versions": {}, + "evidence": "", + "reason": "Pending live Android SDK version check." + }, + { + "name": "sdk-version-ios", + "result": "BLOCKED", + "expected_version": "", + "actual_versions": {}, + "evidence": "", + "reason": "Pending live iOS SDK version check." + }, + { + "name": "sdk-version-macos", + "result": "BLOCKED", + "expected_version": "", + "actual_versions": {}, + "evidence": "", + "reason": "Pending live macOS SDK version check." + }, + { + "name": "sdk-version-windows", + "result": "BLOCKED", + "expected_version": "", + "actual_versions": {}, + "evidence": "", + "reason": "Pending live Windows SDK version check." + } + ], + "skipped_checks": [] + }, + "knowledge_updates": [] +} diff --git a/docs/ai-engineering/templates/release-dry-run-template.md b/docs/ai-engineering/templates/release-dry-run-template.md new file mode 100644 index 000000000..0439c82cc --- /dev/null +++ b/docs/ai-engineering/templates/release-dry-run-template.md @@ -0,0 +1,131 @@ +# Release Dry Run Report Template + +Use this template only when a release dry run is intentionally selected as a repository-level evidence snapshot. Routine execution results should stay in the agent response, PR discussion, CI artifact, or task tracker. + +Do not fill this template with stale values from an older run. Re-check the live repository and target release version before completing every section. + +## Scope + +- Repository: +- Branch: +- Commit: +- Target release SDK version: +- Product scenario: +- Target platform roots: +- Verification budget: +- Non-goals: + +## Sources Used + +- `AGENTS.md` +- `.agent/skills/api-example-release-iteration/SKILL.md` +- `docs/ai-engineering/knowledge-index.md` +- `docs/ai-engineering/repository-profile.json` +- `docs/ai-engineering/release-iteration-gate.md` +- `docs/ai-engineering/release-known-issues.md` +- Platform `AGENTS.md` files: +- Platform SDK version files: +- Platform build scripts: + +## Findings + +### Product Gate + +Status: + +Evidence: + +### Architecture Gate + +Status: + +Evidence: + +### Repository Release Gate + +Status: + +| Gate | Result | Evidence | +| --- | --- | --- | +| SDK version consistency | | | +| Android Extension include freshness | | | +| Third-party beauty license validity | | | +| Repository build checks | | | + +### Static Version Snapshot + +| Platform Project | Version Source | Observed Version | +| --- | --- | --- | +| `Android/APIExample` | `gradle.properties` | | +| `Android/APIExample-Audio` | `gradle.properties` | | +| `Android/APIExample-Compose` | `gradle.properties` | | +| `iOS/APIExample` | `Podfile` | | +| `iOS/APIExample-Audio` | `Podfile` | | +| `iOS/APIExample-OC` | `Podfile` | | +| `iOS/APIExample-SwiftUI` | `Podfile` | | +| `macOS` | `Podfile` | | +| `windows/APIExample` | SDK package or install script | | + +### Script Checks + +Commands: + +```bash + +``` + +Result: + +Not covered: + +- + +### Existing Automation Found + +| Area | Evidence | +| --- | --- | +| SDK version validation | | +| Build script preflight | | +| Signing visibility | | +| Windows packaging preflight | | + +## Automation Gaps + +1. + +## Acceptance Summary + +Product: +- Scenario: +- Target: +- Non-goals: + +Architecture: +- Platform/project: +- Key constraints: + +Implementation: +- Files changed: +- Skills/docs used: + +Review: +- Result: +- Findings: + +Testing: +- Commands: +- Result: +- Skipped checks: + +Release: +- Required: +- Target SDK versions by platform: +- Checks: +- Skipped checks: +- External handoff: Jenkins packaging and QA + +UX: +- Entry point: +- Notes: + +Final status: PASS | PASS WITH RISKS | BLOCKED diff --git a/docs/ai-engineering/tools/README.md b/docs/ai-engineering/tools/README.md new file mode 100644 index 000000000..d6127c98a --- /dev/null +++ b/docs/ai-engineering/tools/README.md @@ -0,0 +1,106 @@ +# AI Engineering Tools + +`orchestrate_case_execution.py` turns one RTC requirement into a shared Contract plus attributed Android, iOS, macOS, and Windows delivery. It launches independent, replayable `codex exec` sessions rather than parent-managed Codex subagent threads. + +## Role Types + +| Phase | Agents | Profile | +| --- | --- | --- | +| `contract` | One shared Contract | `standard` | +| `implementation` | Four platform Implementation agents | `deep` | +| `verification` | Four independent platform Verification agents | `review` | + +Provider model names remain runtime inputs through `--model` or `CODEX_MODEL_STANDARD`, `CODEX_MODEL_DEEP`, and `CODEX_MODEL_REVIEW`. + +## Run A Requirement + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ + --matrix docs/ai-engineering/case-maintenance-matrix.md \ + --feature "Join channel audio" \ + --target-sdk-version "4.6.2" \ + --platform-sdk-version "android=4.6.3" \ + --run-dir /tmp/api-example-requirement +``` + +For a new feature outside the matrix backlog, add `--sdk-family "Full RTC" --key-api ""`; repeat `--key-api` for multiple APIs. + +`--target-sdk-version` is a baseline expanded to Android, iOS, macOS, and Windows. Repeat `--platform-sdk-version platform=x.y.z` for any platform on a different SDK line. For example, the command above resolves to Android 4.6.3 and iOS/macOS/Windows 4.6.2. The final mapping is stored in the execution package and manifest; platform release cadence changes do not require edits to Python or the repository profile. + +The default `docs/ai-engineering/repository-profile.json` defines repository-specific SDK version sources. Shared tools support Gradle property, CocoaPods package, and SDK archive-name sources without hard-coding a distribution in Python. `init` accepts `--repository-profile` for a checked-in alternative and binds its path and SHA-256 into the execution package. + +Dispatch Contract, then the two platform phases: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement --phase contract --model "" + +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement --phase implementation --model "" + +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement --phase verification --model "" +``` + +Omitting `--platform` covers Android, iOS, macOS, and Windows. Implementation runs are serialized in a shared checkout and reconciled before the next agent starts; Verification runs execute concurrently. Use `--platform windows` for one platform. Use `--retry` only to replace a prior `FAIL` or `BLOCKED` artifact; an Implementation retry automatically invalidates that platform's old Verification. + +Each run starts in the Contract-selected target project, writes independent stdout/stderr logs, records its host platform, and has a 900-second default timeout. Input snapshots bind the execution package, current repository state, routing config, repository profile, and dependency artifact hashes. This lets Codex load nested platform/project `AGENTS.md` files automatically. `--dry-run` resolves prompts, content-aware snapshots, models, working directories, and commands without starting Codex. + +Verification PASS/FAIL commands are accepted only when their exact command string and exit code exist in the hashed Codex JSONL log. Build commands must execute a real target-platform build action from the Contract working directory; Windows build `PASS` additionally requires a Windows host. Contract and Verification may create ignored build output but fail if they modify tracked or untracked repository content. Implementation `files_changed` is derived from the real per-run repository delta rather than trusted from model output. + +Assemble with an explicit cross-platform conclusion: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py assemble \ + --run-dir /tmp/api-example-requirement \ + --matrix docs/ai-engineering/case-maintenance-matrix.md \ + --final-status "BLOCKED" \ + --cross-platform-result "BLOCKED" \ + --cross-platform-evidence "Windows-host verification pending" +``` + +Manifest v4 stores one shared Contract and one Implementation/Verification pair per official platform. Implementation retries preserve attempt history and expose one cumulative net delta whose file list must match the manifest. Matrix updates are applied only after structural and evidence-file validation and never for final `BLOCKED`. Assembly also refreshes all live platform SDK dependency versions. + +Assembly does not accept CI job URLs, package artifact URLs, or QA metadata. Jenkins packaging, QA validation, artifact distribution, and website publication are external handoff processes rather than repository acceptance gates. + +## macOS And Windows + +Windows Verification on macOS is static review only. It must not download Windows SDKs, emulate, cross-compile, or treat another compiler as MSBuild evidence. Keep the Windows artifact `BLOCKED`, then replace it from a real Windows host with: + +```bash +python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ + --run-dir /tmp/api-example-requirement \ + --phase verification \ + --platform windows \ + --retry \ + --model "" +``` + +## Supporting Tools + +- `generate_case_backlog.py`: reads `MISSING`/`PARTIAL` matrix cells and prioritizes requirement candidates. +- `prepare_case_execution.py`: creates a v4 requirement package without starting Codex. +- `validate_acceptance_manifest.py`: validates a filled v4 manifest. + +```bash +python3 docs/ai-engineering/tools/prepare_case_execution.py \ + --feature "Join channel audio" \ + --target-sdk-version "4.6.2" \ + --platform-sdk-version "android=4.6.3" \ + --repository-profile docs/ai-engineering/repository-profile.json +python3 docs/ai-engineering/tools/validate_acceptance_manifest.py +``` + +The standalone validator resolves evidence paths relative to the manifest and recomputes prompt, role-artifact, input-snapshot, command-log, and repository-delta hashes. + +Routine manifests and dispatch logs stay outside the repository unless the user requests an evidence snapshot. + +## Tests + +```bash +PYTHONDONTWRITEBYTECODE=1 python3 -m unittest \ + docs/ai-engineering/tools/validate_acceptance_manifest_test.py \ + docs/ai-engineering/tools/generate_case_backlog_test.py \ + docs/ai-engineering/tools/prepare_case_execution_test.py \ + docs/ai-engineering/tools/orchestrate_case_execution_test.py +``` diff --git a/docs/ai-engineering/tools/generate_case_backlog.py b/docs/ai-engineering/tools/generate_case_backlog.py new file mode 100644 index 000000000..b7aedeca6 --- /dev/null +++ b/docs/ai-engineering/tools/generate_case_backlog.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Generate platform execution units from the case-maintenance matrix.""" + +import argparse +import json +import re +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_MATRIX = REPO_ROOT / "docs/ai-engineering/case-maintenance-matrix.md" +NON_PLATFORM_COLUMNS = {"Feature", "SDK Family", "Key APIs", "Notes"} +PLATFORM_PROJECTS = { + "Android full": "Android/APIExample/", + "Android audio": "Android/APIExample-Audio/", + "Android Compose": "Android/APIExample-Compose/", + "iOS UIKit": "iOS/APIExample/", + "iOS SwiftUI": "iOS/APIExample-SwiftUI/", + "iOS Objective-C": "iOS/APIExample-OC/", + "iOS audio": "iOS/APIExample-Audio/", + "macOS": "macOS/", + "Windows": "windows/", +} +SEVERITY_PRIORITY = { + "High": 10, + "Medium": 20, + "Low": 30, + "Unspecified": 90, +} +STATUS_PRIORITY_OFFSET = { + "MISSING": 0, + "PARTIAL": 5, +} +CONFIRMED_GAP_ALIASES = { + "Basic audio-only join channel": ["Join channel audio"], +} + + +def split_markdown_row(line): + return [cell.strip() for cell in line.strip().strip("|").split("|")] + + +def strip_code(value): + value = value.strip() + if value.startswith("`") and value.endswith("`"): + return value[1:-1] + return value + + +def parse_status(cell): + status, detail = parse_matrix_cell(cell) + if status in {"MISSING", "PARTIAL"}: + return status, detail + return "", "" + + +def parse_matrix_cell(cell): + value = strip_code(cell) + if value in {"MISSING", "UNKNOWN"}: + return value, "" + match = re.fullmatch(r"(DONE|PARTIAL|N/A)\((.+)\)", value) + if match: + return match.group(1), match.group(2) + return "", "" + + +def parse_done_path(cell): + status, detail = parse_matrix_cell(cell) + return detail if status == "DONE" else "" + + +def parse_key_apis(cell): + apis = re.findall(r"`([^`]+)`", cell) + if apis: + return [api.strip() for api in apis] + return [part.strip() for part in cell.split(",") if part.strip()] + + +def find_matrix_table(lines): + for index, line in enumerate(lines): + if not line.startswith("|"): + continue + header = split_markdown_row(line) + if "Feature" in header and "Key APIs" in header: + rows = [] + for row_line in lines[index + 2 :]: + if not row_line.startswith("|"): + break + rows.append(split_markdown_row(row_line)) + return header, rows + raise ValueError("could not find pilot matrix table with Feature and Key APIs columns") + + +def find_confirmed_gap_table(lines): + for index, line in enumerate(lines): + if not line.startswith("|"): + continue + header = split_markdown_row(line) + if {"Gap", "Affected Units", "Severity"}.issubset(set(header)): + rows = [] + for row_line in lines[index + 2 :]: + if not row_line.startswith("|"): + break + rows.append(split_markdown_row(row_line)) + return header, rows + return [], [] + + +def parse_severity(value): + label = re.split(r"\s+[-—]\s+", value.strip(), maxsplit=1)[0].strip() + return label if label in SEVERITY_PRIORITY else "Unspecified" + + +def gap_names_for_lookup(gap_name): + return [gap_name, *CONFIRMED_GAP_ALIASES.get(gap_name, [])] + + +def build_gap_severity_map(lines): + header, rows = find_confirmed_gap_table(lines) + if not header: + return {} + gap_index = header.index("Gap") + affected_index = header.index("Affected Units") + severity_index = header.index("Severity") + mapping = {} + for row in rows: + if len(row) != len(header): + continue + severity = parse_severity(row[severity_index]) + affected_units = [unit.strip() for unit in row[affected_index].split(",") if unit.strip()] + for unit in affected_units: + for gap_name in gap_names_for_lookup(row[gap_index]): + mapping[(gap_name, unit)] = severity + return mapping + + +def find_reference_candidates(header, row, target_platform): + candidates = [] + for column_index, column_name in enumerate(header): + if column_name in NON_PLATFORM_COLUMNS or column_name == target_platform: + continue + if column_name not in PLATFORM_PROJECTS: + continue + done_path = parse_done_path(row[column_index]) + if not done_path: + continue + candidates.append( + { + "platform_unit": column_name, + "project": PLATFORM_PROJECTS[column_name], + "path": done_path, + } + ) + return candidates + + +def generate_execution_units(matrix_path): + lines = matrix_path.read_text(encoding="utf-8").splitlines() + header, rows = find_matrix_table(lines) + gap_severities = build_gap_severity_map(lines) + feature_index = header.index("Feature") + sdk_index = header.index("SDK Family") + key_api_index = header.index("Key APIs") + notes_index = header.index("Notes") if "Notes" in header else None + execution_units = [] + errors = [] + + for row in rows: + if len(row) != len(header): + errors.append(f"row has {len(row)} cells but header has {len(header)} cells: {row}") + continue + for column_index, column_name in enumerate(header): + if column_name in NON_PLATFORM_COLUMNS: + continue + status, status_note = parse_status(row[column_index]) + if not status: + continue + if column_name not in PLATFORM_PROJECTS: + errors.append(f"unknown platform column with actionable status: {column_name}") + continue + severity = gap_severities.get((row[feature_index], column_name), "Unspecified") + priority = SEVERITY_PRIORITY[severity] + STATUS_PRIORITY_OFFSET[status] + execution_units.append( + { + "feature": row[feature_index], + "sdk_family": row[sdk_index], + "key_apis": parse_key_apis(row[key_api_index]), + "platform_unit": column_name, + "target_project": PLATFORM_PROJECTS[column_name], + "status": status, + "status_note": status_note, + "severity": severity, + "priority": priority, + "reference_candidates": find_reference_candidates(header, row, column_name), + "notes": row[notes_index] if notes_index is not None else "", + } + ) + + if errors: + raise ValueError("\n".join(errors)) + execution_units.sort(key=lambda unit: (unit["priority"], unit["feature"], unit["platform_unit"])) + return {"execution_units": execution_units} + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--matrix", default=str(DEFAULT_MATRIX), help="Path to case-maintenance-matrix.md") + args = parser.parse_args(argv) + + try: + payload = generate_execution_units(Path(args.matrix)) + except (OSError, ValueError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + print(json.dumps(payload, indent=2, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/ai-engineering/tools/generate_case_backlog_test.py b/docs/ai-engineering/tools/generate_case_backlog_test.py new file mode 100644 index 000000000..447190e15 --- /dev/null +++ b/docs/ai-engineering/tools/generate_case_backlog_test.py @@ -0,0 +1,124 @@ +import json +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +GENERATOR = REPO_ROOT / "docs/ai-engineering/tools/generate_case_backlog.py" + + +class CaseBacklogGeneratorTest(unittest.TestCase): + def run_generator(self, matrix_text): + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as handle: + handle.write(matrix_text) + matrix_path = handle.name + try: + return subprocess.run( + [sys.executable, str(GENERATOR), "--matrix", matrix_path], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + finally: + Path(matrix_path).unlink(missing_ok=True) + + def test_generates_execution_units_for_missing_and_partial_cells(self): + matrix = textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Android full | Android Compose | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | + | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `DONE(samples/JoinChannelAudio.kt)` | `MISSING` | Windows has no basic audio-only join case. | + | Media metadata | Full RTC | `registerMediaMetadataObserver` | `DONE(advanced/MediaMetadata.java)` | `DONE(samples/MediaMetadata.kt)` | `PARTIAL(Advanced/Metadata; smoke pending)` | Runtime metadata smoke pending. | + | Audio mixing | Full RTC | `startAudioMixing` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(samples/PlayAudioFiles.kt)` | `DONE(Advanced/AudioMixing/)` | Full coverage. | + + ## Confirmed Gaps + + | Gap | Affected Units | Severity | + | --- | --- | --- | + | Basic audio-only join channel | Windows | High - missing foundational case | + """ + ) + + result = self.run_generator(matrix) + + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertEqual(len(payload["execution_units"]), 2) + self.assertEqual(payload["execution_units"][0]["feature"], "Join channel audio") + self.assertEqual(payload["execution_units"][0]["platform_unit"], "Windows") + self.assertEqual(payload["execution_units"][0]["target_project"], "windows/") + self.assertEqual(payload["execution_units"][0]["status"], "MISSING") + self.assertEqual( + payload["execution_units"][0]["key_apis"], + ["joinChannel", "setAudioProfile"], + ) + self.assertEqual(payload["execution_units"][0]["severity"], "High") + self.assertEqual(payload["execution_units"][0]["priority"], 10) + self.assertEqual( + payload["execution_units"][0]["reference_candidates"], + [ + { + "platform_unit": "Android full", + "project": "Android/APIExample/", + "path": "basic/JoinChannelAudio.java", + }, + { + "platform_unit": "Android Compose", + "project": "Android/APIExample-Compose/", + "path": "samples/JoinChannelAudio.kt", + } + ], + ) + self.assertEqual(payload["execution_units"][1]["platform_unit"], "Windows") + self.assertEqual(payload["execution_units"][1]["status"], "PARTIAL") + self.assertEqual(payload["execution_units"][1]["status_note"], "Advanced/Metadata; smoke pending") + self.assertEqual(payload["execution_units"][1]["severity"], "Unspecified") + self.assertEqual(payload["execution_units"][1]["priority"], 95) + + def test_unknown_platform_column_fails(self): + matrix = textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Unknown Platform | Notes | + | --- | --- | --- | --- | --- | + | Feature A | Full RTC | `apiA` | `MISSING` | Missing. | + """ + ) + + result = self.run_generator(matrix) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("Unknown Platform", result.stderr) + + def test_confirmed_gap_alias_applies_priority(self): + matrix = textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Android full | Windows | Notes | + | --- | --- | --- | --- | --- | --- | + | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `MISSING` | Windows has no basic audio-only join case. | + + ## Confirmed Gaps + + | Gap | Affected Units | Severity | + | --- | --- | --- | + | Basic audio-only join channel | Windows | High - missing foundational case | + """ + ) + + result = self.run_generator(matrix) + + self.assertEqual(result.returncode, 0, result.stderr) + unit = json.loads(result.stdout)["execution_units"][0] + self.assertEqual(unit["feature"], "Join channel audio") + self.assertEqual(unit["platform_unit"], "Windows") + self.assertEqual(unit["severity"], "High") + self.assertEqual(unit["priority"], 10) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/ai-engineering/tools/orchestrate_case_execution.py b/docs/ai-engineering/tools/orchestrate_case_execution.py new file mode 100644 index 000000000..5429945d2 --- /dev/null +++ b/docs/ai-engineering/tools/orchestrate_case_execution.py @@ -0,0 +1,1594 @@ +#!/usr/bin/env python3 +"""Run one cross-platform API Examples requirement workflow.""" + +import argparse +import concurrent.futures +import hashlib +import json +import os +import signal +import stat +import subprocess +import sys +from pathlib import Path + +from generate_case_backlog import parse_matrix_cell, split_markdown_row +from prepare_case_execution import ( + DEFAULT_REPOSITORY_PROFILE, + PLATFORMS, + collect_sdk_version_checks, + load_repository_profile, + prepare_case_execution, +) +from validate_acceptance_manifest import ( + PLATFORM_ROOTS, + is_platform_build_command, + path_is_within, + validate_contract, + validate_evidence_files, + validate_manifest, + validate_platform_implementation, + validate_platform_verification, +) + + +ROLE_ORDER = ["contract", "implementation", "verification"] +PHASE_ORDER = ["contract", "implementation", "verification"] +REPO_ROOT = Path(__file__).resolve().parents[3] +DEFAULT_ROUTING_CONFIG = REPO_ROOT / "docs/ai-engineering/role-routing.json" +COMPLETED_DEPENDENCY_STATUSES = {"PASS", "WAIVED"} +RESULT_VALUES = ["PASS", "FAIL", "BLOCKED", "SKIPPED"] +ROLE_STATUS_VALUES = ["PASS", "FAIL", "BLOCKED", "WAIVED"] + +STRING = {"type": "string"} +STRING_LIST = {"type": "array", "items": STRING} +RESULT = {"type": "string", "enum": RESULT_VALUES} +REFERENCE = { + "type": "object", + "additionalProperties": False, + "required": ["required", "source_case", "contract_result"], + "properties": { + "required": {"type": "boolean"}, + "source_case": STRING, + "contract_result": RESULT, + }, +} +PLATFORM_TARGET = { + "type": "object", + "additionalProperties": False, + "required": [ + "required", + "target_project", + "key_constraints", + "files_allowed", + "waiver_reason", + ], + "properties": { + "required": {"type": "boolean"}, + "target_project": STRING, + "key_constraints": STRING_LIST, + "files_allowed": STRING_LIST, + "waiver_reason": STRING, + }, +} +PLATFORM_TARGETS = { + "type": "object", + "additionalProperties": False, + "required": PLATFORMS, + "properties": {platform: PLATFORM_TARGET for platform in PLATFORMS}, +} +MATRIX_UPDATE = { + "type": "object", + "additionalProperties": False, + "required": ["feature", "platform_unit", "from", "to", "to_cell", "evidence"], + "properties": { + "feature": STRING, + "platform_unit": STRING, + "from": STRING, + "to": STRING, + "to_cell": STRING, + "evidence": STRING, + }, +} +COMMAND_LIST = { + "type": "array", + "items": { + "type": "object", + "additionalProperties": False, + "required": ["kind", "command", "result"], + "properties": { + "kind": {"type": "string", "enum": ["build", "test", "static", "runtime"]}, + "command": STRING, + "result": RESULT, + }, + }, +} +SKIPPED_CHECK_LIST = { + "type": "array", + "items": { + "type": "object", + "additionalProperties": False, + "required": ["name", "reason"], + "properties": {"name": STRING, "reason": STRING}, + }, +} +ROLE_OUTPUT_PROPERTIES = { + "contract": { + "scenario": STRING, + "key_apis": STRING_LIST, + "non_goals": STRING_LIST, + "reference": REFERENCE, + "cross_platform_requirements": STRING_LIST, + "platform_targets": PLATFORM_TARGETS, + }, + "implementation": { + "target_project": STRING, + "query_cases": STRING, + "upsert_case": STRING, + "files_changed": STRING_LIST, + "matrix_updates": {"type": "array", "items": MATRIX_UPDATE}, + }, + "verification": { + "result": RESULT, + "findings": STRING_LIST, + "parity_result": RESULT, + "entry_point": STRING, + "ux_notes": STRING, + "commands": COMMAND_LIST, + "build_result": RESULT, + "skipped_checks": SKIPPED_CHECK_LIST, + }, +} + + +def write_json(path, payload): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + + +def read_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def load_role_routing(path): + routing = read_json(path) + if routing.get("version") != 1: + raise ValueError("role routing version must be 1") + profiles = routing.get("profiles") + roles = routing.get("roles") + phases = routing.get("phases") + if not isinstance(profiles, dict) or not profiles: + raise ValueError("role routing profiles must be a non-empty object") + if not isinstance(roles, dict) or set(roles) != set(ROLE_ORDER): + raise ValueError(f"role routing must define exactly: {', '.join(ROLE_ORDER)}") + if phases != PHASE_ORDER: + raise ValueError(f"role routing phases must be: {', '.join(PHASE_ORDER)}") + for role, route in roles.items(): + if route.get("phase") != role: + raise ValueError(f"role routing {role}.phase must be {role}") + profile = route.get("profile") + if profile not in profiles: + raise ValueError(f"role routing {role}.profile={profile} is not defined") + if "reasoning_effort" not in profiles[profile]: + raise ValueError(f"role routing profile {profile} needs reasoning_effort") + return routing + + +def init_workspace(args): + run_dir = Path(args.run_dir).resolve() + run_dir.mkdir(parents=True, exist_ok=True) + matrix_path = Path(args.matrix).resolve() + routing_path = Path(args.routing_config).resolve() + profile_path = Path(args.repository_profile).resolve() + routing = load_role_routing(routing_path) + load_repository_profile(profile_path) + package = prepare_case_execution( + matrix_path, + args.feature, + index=args.index, + sdk_family=args.sdk_family, + key_apis=args.key_api, + target_sdk_version=args.target_sdk_version, + platform_sdk_versions=args.platform_sdk_version, + repository_profile=profile_path, + ) + package["matrix_path"] = stable_matrix_path(matrix_path) + package["routing_config"] = repo_relative_path(routing_path) + package["routing_config_sha256"] = sha256_file(routing_path) + package["repository_profile"] = repo_relative_path(profile_path) + package["repository_profile_sha256"] = sha256_file(profile_path) + write_json(run_dir / "execution-package.json", package) + write_json(run_dir / "acceptance-manifest.json", package["acceptance_manifest_seed"]) + + contract = package["acceptance_manifest_seed"]["contract"] + write_artifact_seed(run_dir, "contract", "contract", None, contract) + write_role_schema(run_dir / "role-schemas/contract.json", "contract") + write_prompt(run_dir, "contract", "contract", None, package, routing, {}) + for platform in PLATFORMS: + unit = package["acceptance_manifest_seed"]["platforms"][platform] + for role in ["implementation", "verification"]: + name = artifact_name(role, platform) + write_artifact_seed(run_dir, name, role, platform, unit[role]) + write_role_schema(run_dir / "role-schemas" / f"{name}.json", role) + write_prompt(run_dir, name, role, platform, package, routing, {}) + print(f"Execution workspace initialized: {run_dir}") + return 0 + + +def write_artifact_seed(run_dir, name, role, platform, artifact): + payload = {"name": name, "role": role, **artifact} + if platform: + payload["platform"] = platform + write_json(run_dir / "role-artifacts" / f"{name}.json", payload) + + +def write_role_schema(path, role): + output_properties = ROLE_OUTPUT_PROPERTIES[role] + schema = { + "type": "object", + "additionalProperties": False, + "required": ["status", "evidence", "summary", "output"], + "properties": { + "status": {"type": "string", "enum": ROLE_STATUS_VALUES}, + "evidence": STRING, + "summary": STRING, + "waiver_reason": STRING, + "output": { + "type": "object", + "additionalProperties": False, + "required": list(output_properties), + "properties": output_properties, + }, + }, + } + write_json(path, schema) + + +def write_prompt(run_dir, name, role, platform, package, routing, dependencies): + requirement = package["requirement"] + target = "all official platforms" + if platform: + target = resolved_platform_target(package, platform, dependencies) + host_note = host_constraint(platform) if role == "verification" else "" + waiver_note = "" + if platform: + waiver_note = ( + "If Contract marks this platform required=false, do not modify or verify it. " + "Return status=WAIVED, copy Contract's waiver_reason into the top-level " + "waiver_reason field, use SKIPPED for Verification result/parity_result/build_result, " + "and report no executed build command." + ) + body = f"""# {name} Agent Task + +Requirement: {requirement['feature']} +SDK family: {requirement['sdk_family']} +Key APIs: {', '.join(requirement['key_apis'])} +Target SDK versions: +{json.dumps(requirement['target_sdk_versions'], indent=2, ensure_ascii=False)} +Role: {role} +Platform: {platform or 'shared'} +Target: {target} +Model profile: {routing['roles'][role]['profile']} + +Required output: +{json.dumps(package['role_contracts'][role], indent=2, ensure_ascii=False)} + +Dependency artifacts: +{format_dependency_context(dependencies)} + +The dispatcher starts platform work in the Contract-selected target so nested AGENTS.md files load automatically. Confirm those instructions before acting. Keep platform source changes inside the Contract target and never share source files across platform roots. + +{host_note} + +For Verification, report every executed check with kind=build|test|static|runtime and copy the command string exactly from Codex command telemetry. The dispatcher binds the declared result to the JSONL exit code. + +{waiver_note} + +Return only the JSON object required by the supplied output schema. The dispatcher records identity and provenance. +""" + path = run_dir / "role-prompts" / f"{name}.md" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(body, encoding="utf-8") + return path + + +def host_constraint(platform): + if platform == "windows" and sys.platform != "win32": + host = "macOS" if sys.platform == "darwin" else sys.platform + return ( + f"This host is {host}. {host} cannot provide Windows build evidence. " + "Do not download Windows SDK archives, emulate Windows, cross-compile, or use a " + "substitute compiler as platform evidence. Run repository-local static checks only " + "and report unavailable Windows build/runtime evidence as BLOCKED." + ) + return "" + + +def resolved_platform_target(package, platform, dependencies): + contract = dependencies.get("contract", {}) + output = contract.get("output", {}) if isinstance(contract, dict) else {} + targets = output.get("platform_targets", {}) if isinstance(output, dict) else {} + if platform in targets and targets[platform].get("target_project"): + return targets[platform]["target_project"] + return package["acceptance_manifest_seed"]["contract"]["output"]["platform_targets"][platform][ + "target_project" + ] + + +def format_dependency_context(dependencies): + if not dependencies: + return "None." + sections = [] + for name, artifact in dependencies.items(): + payload = { + "status": artifact.get("status"), + "summary": artifact.get("summary"), + "output": artifact.get("output"), + } + sections.append(f"- {name}:\n```json\n{json.dumps(payload, indent=2, ensure_ascii=False)}\n```") + return "\n".join(sections) + + +def dispatch_workspace(args): + run_dir = Path(args.run_dir).resolve() + package = read_json(run_dir / "execution-package.json") + routing_path = Path(args.routing_config).resolve() + routing = load_role_routing(routing_path) + validate_execution_configuration(package, routing_path) + specs = dispatch_specs(run_dir, args.phase, args.platform, args.retry) + route = routing["roles"][args.phase] + profile = route["profile"] + model = resolve_model(profile, args.model) + codex_version = "dry-run" if args.dry_run else read_codex_version(args.codex_bin) + tasks = [] + for spec in specs: + dependencies = dependencies_for_spec(run_dir, spec) + working_directory = resolve_working_directory(package, spec, dependencies) + resolved = { + "version": 1, + "routing_config": repo_relative_path(routing_path), + "routing_config_sha256": sha256_file(routing_path), + "phase": args.phase, + "name": spec["name"], + "platform": spec["platform"], + "profile": profile, + "model": model, + "reasoning_effort": routing["profiles"][profile]["reasoning_effort"], + "codex_version": codex_version, + "working_directory": repo_relative_path(working_directory), + } + write_json(run_dir / "resolved-role-routing" / f"{spec['name']}.json", resolved) + snapshot_path, snapshot_sha256 = write_input_snapshot( + run_dir, args.phase, spec["name"], dependencies, resolved + ) + input_snapshot = read_json(snapshot_path) + expected_repository_state = input_snapshot["repository_state"] + prompt_path = write_prompt( + run_dir, spec["name"], spec["role"], spec["platform"], package, routing, dependencies + ) + schema_path = run_dir / "role-schemas" / f"{spec['name']}.json" + write_role_schema(schema_path, spec["role"]) + raw_result_path = run_dir / "raw-results" / f"{spec['name']}.json" + command = build_codex_command( + args.codex_bin, + route, + model, + routing["profiles"][profile]["reasoning_effort"], + schema_path, + raw_result_path, + working_directory, + ) + tasks.append( + { + **spec, + "command": command, + "prompt_path": prompt_path, + "artifact_path": run_dir / "role-artifacts" / f"{spec['name']}.json", + "raw_result_path": raw_result_path, + "log_path": run_dir / "dispatch-logs" / f"{spec['name']}.jsonl", + "stderr_path": run_dir / "dispatch-logs" / f"{spec['name']}.stderr.log", + "model_profile": profile, + "model": model, + "reasoning_effort": routing["profiles"][profile]["reasoning_effort"], + "codex_version": codex_version, + "working_directory": working_directory, + "working_directory_manifest": repo_relative_path(working_directory), + "input_snapshot": run_relative_path(snapshot_path, run_dir), + "input_snapshot_sha256": snapshot_sha256, + "expected_repository_state": expected_repository_state, + "snapshot_dependencies": dependencies, + "resolved_routing": resolved, + "previous_files_changed": ( + read_json(run_dir / "role-artifacts" / f"{spec['name']}.json") + .get("output", {}) + .get("files_changed", []) + if spec["role"] == "implementation" + else [] + ), + "timeout_seconds": args.timeout_seconds, + } + ) + if args.dry_run: + print( + json.dumps( + { + "phase": args.phase, + "commands": [ + { + "name": task["name"], + "platform": task["platform"], + "command": task["command"], + } + for task in tasks + ], + }, + indent=2, + ensure_ascii=False, + ) + ) + return 0 + if args.phase == "implementation": + for task in tasks: + invalidate_platform_verification(run_dir, package, task["platform"]) + run_implementation_tasks(run_dir, tasks) + elif args.phase == "verification": + run_verification_tasks(run_dir, tasks) + else: + run_tasks(tasks) + if args.phase == "contract": + validate_completed_contract(run_dir) + print(f"Codex phase completed: {args.phase} ({', '.join(task['name'] for task in tasks)})") + return 0 + + +def dispatch_specs(run_dir, phase, requested_platforms, retry): + if phase == "contract": + if requested_platforms: + raise ValueError("--platform is not valid for the shared contract phase") + candidates = [{"name": "contract", "role": "contract", "platform": None}] + else: + platforms = requested_platforms or PLATFORMS + candidates = [ + {"name": artifact_name(phase, platform), "role": phase, "platform": platform} + for platform in platforms + ] + selected = [] + for spec in candidates: + path = run_dir / "role-artifacts" / f"{spec['name']}.json" + if not path.exists(): + raise ValueError(f"missing role artifact: {spec['name']}") + artifact = read_json(path) + already_dispatched = artifact.get("dispatch", {}).get("mode") == "codex-exec" + status = artifact.get("status") + if already_dispatched: + if retry and status in {"FAIL", "BLOCKED"}: + selected.append(spec) + elif requested_platforms or phase == "contract": + raise ValueError(f"role {spec['name']} has already been dispatched with status {status}") + continue + selected.append(spec) + if not selected: + raise ValueError(f"all {phase} role artifacts are already complete") + return selected + + +def validate_completed_contract(run_dir): + artifact_path = run_dir / "role-artifacts/contract.json" + artifact = read_json(artifact_path) + if artifact.get("status") != "PASS": + if artifact.get("status") not in {"FAIL", "BLOCKED"}: + artifact["status"] = "BLOCKED" + artifact["evidence"] = "Contract is required and cannot be waived." + artifact["summary"] = "Contract requires correction before platform implementation." + write_json(artifact_path, artifact) + raise ValueError(f"Contract gate returned status {artifact.get('status')}") + manifest = read_json(run_dir / "acceptance-manifest.json") + manifest["contract"] = strip_artifact_metadata(artifact) + errors = [] + validate_contract(manifest, errors, {}) + if not errors: + return + artifact["status"] = "BLOCKED" + artifact["evidence"] = "Contract semantic gate failed: " + "; ".join(errors) + artifact["summary"] = "Contract requires correction before platform implementation." + write_json(artifact_path, artifact) + raise ValueError("Contract semantic gate failed: " + "; ".join(errors)) + + +def dependencies_for_spec(run_dir, spec): + dependency_names = [] + if spec["role"] == "implementation": + dependency_names = ["contract"] + elif spec["role"] == "verification": + dependency_names = ["contract", artifact_name("implementation", spec["platform"])] + dependencies = {} + for name in dependency_names: + if name == "contract": + validate_completed_contract(run_dir) + if name.endswith("-implementation"): + validate_completed_implementation(run_dir, spec["platform"]) + path = run_dir / "role-artifacts" / f"{name}.json" + if not path.exists(): + raise ValueError(f"role {spec['name']} requires missing dependency artifact: {name}") + artifact = read_json(path) + status = artifact.get("status") + allowed_statuses = {"PASS"} if name == "contract" else COMPLETED_DEPENDENCY_STATUSES + if status not in allowed_statuses: + raise ValueError( + f"role {spec['name']} requires {name} status " + f"{'PASS' if name == 'contract' else 'PASS or WAIVED'}, got {status}" + ) + dependencies[name] = artifact + return dependencies + + +def invalidate_platform_verification(run_dir, package, platform): + seed = package["acceptance_manifest_seed"]["platforms"][platform]["verification"] + name = artifact_name("verification", platform) + write_artifact_seed(run_dir, name, "verification", platform, seed) + + +def resolve_working_directory(package, spec, dependencies): + if not spec["platform"]: + return REPO_ROOT + target = resolved_platform_target(package, spec["platform"], dependencies) + platform_root = PLATFORM_ROOTS[spec["platform"]] + if not path_is_within(target, platform_root): + raise ValueError( + f"{spec['platform']} target must be inside {platform_root}: {target}" + ) + path = (REPO_ROOT / target).resolve() + try: + path.relative_to(REPO_ROOT) + except ValueError as exc: + raise ValueError(f"platform target must be inside the repository: {target}") from exc + if not path.is_dir(): + raise ValueError(f"platform target directory does not exist: {target}") + return path + + +def run_tasks(tasks): + errors = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(tasks), len(PLATFORMS))) as executor: + future_tasks = {executor.submit(run_codex_role, **task_args(task)): task for task in tasks} + for future in concurrent.futures.as_completed(future_tasks): + task = future_tasks[future] + try: + future.result() + except (OSError, ValueError, json.JSONDecodeError) as exc: + errors.append((task["name"], str(exc))) + if errors: + errors.sort() + raise ValueError("; ".join(detail for _, detail in errors)) + + +def run_verification_tasks(run_dir, tasks): + errors = [] + try: + run_tasks(tasks) + except ValueError as exc: + errors.append(str(exc)) + for task in tasks: + try: + validate_completed_verification(run_dir, task["platform"]) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: + errors.append(str(exc)) + if errors: + raise ValueError("; ".join(errors)) + + +def run_implementation_tasks(run_dir, tasks): + errors = [] + for task in tasks: + refresh_implementation_input_snapshot(run_dir, task) + before = collect_repository_fingerprints() + try: + run_codex_role(**task_args(task)) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: + errors.append((task["name"], str(exc))) + try: + reconcile_implementation_changes(run_dir, [task], before) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: + errors.append((task["name"], str(exc))) + try: + validate_completed_implementation(run_dir, task["platform"]) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: + errors.append((task["name"], str(exc))) + if errors: + errors.sort() + raise ValueError("; ".join(detail for _, detail in errors)) + + +def validate_completed_implementation(run_dir, platform): + artifact_path = run_dir / "role-artifacts" / f"{platform}-implementation.json" + artifact = read_json(artifact_path) + contract = read_json(run_dir / "role-artifacts/contract.json") + target = contract.get("output", {}).get("platform_targets", {}).get(platform, {}) + required = bool(target.get("required", True)) + expected_status = "PASS" if required else "WAIVED" + errors = [] + if artifact.get("status") != expected_status: + errors.append( + f"{platform} Implementation gate requires status {expected_status}, " + f"got {artifact.get('status')}" + ) + manifest = read_json(run_dir / "acceptance-manifest.json") + manifest["final_status"] = "PASS" + verification_stub = { + "build_result": "PASS", + "parity_result": "PASS", + "skipped_checks": [], + } + output = artifact.get("output") + if not isinstance(output, dict) or not output: + errors.append(f"{platform} Implementation output must be a non-empty object") + else: + validate_platform_implementation( + platform, + target, + output, + verification_stub, + manifest, + errors, + ) + if not errors: + return + if artifact.get("status") not in {"FAIL", "BLOCKED"}: + artifact["status"] = "BLOCKED" + artifact["evidence"] = "Implementation semantic gate failed: " + "; ".join(errors) + artifact["summary"] = "Implementation requires correction before Verification." + write_json(artifact_path, artifact) + raise ValueError("Implementation semantic gate failed: " + "; ".join(errors)) + + +def validate_completed_verification(run_dir, platform): + artifact_path = run_dir / "role-artifacts" / f"{platform}-verification.json" + artifact = read_json(artifact_path) + contract = read_json(run_dir / "role-artifacts/contract.json") + target = contract.get("output", {}).get("platform_targets", {}).get(platform, {}) + required = bool(target.get("required", True)) + status = artifact.get("status") + if required and status in {"FAIL", "BLOCKED"}: + return + errors = [] + if required and status != "PASS": + errors.append(f"{platform} Verification gate requires PASS, FAIL, or BLOCKED, got {status}") + if not required and status != "WAIVED": + errors.append(f"{platform} Verification gate requires status WAIVED, got {status}") + manifest = read_json(run_dir / "acceptance-manifest.json") + manifest["final_status"] = "PASS WITH RISKS" + output = artifact.get("output") + if not isinstance(output, dict) or not output: + errors.append(f"{platform} Verification output must be a non-empty object") + else: + validate_platform_verification( + platform, + required, + output, + artifact.get("dispatch", {}), + manifest, + errors, + ) + if not errors: + return + if status not in {"FAIL", "BLOCKED"}: + artifact["status"] = "BLOCKED" + artifact["evidence"] = "Verification semantic gate failed: " + "; ".join(errors) + artifact["summary"] = "Verification requires correction before acceptance." + write_json(artifact_path, artifact) + raise ValueError("Verification semantic gate failed: " + "; ".join(errors)) + + +def refresh_implementation_input_snapshot(run_dir, task): + snapshot_path, snapshot_sha256 = write_input_snapshot( + run_dir, + "implementation", + task["name"], + task["snapshot_dependencies"], + task["resolved_routing"], + ) + task["input_snapshot"] = run_relative_path(snapshot_path, run_dir) + task["input_snapshot_sha256"] = snapshot_sha256 + task["expected_repository_state"] = read_json(snapshot_path)["repository_state"] + + +def task_args(task): + allowed = { + "command", + "name", + "role", + "platform", + "prompt_path", + "artifact_path", + "raw_result_path", + "log_path", + "stderr_path", + "model_profile", + "model", + "reasoning_effort", + "codex_version", + "working_directory", + "working_directory_manifest", + "input_snapshot", + "input_snapshot_sha256", + "expected_repository_state", + "timeout_seconds", + } + return {key: value for key, value in task.items() if key in allowed} + + +def resolve_bound_repository_profile(package): + profile_value = package.get("repository_profile") + if not isinstance(profile_value, str) or not profile_value: + raise ValueError("execution package repository profile is missing; re-run init") + profile_path = (REPO_ROOT / profile_value).resolve() + try: + profile_path.relative_to(REPO_ROOT.resolve()) + except ValueError as exc: + raise ValueError("execution package repository profile must be inside the repository") from exc + if package.get("repository_profile_sha256") != sha256_file(profile_path): + raise ValueError("repository profile content changed since init; re-run init") + load_repository_profile(profile_path) + return profile_path + + +def validate_execution_configuration(package, routing_path): + if package.get("routing_config") != repo_relative_path(routing_path): + raise ValueError("routing config path changed since init; re-run init") + if package.get("routing_config_sha256") != sha256_file(routing_path): + raise ValueError("routing config content changed since init; re-run init") + resolve_bound_repository_profile(package) + + +def resolve_model(profile, override): + model = override or os.environ.get(f"CODEX_MODEL_{profile.upper()}") + if not model: + raise ValueError(f"model is required; pass --model or set CODEX_MODEL_{profile.upper()}") + return model + + +def read_codex_version(codex_bin): + try: + result = subprocess.run( + [codex_bin, "--version"], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=10, + ) + except subprocess.TimeoutExpired as exc: + raise ValueError("timed out while reading Codex version") from exc + if result.returncode != 0: + raise ValueError(f"failed to read Codex version: {result.stderr.strip()}") + if not result.stdout.strip(): + raise ValueError("Codex version output is empty") + return result.stdout.strip() + + +def write_input_snapshot(run_dir, phase, name, dependencies, resolved_routing): + package = read_json(run_dir / "execution-package.json") + dependency_hashes = { + dependency: sha256_file(run_dir / "role-artifacts" / f"{dependency}.json") + for dependency in dependencies + } + snapshot = { + "version": 1, + "phase": phase, + "name": name, + "execution_package_sha256": sha256_file(run_dir / "execution-package.json"), + "repository_state": repository_state(), + "dependency_artifact_sha256": dependency_hashes, + "routing_config": resolved_routing["routing_config"], + "routing_config_sha256": resolved_routing["routing_config_sha256"], + "repository_profile": package["repository_profile"], + "repository_profile_sha256": package["repository_profile_sha256"], + } + serialized = json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n" + snapshot_sha256 = sha256_text(serialized) + path = run_dir / "input-snapshots" / f"{name}-{snapshot_sha256[:12]}.json" + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists() and path.read_text(encoding="utf-8") != serialized: + raise ValueError(f"input snapshot hash collision: {path}") + path.write_text(serialized, encoding="utf-8") + return path, snapshot_sha256 + + +def run_git(arguments): + result = subprocess.run( + ["git", *arguments], cwd=REPO_ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if result.returncode != 0: + raise ValueError(f"git {' '.join(arguments)} failed: {result.stderr.strip()}") + return result.stdout.rstrip("\n") + + +def run_git_bytes(arguments): + result = subprocess.run( + ["git", *arguments], cwd=REPO_ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if result.returncode != 0: + detail = result.stderr.decode("utf-8", errors="replace").strip() + raise ValueError(f"git {' '.join(arguments)} failed: {detail}") + return result.stdout + + +def collect_repository_fingerprints(repo_root=REPO_ROOT, include_tracked=True): + repo_root = Path(repo_root) + fingerprints = {} + if include_tracked: + staged = run_git_bytes_at(repo_root, ["ls-files", "--stage", "-z"]) + for record in filter(None, staged.split(b"\0")): + metadata, separator, raw_path = record.partition(b"\t") + if not separator: + continue + mode, object_id, stage = metadata.split() + path_text = os.fsdecode(raw_path) + fingerprints[path_text] = { + "type": "index", + "mode": mode.decode("ascii"), + "object_id": object_id.decode("ascii"), + "stage": stage.decode("ascii"), + } + dirty = run_git_bytes_at(repo_root, ["diff", "--name-only", "-z"]) + for raw_path in filter(None, dirty.split(b"\0")): + path_text = os.fsdecode(raw_path) + fingerprints[path_text] = fingerprint_repository_path(repo_root / path_text) + + untracked = run_git_bytes_at( + repo_root, ["ls-files", "-z", "--others", "--exclude-standard"] + ) + for raw_path in filter(None, untracked.split(b"\0")): + path_text = os.fsdecode(raw_path) + fingerprints[path_text] = fingerprint_repository_path(repo_root / path_text) + return dict(sorted(fingerprints.items())) + + +def run_git_bytes_at(repo_root, arguments): + result = subprocess.run( + ["git", *arguments], cwd=repo_root, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if result.returncode != 0: + detail = result.stderr.decode("utf-8", errors="replace").strip() + raise ValueError(f"git {' '.join(arguments)} failed: {detail}") + return result.stdout + + +def fingerprint_repository_path(path): + try: + metadata = path.lstat() + except FileNotFoundError: + return {"type": "missing", "mode": "", "sha256": ""} + mode = oct(stat.S_IMODE(metadata.st_mode)) + if path.is_symlink(): + return { + "type": "symlink", + "mode": mode, + "sha256": sha256_text(os.readlink(path)), + } + if path.is_file(): + return {"type": "file", "mode": mode, "sha256": sha256_file(path)} + return {"type": "other", "mode": mode, "sha256": ""} + + +def repository_state(): + return { + "head": run_git(["rev-parse", "HEAD"]), + "status": run_git(["status", "--short"]), + "diff_sha256": sha256_bytes(run_git_bytes(["diff", "--binary", "HEAD"])), + "untracked_files": collect_repository_fingerprints(include_tracked=False), + } + + +def changed_repository_paths(before, after): + return sorted( + path + for path in set(before) | set(after) + if before.get(path) != after.get(path) + ) + + +def normalize_repo_path_text(path_text): + path = Path(path_text) + if path.is_absolute(): + return repo_relative_path(path) + normalized = str(path_text).replace("\\", "/") + return normalized[2:] if normalized.startswith("./") else normalized + + +def reconcile_implementation_changes(run_dir, tasks, before): + after = collect_repository_fingerprints() + changed = changed_repository_paths(before, after) + dispatched_platforms = {task["platform"] for task in tasks} + unexpected = [ + path + for path in changed + if not any( + platform in dispatched_platforms and path_is_within(path, root) + for platform, root in PLATFORM_ROOTS.items() + ) + ] + contract = read_json(run_dir / "role-artifacts/contract.json") + targets = contract.get("output", {}).get("platform_targets", {}) + scope_errors = [] + for task in tasks: + platform = task["platform"] + platform_changes = [ + path for path in changed if path_is_within(path, PLATFORM_ROOTS[platform]) + ] + allowed = targets.get(platform, {}).get("files_allowed", []) + outside_allowed = [ + path + for path in platform_changes + if not any(path_is_within(path, allowed_path) for allowed_path in allowed) + ] + if outside_allowed: + scope_errors.append( + f"{platform} Implementation modified files outside Contract files_allowed: " + + ", ".join(outside_allowed) + ) + + artifact_path = run_dir / "role-artifacts" / f"{task['name']}.json" + artifact = read_json(artifact_path) + previous = { + normalize_repo_path_text(path) for path in task.get("previous_files_changed", []) + } + delta_path = run_dir / "repository-deltas" / f"{task['name']}.json" + prior_delta = read_json(delta_path) if delta_path.exists() else {} + before_values = dict(prior_delta.get("before", {})) + after_values = dict(prior_delta.get("after", {})) + for path in previous: + before_values.setdefault(path, None) + after_values.setdefault(path, after.get(path)) + for path in platform_changes: + before_values.setdefault(path, before.get(path)) + after_values[path] = after.get(path) + cumulative_paths = previous | set(platform_changes) | set(prior_delta.get("changed_files", [])) + net_changes = sorted( + path for path in cumulative_paths if before_values.get(path) != after_values.get(path) + ) + artifact["output"]["files_changed"] = net_changes + current_dispatch = artifact.get("dispatch", {}) + current_snapshot = current_dispatch.get("input_snapshot_sha256") + attempt = { + "input_snapshot": task.get("input_snapshot"), + "input_snapshot_sha256": task.get("input_snapshot_sha256"), + "run_id": ( + current_dispatch.get("run_id") + if current_snapshot == task.get("input_snapshot_sha256") + else "" + ), + "status": ( + artifact.get("status") + if current_snapshot == task.get("input_snapshot_sha256") + else "DISPATCH_ERROR" + ), + "changed_files": platform_changes, + } + delta = { + "version": 2, + "platform": platform, + "changed_files": net_changes, + "before": {path: before_values.get(path) for path in net_changes}, + "after": {path: after_values.get(path) for path in net_changes}, + "attempts": [*prior_delta.get("attempts", []), attempt], + } + write_json(delta_path, delta) + artifact["dispatch"]["repository_delta"] = run_relative_path(delta_path, run_dir) + artifact["dispatch"]["repository_delta_sha256"] = sha256_file(delta_path) + write_json(artifact_path, artifact) + if unexpected: + violation_path = run_dir / "repository-deltas/implementation-scope-violations.json" + write_json( + violation_path, + { + "version": 1, + "changed_files": unexpected, + "before": {path: before.get(path) for path in unexpected}, + "after": {path: after.get(path) for path in unexpected}, + }, + ) + scope_errors.append( + "Implementation modified files outside dispatched platform roots: " + + ", ".join(unexpected) + ) + if scope_errors: + raise ValueError("; ".join(scope_errors)) + + +def build_codex_command( + codex_bin, route, model, reasoning_effort, schema_path, raw_result_path, working_directory +): + raw_result_path.parent.mkdir(parents=True, exist_ok=True) + command = [ + codex_bin, + "exec", + "-C", + str(working_directory), + "-s", + route["sandbox"], + ] + command.extend( + [ + "--json", + "--output-schema", + str(schema_path), + "--output-last-message", + str(raw_result_path), + "--model", + model, + "-c", + f'model_reasoning_effort="{reasoning_effort}"', + "-", + ] + ) + return command + + +def run_codex_role( + command, + name, + role, + platform, + prompt_path, + artifact_path, + raw_result_path, + log_path, + stderr_path, + model_profile, + model, + reasoning_effort, + codex_version, + working_directory, + working_directory_manifest, + input_snapshot, + input_snapshot_sha256, + expected_repository_state, + timeout_seconds, +): + prompt = prompt_path.read_text(encoding="utf-8") + log_path.parent.mkdir(parents=True, exist_ok=True) + print(f"Dispatching {name} (timeout {timeout_seconds}s); logs: {log_path}", flush=True) + execution_error = None + with log_path.open("w", encoding="utf-8") as stdout_handle, stderr_path.open( + "w", encoding="utf-8" + ) as stderr_handle: + process = subprocess.Popen( + command, + cwd=working_directory, + stdin=subprocess.PIPE, + stdout=stdout_handle, + stderr=stderr_handle, + text=True, + start_new_session=sys.platform != "win32", + creationflags=( + getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) + if sys.platform == "win32" + else 0 + ), + ) + try: + process.communicate(input=prompt, timeout=timeout_seconds) + except subprocess.TimeoutExpired: + terminate_process_tree(process) + process.communicate() + execution_error = ValueError( + f"Codex dispatch for {name} timed out after {timeout_seconds} seconds" + ) + stdout = log_path.read_text(encoding="utf-8") + stderr = stderr_path.read_text(encoding="utf-8") + if execution_error is None and process.returncode != 0: + execution_error = ValueError( + f"Codex dispatch failed for {name}: {(stderr or stdout).strip()}" + ) + mutation_error = None + if role in {"contract", "verification"}: + actual_repository_state = repository_state() + if actual_repository_state != expected_repository_state: + violation_path = ( + artifact_path.parent.parent + / "repository-deltas" + / f"{name}-scope-violation.json" + ) + write_json( + violation_path, + { + "version": 1, + "role": role, + "name": name, + "before": expected_repository_state, + "after": actual_repository_state, + }, + ) + mutation_error = ValueError( + f"Codex {role} run {name} modified repository files" + ) + if execution_error or mutation_error: + details = [str(error) for error in [execution_error, mutation_error] if error] + raise ValueError("; ".join(details)) + run_id = extract_run_id(stdout) + if not run_id: + raise ValueError(f"Codex dispatch for {name} did not report a thread or run id") + agent_result = read_json(raw_result_path) + for field in ["status", "evidence", "summary", "output"]: + if field not in agent_result: + raise ValueError(f"Codex result for {name} is missing {field}") + if role == "verification": + validate_verification_host(platform, agent_result, sys.platform) + bind_verification_command_evidence( + agent_result["output"], + stdout, + run_relative_path(log_path, artifact_path.parent.parent), + platform, + working_directory_manifest, + ) + artifact = { + "name": name, + "role": role, + **({"platform": platform} if platform else {}), + "agent_id": f"{name}-{run_id}", + "dispatch": { + "mode": "codex-exec", + "run_id": run_id, + "prompt": f"role-prompts/{name}.md", + "prompt_sha256": sha256_text(prompt), + "artifact": f"role-artifacts/{name}.json", + "evidence": f"{codex_version} completed {name}; log: dispatch-logs/{name}.jsonl", + "command_log": run_relative_path(log_path, artifact_path.parent.parent), + "command_log_sha256": sha256_file(log_path), + "model_profile": model_profile, + "model": model, + "reasoning_effort": reasoning_effort, + "codex_version": codex_version, + "working_directory": working_directory_manifest, + "host_platform": sys.platform, + "input_snapshot": input_snapshot, + "input_snapshot_sha256": input_snapshot_sha256, + }, + "status": agent_result["status"], + "evidence": agent_result["evidence"], + "summary": agent_result["summary"], + "output": agent_result["output"], + } + if "waiver_reason" in agent_result: + artifact["waiver_reason"] = agent_result["waiver_reason"] + write_json(artifact_path, artifact) + + +def terminate_process_tree(process): + if process.poll() is not None: + return + if sys.platform == "win32": + result = subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(process.pid)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + if result.returncode != 0 and process.poll() is None: + process.kill() + return + try: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + except ProcessLookupError: + return + + +def validate_verification_host(platform, agent_result, host_platform): + if platform != "windows" or host_platform == "win32": + return + output = agent_result.get("output", {}) + has_build_execution = any( + isinstance(command, dict) + and command.get("kind") == "build" + and command.get("result") in {"PASS", "FAIL"} + for command in output.get("commands", []) + ) + if agent_result.get("status") == "WAIVED": + if output.get("build_result") == "PASS" or has_build_execution: + raise ValueError("WAIVED Windows Verification cannot report build execution") + return + if ( + agent_result.get("status") != "BLOCKED" + or output.get("result") != "BLOCKED" + or output.get("build_result") != "BLOCKED" + or has_build_execution + ): + raise ValueError( + "Windows Verification build evidence requires a Windows host; " + f"current host_platform={host_platform}" + ) + + +def bind_verification_command_evidence( + output, jsonl, log_path, platform, working_directory_manifest +): + commands = output.get("commands", []) if isinstance(output, dict) else [] + events = extract_command_events(jsonl) + used = set() + for index, declared in enumerate(commands): + result = declared.get("result") + if result not in {"PASS", "FAIL"}: + continue + if declared.get("kind") == "build" and not is_platform_build_command( + platform, declared.get("command"), working_directory_manifest + ): + raise ValueError( + f"Verification command {index} is not a recognized {platform} build command: " + f"{declared.get('command')}" + ) + declared_text = normalize_command(declared.get("command")) + match_index = next( + ( + event_index + for event_index, event in enumerate(events) + if event_index not in used and normalize_command(event["command"]) == declared_text + ), + None, + ) + if match_index is None: + raise ValueError( + f"Verification command {index} has no matching command_execution event: {declared.get('command')}" + ) + used.add(match_index) + event = events[match_index] + actual_result = "PASS" if event["exit_code"] == 0 else "FAIL" + if result != actual_result: + raise ValueError( + f"Verification command {declared.get('command')} declared {result} but exit_code={event['exit_code']}" + ) + declared["evidence"] = ( + f"{log_path}#{event['id']}; exit_code={event['exit_code']}" + ) + + +def extract_command_events(jsonl): + events = [] + for line in jsonl.splitlines(): + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + item = event.get("item") + if event.get("type") != "item.completed" or not isinstance(item, dict): + continue + if item.get("type") != "command_execution" or not isinstance(item.get("exit_code"), int): + continue + events.append( + { + "id": str(item.get("id") or f"command-{len(events)}"), + "command": item.get("command"), + "exit_code": item["exit_code"], + } + ) + return events + + +def normalize_command(command): + if isinstance(command, list): + command = " ".join(str(part) for part in command) + return " ".join(str(command or "").split()) + + +def extract_run_id(jsonl): + for line in jsonl.splitlines(): + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + for field in ["thread_id", "run_id", "session_id"]: + if event.get(field): + return str(event[field]) + thread = event.get("thread") + if isinstance(thread, dict) and thread.get("id"): + return str(thread["id"]) + return "" + + +def assemble_workspace(args): + run_dir = Path(args.run_dir).resolve() + scope_violations = sorted( + (run_dir / "repository-deltas").glob("*scope-violation*.json") + ) + if scope_violations: + raise ValueError( + "repository scope violation is unresolved; restore the repository and re-run init" + ) + package = read_json(run_dir / "execution-package.json") + profile_path = resolve_bound_repository_profile(package) + matrix_path = Path(args.matrix).resolve() + if package.get("matrix_path") != stable_matrix_path(matrix_path): + raise ValueError("matrix path changed since init; re-run init") + manifest = read_json(run_dir / "acceptance-manifest.json") + if manifest.get("requirement") != package.get("requirement"): + raise ValueError("requirement changed since init; re-run init") + artifacts = load_role_artifacts(run_dir / "role-artifacts") + manifest["final_status"] = args.final_status + manifest["contract"] = strip_artifact_metadata(artifacts["contract"]) + for platform in PLATFORMS: + manifest["platforms"][platform] = { + role: strip_artifact_metadata(artifacts[artifact_name(role, platform)]) + for role in ["implementation", "verification"] + } + manifest["cross_platform_acceptance"] = { + "result": args.cross_platform_result, + "evidence": args.cross_platform_evidence, + "differences": args.cross_platform_difference, + } + manifest["release"]["checks"] = collect_sdk_version_checks( + manifest["requirement"]["target_sdk_versions"], profile_path=profile_path + ) + errors = validate_manifest(manifest) + errors.extend(validate_evidence_files(manifest, run_dir)) + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + updates = [] + for platform in PLATFORMS: + updates.extend(manifest["platforms"][platform]["implementation"]["output"]["matrix_updates"]) + if updates and manifest["final_status"] != "BLOCKED": + update_matrix(matrix_path, updates) + output_path = run_dir / "final-acceptance-manifest.json" + write_json(output_path, manifest) + print(f"Acceptance manifest valid: {output_path}") + return 0 + +def load_role_artifacts(artifact_dir): + artifacts = {} + for artifact_path in sorted(artifact_dir.glob("*.json")): + artifact = read_json(artifact_path) + if not isinstance(artifact, dict): + raise ValueError(f"role artifact must be an object: {artifact_path.name}") + name = artifact.get("name") or artifact_path.stem + attach_dispatch_hashes(artifact, name, artifact_path, artifact_dir.parent) + artifacts[name] = artifact + expected = {"contract"} + expected.update( + artifact_name(role, platform) + for platform in PLATFORMS + for role in ["implementation", "verification"] + ) + missing = expected - set(artifacts) + if missing: + raise ValueError(f"missing role artifacts: {', '.join(sorted(missing))}") + return artifacts + + +def attach_dispatch_hashes(artifact, name, artifact_path, run_dir): + dispatch = artifact.setdefault("dispatch", {}) + dispatch.setdefault("artifact", f"role-artifacts/{name}.json") + dispatch["artifact_sha256"] = sha256_file(artifact_path) + prompt_path = resolve_run_path(run_dir, dispatch.get("prompt", f"role-prompts/{name}.md")) + if prompt_path.exists(): + actual_sha256 = sha256_file(prompt_path) + expected_sha256 = dispatch.get("prompt_sha256") + if expected_sha256 and expected_sha256 != actual_sha256: + raise ValueError(f"prompt sha256 mismatch for {name}: {dispatch.get('prompt')}") + dispatch["prompt_sha256"] = actual_sha256 + snapshot_value = dispatch.get("input_snapshot") + if snapshot_value: + snapshot_path = resolve_run_path(run_dir, snapshot_value) + if not snapshot_path.exists(): + raise ValueError(f"input snapshot does not exist for {name}: {snapshot_value}") + actual_sha256 = sha256_file(snapshot_path) + if dispatch.get("input_snapshot_sha256") != actual_sha256: + raise ValueError(f"input snapshot sha256 mismatch for {name}: {snapshot_value}") + snapshot = read_json(snapshot_path) + if not isinstance(snapshot, dict): + raise ValueError(f"input snapshot must be an object for {name}") + package_path = run_dir / "execution-package.json" + if not package_path.is_file(): + raise ValueError(f"execution package does not exist for {name}") + if snapshot.get("execution_package_sha256") != sha256_file(package_path): + raise ValueError(f"execution package sha256 mismatch for {name}") + dependency_hashes = snapshot.get("dependency_artifact_sha256", {}) + if not isinstance(dependency_hashes, dict): + raise ValueError(f"input snapshot dependencies must be an object for {name}") + for dependency, expected_sha256 in dependency_hashes.items(): + if Path(dependency).name != dependency: + raise ValueError(f"invalid dependency artifact name for {name}: {dependency}") + dependency_path = run_dir / "role-artifacts" / f"{dependency}.json" + if not dependency_path.is_file(): + raise ValueError(f"dependency artifact does not exist for {name}: {dependency}") + if sha256_file(dependency_path) != expected_sha256: + raise ValueError( + f"dependency artifact sha256 mismatch for {name}: {dependency}" + ) + command_log_value = dispatch.get("command_log") + if command_log_value: + command_log_path = resolve_run_path(run_dir, command_log_value) + if not command_log_path.exists(): + raise ValueError(f"command log does not exist for {name}: {command_log_value}") + actual_sha256 = sha256_file(command_log_path) + if dispatch.get("command_log_sha256") != actual_sha256: + raise ValueError(f"command log sha256 mismatch for {name}: {command_log_value}") + repository_delta_value = dispatch.get("repository_delta") + if repository_delta_value: + repository_delta_path = resolve_run_path(run_dir, repository_delta_value) + if not repository_delta_path.exists(): + raise ValueError( + f"repository delta does not exist for {name}: {repository_delta_value}" + ) + actual_sha256 = sha256_file(repository_delta_path) + if dispatch.get("repository_delta_sha256") != actual_sha256: + raise ValueError( + f"repository delta sha256 mismatch for {name}: {repository_delta_value}" + ) + delta = read_json(repository_delta_path) + if not isinstance(delta, dict): + raise ValueError(f"repository delta must be an object for {name}") + delta_files = { + normalize_repo_path_text(path) for path in delta.get("changed_files", []) + } + artifact_files = { + normalize_repo_path_text(path) + for path in artifact.get("output", {}).get("files_changed", []) + } + if delta_files != artifact_files: + raise ValueError(f"repository delta changed_files mismatch for {name}") + + +def strip_artifact_metadata(artifact): + return { + key: value + for key, value in artifact.items() + if key not in {"name", "role", "platform"} + } + + +def resolve_run_path(run_dir, path_text): + path = Path(path_text) + if path.is_absolute() or ".." in path.parts: + raise ValueError(f"execution artifact path must be relative to run directory: {path_text}") + return run_dir / path + + +def update_matrix(matrix_path, updates): + lines = matrix_path.read_text(encoding="utf-8").splitlines() + for update in updates: + lines = apply_matrix_update(lines, update) + matrix_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def apply_matrix_update(lines, update): + for index, line in enumerate(lines): + if not line.startswith("|"): + continue + header = split_markdown_row(line) + if "Feature" not in header or update["platform_unit"] not in header: + continue + feature_index = header.index("Feature") + platform_index = header.index(update["platform_unit"]) + for row_index in range(index + 2, len(lines)): + row_line = lines[row_index] + if not row_line.startswith("|"): + break + row = split_markdown_row(row_line) + if len(row) != len(header) or row[feature_index] != update["feature"]: + continue + status, _ = parse_matrix_cell(row[platform_index]) + if status != update["from"]: + raise ValueError( + f"matrix cell {update['feature']} / {update['platform_unit']} is {status}, expected {update['from']}" + ) + row[platform_index] = f"`{matrix_to_cell(update)}`" + lines[row_index] = "| " + " | ".join(row) + " |" + return lines + raise ValueError(f"matrix row not found for {update['feature']} / {update['platform_unit']}") + + +def matrix_to_cell(update): + cell = str(update.get("to_cell") or "").strip() + if cell.startswith("`") and cell.endswith("`"): + cell = cell[1:-1] + status, _ = parse_matrix_cell(cell) + if status != update.get("to") or any(character in cell for character in "\n\r|`"): + raise ValueError( + f"matrix to_cell must encode to={update.get('to')}: {update.get('to_cell')}" + ) + return cell + + +def artifact_name(role, platform): + return role if platform is None else f"{platform}-{role}" + + +def repo_relative_path(path): + try: + return path.resolve().relative_to(REPO_ROOT).as_posix() + except ValueError as exc: + raise ValueError(f"path must be inside the repository: {path}") from exc + + +def stable_matrix_path(path): + resolved = Path(path).resolve() + try: + return resolved.relative_to(REPO_ROOT).as_posix() + except ValueError: + return str(resolved) + + +def run_relative_path(path, run_dir): + return path.resolve().relative_to(run_dir.resolve()).as_posix() + + +def sha256_file(path): + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def sha256_text(value): + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +def sha256_bytes(value): + return hashlib.sha256(value).hexdigest() + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + init_parser = subparsers.add_parser("init", help="Create a requirement workspace") + init_parser.add_argument("--matrix", required=True) + init_parser.add_argument("--feature", help="Requirement feature; defaults to highest-priority gap") + init_parser.add_argument("--sdk-family", help="Required for a feature outside the matrix backlog") + init_parser.add_argument("--key-api", action="append", help="Key SDK API; repeat as needed") + init_parser.add_argument("--target-sdk-version", required=True) + init_parser.add_argument( + "--platform-sdk-version", + action="append", + default=[], + help="Override one platform as platform=x.y.z; repeat as needed", + ) + init_parser.add_argument("--run-dir", required=True) + init_parser.add_argument("--index", type=int, default=0) + init_parser.add_argument("--routing-config", default=str(DEFAULT_ROUTING_CONFIG)) + init_parser.add_argument("--repository-profile", default=str(DEFAULT_REPOSITORY_PROFILE)) + + dispatch_parser = subparsers.add_parser("dispatch", help="Dispatch one requirement phase") + dispatch_parser.add_argument("--run-dir", required=True) + dispatch_parser.add_argument("--phase", required=True, choices=PHASE_ORDER) + dispatch_parser.add_argument("--platform", action="append", choices=PLATFORMS) + dispatch_parser.add_argument("--model") + dispatch_parser.add_argument("--routing-config", default=str(DEFAULT_ROUTING_CONFIG)) + dispatch_parser.add_argument("--codex-bin", default="codex") + dispatch_parser.add_argument("--timeout-seconds", type=int, default=900) + dispatch_parser.add_argument("--retry", action="store_true") + dispatch_parser.add_argument("--dry-run", action="store_true") + + assemble_parser = subparsers.add_parser("assemble", help="Assemble the v4 manifest") + assemble_parser.add_argument("--run-dir", required=True) + assemble_parser.add_argument("--matrix", required=True) + assemble_parser.add_argument( + "--final-status", choices=["PASS", "PASS WITH RISKS", "BLOCKED"], default="BLOCKED" + ) + assemble_parser.add_argument( + "--cross-platform-result", choices=RESULT_VALUES, default="BLOCKED" + ) + assemble_parser.add_argument( + "--cross-platform-evidence", default="Pending required platform verification." + ) + assemble_parser.add_argument("--cross-platform-difference", action="append", default=[]) + args = parser.parse_args(argv) + try: + if getattr(args, "timeout_seconds", 1) <= 0: + raise ValueError("timeout-seconds must be positive") + if args.command == "init": + return init_workspace(args) + if args.command == "dispatch": + return dispatch_workspace(args) + if args.command == "assemble": + return assemble_workspace(args) + except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/ai-engineering/tools/orchestrate_case_execution_test.py b/docs/ai-engineering/tools/orchestrate_case_execution_test.py new file mode 100644 index 000000000..07875122d --- /dev/null +++ b/docs/ai-engineering/tools/orchestrate_case_execution_test.py @@ -0,0 +1,1176 @@ +import json +import subprocess +import sys +import tempfile +import textwrap +import time +import unittest +from pathlib import Path +from unittest import mock + + +REPO_ROOT = Path(__file__).resolve().parents[3] +ORCHESTRATOR = REPO_ROOT / "docs/ai-engineering/tools/orchestrate_case_execution.py" +VALIDATOR = REPO_ROOT / "docs/ai-engineering/tools/validate_acceptance_manifest.py" +PLATFORMS = ["android", "ios", "macos", "windows"] +TOOLS_DIR = REPO_ROOT / "docs/ai-engineering/tools" +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +import orchestrate_case_execution as orchestrator +from orchestrate_case_execution import collect_repository_fingerprints, run_codex_role + + +class RequirementOrchestratorTest(unittest.TestCase): + TARGET_SDK_VERSION = "4.6.4" + + def write_matrix(self): + handle = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) + handle.write( + textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | + | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(app/JoinChannelAudio.java)` | `DONE(APIExample/JoinChannelAudio.swift)` | `PARTIAL(APIExample/JoinChannelAudio.swift)` | `MISSING` | Keep official platforms aligned. | + + ## Confirmed Gaps + + | Gap | Affected Units | Severity | + | --- | --- | --- | + | Basic audio-only join channel | Windows | High - missing foundational case | + """ + ) + ) + handle.close() + self.addCleanup(lambda: Path(handle.name).unlink(missing_ok=True)) + return Path(handle.name) + + def run_orchestrator(self, *args): + return subprocess.run( + [sys.executable, str(ORCHESTRATOR), *args], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + def init_workspace(self, matrix_path, run_dir, platform_sdk_versions=None): + version_args = [] + for value in platform_sdk_versions or []: + version_args.extend(["--platform-sdk-version", value]) + result = self.run_orchestrator( + "init", + "--matrix", + str(matrix_path), + "--feature", + "Join channel audio", + "--target-sdk-version", + self.TARGET_SDK_VERSION, + *version_args, + "--run-dir", + str(run_dir), + ) + self.assertEqual(result.returncode, 0, result.stderr) + + def write_fake_codex( + self, directory, implementation_sleep=0, fail_implementation_platform=None + ): + path = Path(directory) / "fake-codex" + path.write_text( + textwrap.dedent( + f"""\ + #!/usr/bin/env python3 + import json + import sys + import time + from pathlib import Path + + if sys.argv[1:] == ["--version"]: + print("codex-cli 0.test") + raise SystemExit(0) + + args = sys.argv[1:] + output_path = Path(args[args.index("--output-last-message") + 1]) + name = output_path.stem + fail_name = {json.dumps(f'{fail_implementation_platform}-implementation' if fail_implementation_platform else '')} + if name == fail_name: + print(json.dumps({{"type": "thread.started", "thread_id": f"run-{{name}}"}}), flush=True) + raise SystemExit(7) + targets = {{ + "android": "Android/APIExample/", + "ios": "iOS/APIExample/", + "macos": "macOS/", + "windows": "windows/", + }} + build_commands = {{ + "android": "./gradlew assembleDebug", + "ios": "xcodebuild -scheme APIExample build", + "macos": "xcodebuild -scheme APIExample build", + "windows": "msbuild APIExample.sln /t:Build", + }} + status = "PASS" + if name == "contract": + output = {{ + "scenario": "Keep Join channel audio aligned across official platforms.", + "key_apis": ["joinChannel", "setAudioProfile"], + "non_goals": ["Release packaging"], + "reference": {{ + "required": True, + "source_case": "Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java", + "contract_result": "PASS", + }}, + "cross_platform_requirements": ["Equivalent join and leave behavior"], + "platform_targets": {{ + platform: {{ + "required": True, + "target_project": target, + "key_constraints": [f"Keep changes in {{target}}"], + "files_allowed": [target], + "waiver_reason": "", + }} + for platform, target in targets.items() + }}, + }} + else: + platform, role = name.rsplit("-", 1) + if role == "implementation": + time.sleep({implementation_sleep}) + output = {{ + "target_project": targets[platform], + "query_cases": "No duplicate case found.", + "upsert_case": "Platform case updated.", + "files_changed": [f"{{targets[platform]}}AGENTS.md"], + "matrix_updates": [], + }} + else: + if platform == "windows" and sys.platform != "win32": + status = "BLOCKED" + output = {{ + "result": "BLOCKED", + "findings": ["Windows build requires a Windows host."], + "parity_result": "PASS", + "entry_point": "Basic > Join channel audio", + "ux_notes": "Static review only on this host.", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [{{ + "name": "Windows MSBuild", + "reason": "Current host is not Windows.", + }}], + }} + else: + output = {{ + "result": "PASS", + "findings": ["No blocking findings."], + "parity_result": "PASS", + "entry_point": "Basic > Join channel audio", + "ux_notes": "Matches adjacent examples.", + "commands": [{{ + "kind": "build", + "command": build_commands[platform], + "result": "PASS", + }}], + "build_result": "PASS", + "skipped_checks": [], + }} + result = {{ + "status": status, + "evidence": f"{{name}} evidence", + "summary": f"{{name}} completed", + "output": output, + }} + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(result), encoding="utf-8") + print(json.dumps({{"type": "thread.started", "thread_id": f"run-{{name}}"}}), flush=True) + if name != "contract" and name.endswith("-verification") and output["commands"]: + print(json.dumps({{ + "type": "item.completed", + "item": {{ + "id": f"build-{{platform}}", + "type": "command_execution", + "command": build_commands[platform], + "status": "completed", + "exit_code": 0, + }}, + }}), flush=True) + """ + ), + encoding="utf-8", + ) + path.chmod(0o755) + return path + + def dispatch_contract(self, run_dir, codex_bin): + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "contract", + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + + def test_init_creates_shared_contract_and_eight_platform_role_tasks(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + + manifest = json.loads((run_dir / "acceptance-manifest.json").read_text()) + package = json.loads((run_dir / "execution-package.json").read_text()) + artifact_names = sorted(path.stem for path in (run_dir / "role-artifacts").glob("*.json")) + expected = ["contract"] + [ + f"{platform}-{role}" + for platform in PLATFORMS + for role in ["implementation", "verification"] + ] + self.assertEqual(artifact_names, sorted(expected)) + self.assertEqual(sorted(manifest["platforms"]), PLATFORMS) + self.assertNotIn("roles", manifest) + self.assertEqual( + package["repository_profile"], + "docs/ai-engineering/repository-profile.json", + ) + self.assertEqual(len(package["repository_profile_sha256"]), 64) + + def test_execution_configuration_rejects_repository_profile_drift(self): + routing_path = REPO_ROOT / "docs/ai-engineering/role-routing.json" + package = { + "routing_config": "docs/ai-engineering/role-routing.json", + "routing_config_sha256": orchestrator.sha256_file(routing_path), + "repository_profile": "docs/ai-engineering/repository-profile.json", + "repository_profile_sha256": "0" * 64, + } + + with self.assertRaisesRegex(ValueError, "repository profile content changed"): + orchestrator.validate_execution_configuration(package, routing_path) + + def test_contract_input_snapshot_binds_repository_profile(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + + package = json.loads((run_dir / "execution-package.json").read_text()) + artifact = json.loads((run_dir / "role-artifacts/contract.json").read_text()) + snapshot_path = run_dir / artifact["dispatch"]["input_snapshot"] + snapshot = json.loads(snapshot_path.read_text()) + + self.assertEqual(snapshot["repository_profile"], package["repository_profile"]) + self.assertEqual( + snapshot["repository_profile_sha256"], + package["repository_profile_sha256"], + ) + + def test_init_accepts_new_requirement_outside_the_matrix_backlog(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + result = self.run_orchestrator( + "init", + "--matrix", + str(matrix_path), + "--feature", + "Spatial audio", + "--sdk-family", + "Full RTC", + "--key-api", + "enableSpatialAudio", + "--target-sdk-version", + self.TARGET_SDK_VERSION, + "--run-dir", + str(run_dir), + ) + + self.assertEqual(result.returncode, 0, result.stderr) + package = json.loads((run_dir / "execution-package.json").read_text()) + self.assertEqual(package["requirement"]["feature"], "Spatial audio") + self.assertEqual(package["requirement"]["key_apis"], ["enableSpatialAudio"]) + contract_prompt = (run_dir / "role-prompts/contract.md").read_text() + self.assertNotIn("If Contract marks this platform required=false", contract_prompt) + + def test_init_persists_and_prompts_staggered_platform_sdk_versions(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + result = self.run_orchestrator( + "init", + "--matrix", + str(matrix_path), + "--feature", + "Join channel audio", + "--target-sdk-version", + "4.6.2", + "--platform-sdk-version", + "android=4.6.3", + "--run-dir", + str(run_dir), + ) + + self.assertEqual(result.returncode, 0, result.stderr) + package = json.loads((run_dir / "execution-package.json").read_text()) + expected = { + "android": "4.6.3", + "ios": "4.6.2", + "macos": "4.6.2", + "windows": "4.6.2", + } + self.assertEqual(package["requirement"]["target_sdk_versions"], expected) + contract_prompt = (run_dir / "role-prompts/contract.md").read_text() + self.assertIn('"android": "4.6.3"', contract_prompt) + self.assertIn('"windows": "4.6.2"', contract_prompt) + + def test_repository_matrix_identity_is_portable(self): + identity = orchestrator.stable_matrix_path( + REPO_ROOT / "docs/ai-engineering/case-maintenance-matrix.md" + ) + + self.assertEqual(identity, "docs/ai-engineering/case-maintenance-matrix.md") + + def test_platform_implementation_waits_for_shared_contract(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--platform", + "android", + "--model", + "test-model", + "--dry-run", + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("Contract gate returned status BLOCKED", result.stderr) + + def test_contract_semantic_failure_is_blocked_before_implementation(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + artifact_path = run_dir / "role-artifacts/contract.json" + artifact = json.loads(artifact_path.read_text()) + artifact["status"] = "PASS" + artifact["output"]["reference"]["contract_result"] = "PASS" + artifact["output"]["cross_platform_requirements"] = [] + artifact_path.write_text(json.dumps(artifact), encoding="utf-8") + + with self.assertRaisesRegex(ValueError, "Contract semantic gate failed"): + orchestrator.validate_completed_contract(run_dir) + + blocked = json.loads(artifact_path.read_text()) + self.assertEqual(blocked["status"], "BLOCKED") + self.assertIn("cross_platform_requirements", blocked["evidence"]) + + def test_implementation_phase_dispatches_four_platform_agents_with_scoped_deltas(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + for platform in PLATFORMS: + artifact = json.loads( + (run_dir / f"role-artifacts/{platform}-implementation.json").read_text() + ) + self.assertEqual(artifact["dispatch"]["mode"], "codex-exec") + self.assertEqual(artifact["dispatch"]["run_id"], f"run-{platform}-implementation") + self.assertEqual(artifact["output"]["files_changed"], []) + + def test_implementation_reconciles_each_agent_before_starting_the_next(self): + tasks = [ + {"name": "android-implementation", "platform": "android"}, + {"name": "ios-implementation", "platform": "ios"}, + ] + events = [] + + with mock.patch.object( + orchestrator, + "refresh_implementation_input_snapshot", + side_effect=lambda run_dir, task: events.append(f"snapshot:{task['name']}"), + ), mock.patch.object( + orchestrator, + "collect_repository_fingerprints", + side_effect=[{"state": "android"}, {"state": "ios"}], + ), mock.patch.object( + orchestrator, + "run_codex_role", + side_effect=lambda **kwargs: events.append(f"run:{kwargs['name']}"), + ), mock.patch.object( + orchestrator, + "reconcile_implementation_changes", + side_effect=lambda run_dir, task_group, before: events.append( + f"reconcile:{task_group[0]['name']}" + ), + ), mock.patch.object( + orchestrator, + "validate_completed_implementation", + side_effect=lambda run_dir, platform: events.append(f"validate:{platform}"), + ): + orchestrator.run_implementation_tasks(Path("/tmp/run"), tasks) + + self.assertEqual( + events, + [ + "snapshot:android-implementation", + "run:android-implementation", + "reconcile:android-implementation", + "validate:android", + "snapshot:ios-implementation", + "run:ios-implementation", + "reconcile:ios-implementation", + "validate:ios", + ], + ) + + def test_implementation_semantic_failure_blocks_verification(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + artifact_path = run_dir / "role-artifacts/android-implementation.json" + artifact = json.loads(artifact_path.read_text()) + artifact["status"] = "PASS" + artifact["output"]["query_cases"] = "" + artifact_path.write_text(json.dumps(artifact), encoding="utf-8") + + with self.assertRaisesRegex(ValueError, "Implementation semantic gate failed"): + orchestrator.validate_completed_implementation(run_dir, "android") + + blocked = json.loads(artifact_path.read_text()) + self.assertEqual(blocked["status"], "BLOCKED") + self.assertIn("query_cases", blocked["evidence"]) + + def test_implementation_refreshes_snapshot_after_previous_platform_delta(self): + task = { + "name": "ios-implementation", + "snapshot_dependencies": {"contract": {}}, + "resolved_routing": {"routing_config": "routing.json"}, + "input_snapshot": "old.json", + "input_snapshot_sha256": "old-sha", + "expected_repository_state": {"head": "old"}, + } + snapshot_path = Path("/tmp/run/input-snapshots/ios-new.json") + + with mock.patch.object( + orchestrator, + "write_input_snapshot", + return_value=(snapshot_path, "new-sha"), + ), mock.patch.object( + orchestrator, + "read_json", + return_value={"repository_state": {"head": "after-android"}}, + ): + orchestrator.refresh_implementation_input_snapshot(Path("/tmp/run"), task) + + self.assertEqual(task["input_snapshot"], "input-snapshots/ios-new.json") + self.assertEqual(task["input_snapshot_sha256"], "new-sha") + self.assertEqual(task["expected_repository_state"], {"head": "after-android"}) + + def test_repository_fingerprints_include_untracked_file_contents(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + path = root / "new-example.swift" + path.write_text("first\n", encoding="utf-8") + before = collect_repository_fingerprints(root) + + path.write_text("second\n", encoding="utf-8") + after = collect_repository_fingerprints(root) + + self.assertNotEqual(before["new-example.swift"], after["new-example.swift"]) + + def test_artifact_loading_rejects_stale_dependency_snapshot(self): + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + artifact_dir = run_dir / "role-artifacts" + artifact_dir.mkdir(parents=True) + package_path = run_dir / "execution-package.json" + package_path.write_text('{"requirement":{}}\n', encoding="utf-8") + contract_path = artifact_dir / "contract.json" + contract_path.write_text('{"status":"PASS"}\n', encoding="utf-8") + snapshot_path = run_dir / "input-snapshots/android-verification.json" + snapshot_path.parent.mkdir(parents=True) + snapshot_path.write_text( + json.dumps( + { + "execution_package_sha256": orchestrator.sha256_file(package_path), + "dependency_artifact_sha256": {"contract": "0" * 64}, + } + ), + encoding="utf-8", + ) + artifact_path = artifact_dir / "android-verification.json" + artifact = { + "dispatch": { + "mode": "codex-exec", + "input_snapshot": "input-snapshots/android-verification.json", + "input_snapshot_sha256": orchestrator.sha256_file(snapshot_path), + } + } + artifact_path.write_text(json.dumps(artifact), encoding="utf-8") + + with self.assertRaisesRegex(ValueError, "dependency artifact sha256 mismatch"): + orchestrator.attach_dispatch_hashes( + artifact, + "android-verification", + artifact_path, + run_dir, + ) + + def test_implementation_retry_delta_covers_all_attempt_changes(self): + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + contract_path = run_dir / "role-artifacts/contract.json" + contract_path.parent.mkdir(parents=True) + contract_path.write_text( + json.dumps( + { + "output": { + "platform_targets": { + "android": {"files_allowed": ["Android/APIExample/"]} + } + } + } + ), + encoding="utf-8", + ) + artifact_path = run_dir / "role-artifacts/android-implementation.json" + artifact = { + "status": "BLOCKED", + "dispatch": { + "mode": "codex-exec", + "run_id": "run-1", + "input_snapshot_sha256": "snapshot-1", + }, + "output": {"files_changed": []}, + } + artifact_path.write_text(json.dumps(artifact), encoding="utf-8") + first_before = {"Android/APIExample/A.java": {"sha256": "before-a"}} + first_after = {"Android/APIExample/A.java": {"sha256": "after-a"}} + first_task = { + "name": "android-implementation", + "platform": "android", + "input_snapshot": "input-snapshots/android-1.json", + "input_snapshot_sha256": "snapshot-1", + "previous_files_changed": [], + } + with mock.patch.object( + orchestrator, "collect_repository_fingerprints", return_value=first_after + ): + orchestrator.reconcile_implementation_changes( + run_dir, [first_task], first_before + ) + + artifact = json.loads(artifact_path.read_text()) + artifact["status"] = "PASS" + artifact["dispatch"]["run_id"] = "run-2" + artifact["dispatch"]["input_snapshot_sha256"] = "snapshot-2" + artifact_path.write_text(json.dumps(artifact), encoding="utf-8") + second_after = { + **first_after, + "Android/APIExample/B.java": {"sha256": "after-b"}, + } + second_task = { + "name": "android-implementation", + "platform": "android", + "input_snapshot": "input-snapshots/android-2.json", + "input_snapshot_sha256": "snapshot-2", + "previous_files_changed": ["Android/APIExample/A.java"], + } + with mock.patch.object( + orchestrator, "collect_repository_fingerprints", return_value=second_after + ): + orchestrator.reconcile_implementation_changes( + run_dir, [second_task], first_after + ) + + artifact = json.loads(artifact_path.read_text()) + delta = json.loads( + (run_dir / "repository-deltas/android-implementation.json").read_text() + ) + expected = ["Android/APIExample/A.java", "Android/APIExample/B.java"] + self.assertEqual(artifact["output"]["files_changed"], expected) + self.assertEqual(delta["changed_files"], expected) + self.assertEqual([attempt["run_id"] for attempt in delta["attempts"]], ["run-1", "run-2"]) + + def test_failed_implementation_still_reconciles_successful_artifacts(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex( + tmpdir, fail_implementation_platform="windows" + ) + self.dispatch_contract(run_dir, codex_bin) + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + + self.assertEqual(result.returncode, 1) + android = json.loads( + (run_dir / "role-artifacts/android-implementation.json").read_text() + ) + self.assertEqual(android["output"]["files_changed"], []) + self.assertIn("repository_delta", android["dispatch"]) + + def test_failed_verification_mutation_creates_persistent_scope_violation(self): + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + prompt_path = run_dir / "role-prompts/ios-verification.md" + prompt_path.parent.mkdir(parents=True) + prompt_path.write_text("verify", encoding="utf-8") + artifact_path = run_dir / "role-artifacts/ios-verification.json" + + with mock.patch( + "orchestrate_case_execution.repository_state", + return_value={"state": "after"}, + ): + with self.assertRaisesRegex(ValueError, "modified repository files"): + run_codex_role( + command=[sys.executable, "-c", "raise SystemExit(7)"], + name="ios-verification", + role="verification", + platform="ios", + prompt_path=prompt_path, + artifact_path=artifact_path, + raw_result_path=run_dir / "raw-results/ios-verification.json", + log_path=run_dir / "dispatch-logs/ios-verification.jsonl", + stderr_path=run_dir / "dispatch-logs/ios-verification.stderr.log", + model_profile="review", + model="test-model", + reasoning_effort="high", + codex_version="codex-cli test", + working_directory=Path(tmpdir), + working_directory_manifest="iOS/APIExample/", + input_snapshot="input-snapshots/ios-verification.json", + input_snapshot_sha256="a" * 64, + expected_repository_state={"state": "before"}, + timeout_seconds=5, + ) + + self.assertTrue( + (run_dir / "repository-deltas/ios-verification-scope-violation.json").exists() + ) + + def test_macos_rejects_windows_build_pass(self): + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + prompt_path = run_dir / "role-prompts/windows-verification.md" + prompt_path.parent.mkdir(parents=True) + prompt_path.write_text("verify", encoding="utf-8") + raw_result_path = run_dir / "raw-results/windows-verification.json" + raw_result_path.parent.mkdir(parents=True) + raw_result_path.write_text( + json.dumps( + { + "status": "PASS", + "evidence": "MSBuild passed.", + "summary": "Windows verified.", + "output": { + "result": "PASS", + "findings": [], + "parity_result": "PASS", + "entry_point": "Basic > Join channel audio", + "ux_notes": "Matches adjacent examples.", + "commands": [ + { + "kind": "build", + "command": "msbuild APIExample.sln /t:Build", + "result": "PASS", + } + ], + "build_result": "PASS", + "skipped_checks": [], + }, + } + ), + encoding="utf-8", + ) + command = [ + sys.executable, + "-c", + ( + "import json; " + "print(json.dumps({'type':'thread.started','thread_id':'run-windows'})); " + "print(json.dumps({'type':'item.completed','item':" + "{'id':'build','type':'command_execution','command':" + "'msbuild APIExample.sln /t:Build','exit_code':0}}))" + ), + ] + + with mock.patch.object(orchestrator.sys, "platform", "darwin"), mock.patch.object( + orchestrator, + "repository_state", + return_value={"state": "clean"}, + ): + with self.assertRaisesRegex(ValueError, "requires a Windows host"): + run_codex_role( + command=command, + name="windows-verification", + role="verification", + platform="windows", + prompt_path=prompt_path, + artifact_path=run_dir / "role-artifacts/windows-verification.json", + raw_result_path=raw_result_path, + log_path=run_dir / "dispatch-logs/windows-verification.jsonl", + stderr_path=run_dir / "dispatch-logs/windows-verification.stderr.log", + model_profile="review", + model="test-model", + reasoning_effort="high", + codex_version="codex-cli test", + working_directory=Path(tmpdir), + working_directory_manifest="windows/", + input_snapshot="input-snapshots/windows-verification.json", + input_snapshot_sha256="a" * 64, + expected_repository_state={"state": "clean"}, + timeout_seconds=5, + ) + + def test_verification_semantic_failure_becomes_retryable(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + artifact_path = run_dir / "role-artifacts/ios-verification.json" + artifact = json.loads(artifact_path.read_text()) + artifact["status"] = "PASS" + artifact["output"]["result"] = "BLOCKED" + artifact_path.write_text(json.dumps(artifact), encoding="utf-8") + + with self.assertRaisesRegex(ValueError, "Verification semantic gate failed"): + orchestrator.validate_completed_verification(run_dir, "ios") + + blocked = json.loads(artifact_path.read_text()) + self.assertEqual(blocked["status"], "BLOCKED") + self.assertIn("output.result", blocked["evidence"]) + + def test_platform_filter_dispatches_only_requested_agent(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--platform", + "windows", + "--model", + "test-model", + "--dry-run", + ) + + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + payload = json.loads(result.stdout) + self.assertEqual([item["platform"] for item in payload["commands"]], ["windows"]) + command = payload["commands"][0]["command"] + self.assertIn("-C", command) + self.assertIn(str(REPO_ROOT / "windows"), command) + + def test_platform_prompt_uses_target_selected_by_completed_contract(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + contract_path = run_dir / "role-artifacts/contract.json" + contract = json.loads(contract_path.read_text()) + contract["output"]["platform_targets"]["ios"]["target_project"] = "iOS/APIExample-OC/" + contract["output"]["platform_targets"]["ios"]["files_allowed"] = ["iOS/APIExample-OC/"] + contract_path.write_text(json.dumps(contract), encoding="utf-8") + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--platform", + "ios", + "--model", + "test-model", + "--dry-run", + ) + + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + prompt = (run_dir / "role-prompts/ios-implementation.md").read_text() + self.assertIn("Target: iOS/APIExample-OC/", prompt) + self.assertIn("If Contract marks this platform required=false", prompt) + + def test_verification_uses_corresponding_platform_implementation(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + implementation = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + self.assertEqual(implementation.returncode, 0, implementation.stderr) + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "verification", + "--platform", + "windows", + "--model", + "test-model", + "--dry-run", + ) + + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + prompt = (run_dir / "role-prompts/windows-verification.md").read_text() + self.assertIn("windows-implementation completed", prompt) + self.assertIn("macOS cannot provide Windows build evidence", prompt) + self.assertIn("Do not download Windows SDK archives", prompt) + implementation_prompt = (run_dir / "role-prompts/windows-implementation.md").read_text() + self.assertNotIn("cannot provide Windows build evidence", implementation_prompt) + + def test_platform_dispatch_timeout_keeps_other_platform_scope_isolated(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir, implementation_sleep=2) + self.dispatch_contract(run_dir, codex_bin) + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--platform", + "android", + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + "--timeout-seconds", + "1", + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("android-implementation timed out after 1 seconds", result.stderr) + self.assertTrue((run_dir / "dispatch-logs/android-implementation.jsonl").exists()) + self.assertFalse((run_dir / "dispatch-logs/ios-implementation.jsonl").exists()) + + def test_timeout_terminates_agent_child_processes(self): + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + prompt_path = run_dir / "role-prompts/android-implementation.md" + prompt_path.parent.mkdir(parents=True) + prompt_path.write_text("implement", encoding="utf-8") + marker = Path(tmpdir) / "late-child-write.txt" + child_code = ( + "import time; from pathlib import Path; " + f"time.sleep(0.4); Path({str(marker)!r}).write_text('late')" + ) + parent_code = ( + "import subprocess, sys, time; " + f"subprocess.Popen([sys.executable, '-c', {child_code!r}]); " + "time.sleep(10)" + ) + + with self.assertRaisesRegex(ValueError, "timed out"): + run_codex_role( + command=[sys.executable, "-c", parent_code], + name="android-implementation", + role="implementation", + platform="android", + prompt_path=prompt_path, + artifact_path=run_dir / "role-artifacts/android-implementation.json", + raw_result_path=run_dir / "raw-results/android-implementation.json", + log_path=run_dir / "dispatch-logs/android-implementation.jsonl", + stderr_path=run_dir / "dispatch-logs/android-implementation.stderr.log", + model_profile="deep", + model="test-model", + reasoning_effort="high", + codex_version="codex-cli test", + working_directory=Path(tmpdir), + working_directory_manifest="Android/APIExample/", + input_snapshot="input-snapshots/android-implementation.json", + input_snapshot_sha256="a" * 64, + expected_repository_state={"state": "clean"}, + timeout_seconds=0.1, + ) + time.sleep(0.6) + + self.assertFalse(marker.exists()) + + def test_full_multi_platform_run_assembles_blocked_v4_manifest(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + for phase in ["implementation", "verification"]: + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + phase, + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + + result = self.run_orchestrator( + "assemble", + "--run-dir", + str(run_dir), + "--matrix", + str(matrix_path), + "--final-status", + "BLOCKED", + "--cross-platform-result", + "BLOCKED", + "--cross-platform-evidence", + "Target SDK and Windows-host build evidence are pending.", + ) + + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + final_path = run_dir / "final-acceptance-manifest.json" + final_manifest = json.loads(final_path.read_text()) + self.assertEqual(final_manifest["version"], 4) + self.assertEqual(final_manifest["cross_platform_acceptance"]["result"], "BLOCKED") + self.assertEqual(sorted(final_manifest["platforms"]), PLATFORMS) + self.assertTrue(final_manifest["release"]["required"]) + self.assertEqual( + final_manifest["release"]["target_sdk_versions"], + {platform: "4.6.4" for platform in PLATFORMS}, + ) + validation = subprocess.run( + [sys.executable, str(VALIDATOR), str(final_path)], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual(validation.returncode, 0, validation.stderr) + + def test_assemble_reports_only_repository_gates_without_external_metadata(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + for phase in ["implementation", "verification"]: + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + phase, + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + + result = self.run_orchestrator( + "assemble", + "--run-dir", + str(run_dir), + "--matrix", + str(matrix_path), + "--final-status", + "PASS", + "--cross-platform-result", + "PASS", + "--cross-platform-evidence", + "All required platforms passed independent verification.", + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("sdk-version-android", result.stderr) + self.assertNotIn("qa_acceptance", result.stderr) + + def test_assemble_rejects_matrix_path_changed_since_init(self): + matrix_path = self.write_matrix() + other_matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + + result = self.run_orchestrator( + "assemble", + "--run-dir", + str(run_dir), + "--matrix", + str(other_matrix_path), + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("matrix path changed since init", result.stderr) + + def test_assemble_rejects_requirement_changed_since_init(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + manifest_path = run_dir / "acceptance-manifest.json" + manifest = json.loads(manifest_path.read_text()) + manifest["requirement"]["feature"] = "Different requirement" + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") + + result = self.run_orchestrator( + "assemble", + "--run-dir", + str(run_dir), + "--matrix", + str(matrix_path), + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("requirement changed since init", result.stderr) + + def test_assemble_rejects_missing_completed_prompt_evidence(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + (run_dir / "role-prompts/contract.md").unlink() + + result = self.run_orchestrator( + "assemble", + "--run-dir", + str(run_dir), + "--matrix", + str(matrix_path), + ) + + self.assertEqual(result.returncode, 1) + self.assertIn("contract.dispatch.prompt does not exist", result.stderr) + + def test_retrying_platform_implementation_invalidates_old_verification(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + codex_bin = self.write_fake_codex(tmpdir) + self.dispatch_contract(run_dir, codex_bin) + for phase in ["implementation", "verification"]: + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + phase, + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + implementation_path = run_dir / "role-artifacts/windows-implementation.json" + implementation = json.loads(implementation_path.read_text()) + implementation["status"] = "BLOCKED" + implementation_path.write_text(json.dumps(implementation), encoding="utf-8") + + result = self.run_orchestrator( + "dispatch", + "--run-dir", + str(run_dir), + "--phase", + "implementation", + "--platform", + "windows", + "--retry", + "--model", + "test-model", + "--codex-bin", + str(codex_bin), + ) + + self.assertEqual(result.returncode, 0, result.stderr + result.stdout) + verification = json.loads( + (run_dir / "role-artifacts/windows-verification.json").read_text() + ) + self.assertEqual(verification["status"], "BLOCKED") + self.assertEqual(verification["dispatch"]["mode"], "pending") + + def test_removed_single_platform_phases_are_rejected(self): + result = self.run_orchestrator( + "dispatch", + "--run-dir", + "/tmp/not-used", + "--phase", + "target-test", + "--model", + "test-model", + "--dry-run", + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("invalid choice", result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/ai-engineering/tools/prepare_case_execution.py b/docs/ai-engineering/tools/prepare_case_execution.py new file mode 100644 index 000000000..8d0223036 --- /dev/null +++ b/docs/ai-engineering/tools/prepare_case_execution.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +"""Prepare one cross-platform API Examples requirement package.""" + +import argparse +import json +import re +import sys +from pathlib import Path + +from generate_case_backlog import DEFAULT_MATRIX, REPO_ROOT, generate_execution_units + + +PLATFORMS = ["android", "ios", "macos", "windows"] +DEFAULT_REPOSITORY_PROFILE = REPO_ROOT / "docs/ai-engineering/repository-profile.json" +PROFILE_SOURCE_FIELDS = { + "gradle-property": {"path", "kind", "key"}, + "cocoapods": {"path", "kind", "package"}, + "archive-name": {"path", "kind", "prefix", "suffix"}, +} +SEMVER_CAPTURE = r"([0-9]+\.[0-9]+\.[0-9]+)" +SDK_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") +DEFAULT_PLATFORM_TARGETS = { + "android": "Android/APIExample/", + "ios": "iOS/APIExample/", + "macos": "macOS/", + "windows": "windows/", +} +PLATFORM_UNIT_GROUPS = { + "Android full": "android", + "Android audio": "android", + "Android Compose": "android", + "iOS UIKit": "ios", + "iOS SwiftUI": "ios", + "iOS Objective-C": "ios", + "iOS audio": "ios", + "macOS": "macos", + "Windows": "windows", +} +ROLE_CONTRACTS = { + "contract": [ + "shared scenario, APIs, and non-goals", + "cross-platform behavior contract", + "target project and allowed files for every platform", + "reference contract when required", + ], + "implementation": [ + "target project", + "query-cases and upsert-case results", + "changed files", + "matrix update proposals", + ], + "verification": [ + "findings-first review and parity result", + "entry point and UX consistency", + "exact target command strings and command kinds for JSONL evidence binding", + "target build commands and result", + "skipped checks with reasons", + ], +} + + +def select_execution_unit(units, feature=None, index=0): + filtered = [unit for unit in units if feature is None or unit["feature"] == feature] + if not filtered: + suffix = f" for feature={feature}" if feature else "" + raise ValueError(f"no execution units found{suffix}") + if index < 0 or index >= len(filtered): + raise ValueError(f"execution unit index {index} is out of range; {len(filtered)} unit(s) available") + return filtered[index] + + +def source_case_from_candidate(candidate): + if not candidate: + return "" + return f"{candidate['project'].rstrip('/')}/{candidate['path'].lstrip('/')}" + + +def resolve_source_case(candidate): + source_case = source_case_from_candidate(candidate) + if not source_case: + return "" + direct_path = REPO_ROOT / source_case + if direct_path.exists(): + return source_case + project_root = REPO_ROOT / candidate["project"] + basename = Path(candidate["path"].rstrip("/")).name + if not project_root.exists() or not basename: + return source_case + matches = [ + path + for path in project_root.rglob(basename) + if "build" not in path.relative_to(project_root).parts + ] + if not matches: + return source_case + matches.sort(key=lambda path: (len(path.parts), path.as_posix())) + return matches[0].relative_to(REPO_ROOT).as_posix() + + +def pending_dispatch(name): + return { + "mode": "pending", + "prompt": f"role-prompts/{name}.md", + "artifact": f"role-artifacts/{name}.json", + } + + +def pending_artifact(name, summary, output): + return { + "agent_id": f"{name}-agent-pending", + "dispatch": pending_dispatch(name), + "status": "BLOCKED", + "evidence": f"Pending {name} gate.", + "summary": summary, + "output": output, + } + + +def default_platform_targets(): + return { + platform: { + "required": True, + "target_project": target, + "key_constraints": [ + f"Use the {platform} project rules and keep source changes inside {target}" + ], + "files_allowed": [target], + "waiver_reason": "", + } + for platform, target in DEFAULT_PLATFORM_TARGETS.items() + } + + +def load_repository_profile(profile_path=DEFAULT_REPOSITORY_PROFILE): + path = Path(profile_path) + profile = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(profile, dict): + raise ValueError("repository profile must be a JSON object") + if set(profile) != {"version", "sdk_version_sources"}: + raise ValueError("repository profile must contain only version and sdk_version_sources") + if profile.get("version") != 1: + raise ValueError("repository profile version must be 1") + sources = profile.get("sdk_version_sources") + if not isinstance(sources, dict) or set(sources) != set(PLATFORMS): + raise ValueError("repository profile must define SDK version sources for every platform") + for platform in PLATFORMS: + entries = sources[platform] + if not isinstance(entries, list) or not entries: + raise ValueError(f"repository profile {platform} SDK version sources must be non-empty") + seen_paths = set() + for index, source in enumerate(entries): + if not isinstance(source, dict): + raise ValueError(f"repository profile {platform} source {index} must be an object") + kind = source.get("kind") + expected_fields = PROFILE_SOURCE_FIELDS.get(kind) + if expected_fields is None: + raise ValueError( + f"repository profile {platform} source {index} has unknown kind {kind}" + ) + if set(source) != expected_fields: + raise ValueError( + f"repository profile {platform} source {index} must contain " + + ", ".join(sorted(expected_fields)) + ) + if not all( + isinstance(source[field], str) and source[field] for field in expected_fields + ): + raise ValueError( + f"repository profile {platform} source {index} fields must be non-empty strings" + ) + source_path = Path(source["path"]) + if source_path.is_absolute() or ".." in source_path.parts: + raise ValueError( + "repository profile source path must be repository-relative: " + + source["path"] + ) + normalized_path = source_path.as_posix() + if normalized_path in seen_paths: + raise ValueError( + f"repository profile contains duplicate source path: {normalized_path}" + ) + seen_paths.add(normalized_path) + return profile + + +def resolve_target_sdk_versions(target_sdk_version, platform_sdk_versions=None): + if not target_sdk_version: + raise ValueError("target_sdk_version is required") + if not SDK_VERSION_RE.fullmatch(str(target_sdk_version)): + raise ValueError("target_sdk_version must use x.y.z format") + versions = {platform: target_sdk_version for platform in PLATFORMS} + seen = set() + for value in platform_sdk_versions or []: + if not isinstance(value, str) or value.count("=") != 1: + raise ValueError("platform SDK version override must use platform=x.y.z") + platform, version = value.split("=", 1) + if platform not in PLATFORMS: + raise ValueError(f"unknown platform: {platform}") + if platform in seen: + raise ValueError(f"duplicate platform SDK version: {platform}") + if not SDK_VERSION_RE.fullmatch(version): + raise ValueError(f"{platform} SDK version must use x.y.z format") + seen.add(platform) + versions[platform] = version + return versions + + +def sdk_version_pattern(source): + kind = source["kind"] + if kind == "gradle-property": + return rf"(?m)^\s*{re.escape(source['key'])}\s*=\s*{SEMVER_CAPTURE}\s*$" + if kind == "cocoapods": + package = re.escape(source["package"]) + return rf"pod\s+['\"]{package}['\"]\s*,\s*['\"]{SEMVER_CAPTURE}['\"]" + if kind == "archive-name": + return re.escape(source["prefix"]) + SEMVER_CAPTURE + re.escape(source["suffix"]) + raise ValueError(f"unsupported SDK version source kind: {kind}") + + +def collect_sdk_version_checks( + target_sdk_versions, + repo_root=REPO_ROOT, + sources=None, + profile_path=DEFAULT_REPOSITORY_PROFILE, +): + if sources is None: + sources = load_repository_profile(profile_path)["sdk_version_sources"] + checks = [] + for platform in PLATFORMS: + target_sdk_version = target_sdk_versions[platform] + entries = sources[platform] + actual_versions = {} + problems = [] + for source in entries: + path_text = source["path"] + path = Path(repo_root) / path_text + if not path.exists(): + actual_versions[path_text] = "" + problems.append(f"missing {path_text}") + continue + pattern = sdk_version_pattern(source) + matches = sorted(set(re.findall(pattern, path.read_text(encoding="utf-8")))) + if len(matches) != 1: + actual_versions[path_text] = "" + problems.append(f"expected one SDK version in {path_text}, found {len(matches)}") + continue + actual_versions[path_text] = matches[0] + if matches[0] != target_sdk_version: + problems.append(f"{path_text}={matches[0]}") + result = "PASS" if not problems else "BLOCKED" + checks.append( + { + "name": f"sdk-version-{platform}", + "result": result, + "expected_version": target_sdk_version, + "actual_versions": actual_versions, + "evidence": ( + "; ".join(f"{path}={version}" for path, version in actual_versions.items()) + if actual_versions + else "" + ), + "reason": "" if not problems else f"Expected {target_sdk_version}; " + "; ".join(problems), + } + ) + return checks + + +def build_manifest_seed(requirement, source_case, version_sources): + reference_required = bool(source_case) + reference_result = "BLOCKED" if reference_required else "SKIPPED" + targets = default_platform_targets() + contract = pending_artifact( + "contract", + "Pending shared product, architecture, and reference contract.", + { + "scenario": f"Implement {requirement['feature']} consistently across official platforms.", + "key_apis": requirement["key_apis"], + "non_goals": [], + "reference": { + "required": reference_required, + "source_case": source_case, + "contract_result": reference_result, + }, + "cross_platform_requirements": [], + "platform_targets": targets, + }, + ) + platforms = {} + for platform, target in DEFAULT_PLATFORM_TARGETS.items(): + platforms[platform] = { + "implementation": pending_artifact( + f"{platform}-implementation", + f"Pending {platform} implementation.", + { + "target_project": target, + "query_cases": "Pending target project case query.", + "upsert_case": "Pending target project update.", + "files_changed": [], + "matrix_updates": [], + }, + ), + "verification": pending_artifact( + f"{platform}-verification", + f"Pending independent {platform} verification.", + { + "result": "BLOCKED", + "findings": [], + "parity_result": reference_result, + "entry_point": f"Pending {platform} entry point verification.", + "ux_notes": "Pending verification.", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [], + }, + ), + } + return { + "version": 4, + "final_status": "BLOCKED", + "requirement": requirement, + "contract": contract, + "platforms": platforms, + "cross_platform_acceptance": { + "result": "BLOCKED", + "evidence": "Pending required platform verification.", + "differences": [], + }, + "release": { + "required": True, + "target_sdk_versions": dict(requirement["target_sdk_versions"]), + "checks": collect_sdk_version_checks( + requirement["target_sdk_versions"], sources=version_sources + ), + "skipped_checks": [], + }, + "knowledge_updates": [], + } + + +def build_platform_units(units, feature): + result = { + platform: { + "target_project": DEFAULT_PLATFORM_TARGETS[platform], + "matrix_candidates": [], + } + for platform in PLATFORMS + } + for unit in units: + if unit["feature"] != feature: + continue + platform = PLATFORM_UNIT_GROUPS.get(unit["platform_unit"]) + if platform: + result[platform]["matrix_candidates"].append(unit) + return result + + +def prepare_case_execution( + matrix_path, + feature=None, + index=0, + sdk_family=None, + key_apis=None, + target_sdk_version=None, + platform_sdk_versions=None, + repository_profile=DEFAULT_REPOSITORY_PROFILE, +): + target_sdk_versions = resolve_target_sdk_versions( + target_sdk_version, platform_sdk_versions + ) + profile = load_repository_profile(repository_profile) + backlog = generate_execution_units(matrix_path) + matching = [ + unit for unit in backlog["execution_units"] if feature is None or unit["feature"] == feature + ] + selected = select_execution_unit(matching, None, index) if matching else None + if selected is None and not (feature and sdk_family and key_apis): + raise ValueError( + "a requirement outside the actionable matrix needs feature, sdk_family, and key_apis" + ) + requirement = { + "feature": feature or selected["feature"], + "sdk_family": sdk_family or selected["sdk_family"], + "key_apis": key_apis or selected["key_apis"], + "target_sdk_versions": target_sdk_versions, + } + source_candidate = None + if selected and selected["reference_candidates"]: + source_candidate = selected["reference_candidates"][0] + source_case = resolve_source_case(source_candidate) + blockers = [] + if not source_case: + blockers.append("No DONE reference candidate found; Contract must identify a source case.") + return { + "requirement": requirement, + "platform_units": build_platform_units(backlog["execution_units"], requirement["feature"]), + "reference_contract": { + "required": bool(source_case), + "source_candidate": source_candidate, + "source_case": source_case, + }, + "role_contracts": ROLE_CONTRACTS, + "execution_steps": [ + "Run one shared Contract for all official platforms.", + "Run Android, iOS, macOS, and Windows Implementation agents independently with attributed deltas.", + "Run independent platform Verification agents in parallel.", + "Record cross-platform differences and final acceptance.", + "Validate the manifest before applying matrix updates.", + ], + "acceptance_manifest_seed": build_manifest_seed( + requirement, source_case, profile["sdk_version_sources"] + ), + "validation_command": "python3 docs/ai-engineering/tools/validate_acceptance_manifest.py ", + "blockers": blockers, + } + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--matrix", default=str(DEFAULT_MATRIX)) + parser.add_argument("--feature", help="Requirement feature; defaults to the highest-priority gap") + parser.add_argument("--sdk-family", help="Required when the feature is not actionable in the matrix") + parser.add_argument("--key-api", action="append", help="Key SDK API; repeat for multiple APIs") + parser.add_argument("--target-sdk-version", required=True) + parser.add_argument( + "--platform-sdk-version", + action="append", + default=[], + help="Override one platform as platform=x.y.z; repeat as needed", + ) + parser.add_argument("--repository-profile", default=str(DEFAULT_REPOSITORY_PROFILE)) + parser.add_argument("--index", type=int, default=0) + parser.add_argument("--output") + args = parser.parse_args(argv) + try: + package = prepare_case_execution( + Path(args.matrix), + args.feature, + index=args.index, + sdk_family=args.sdk_family, + key_apis=args.key_api, + target_sdk_version=args.target_sdk_version, + platform_sdk_versions=args.platform_sdk_version, + repository_profile=Path(args.repository_profile), + ) + except (OSError, ValueError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + payload = json.dumps(package, indent=2, ensure_ascii=False) + "\n" + if args.output: + Path(args.output).write_text(payload, encoding="utf-8") + else: + print(payload, end="") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/ai-engineering/tools/prepare_case_execution_test.py b/docs/ai-engineering/tools/prepare_case_execution_test.py new file mode 100644 index 000000000..ea50dcd24 --- /dev/null +++ b/docs/ai-engineering/tools/prepare_case_execution_test.py @@ -0,0 +1,292 @@ +import json +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + + +TOOLS_DIR = Path(__file__).resolve().parent +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +from prepare_case_execution import ( + collect_sdk_version_checks, + load_repository_profile, + prepare_case_execution, + resolve_target_sdk_versions, +) +from validate_acceptance_manifest import validate_manifest + + +PLATFORMS = ["android", "ios", "macos", "windows"] + + +class PrepareCaseExecutionTest(unittest.TestCase): + TARGET_SDK_VERSION = "4.6.4" + + def target_sdk_versions(self, **overrides): + versions = {platform: self.TARGET_SDK_VERSION for platform in PLATFORMS} + versions.update(overrides) + return versions + + def write_matrix(self): + handle = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) + handle.write( + textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | + | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(app/JoinChannelAudio.java)` | `DONE/APIExample/JoinChannelAudio.swift` | `PARTIAL(APIExample/JoinChannelAudio.swift)` | `MISSING` | Keep all official platforms aligned. | + + ## Confirmed Gaps + + | Gap | Affected Units | Severity | + | --- | --- | --- | + | Basic audio-only join channel | Windows | High - missing foundational case | + """ + ) + ) + handle.close() + self.addCleanup(lambda: Path(handle.name).unlink(missing_ok=True)) + return Path(handle.name) + + def test_prepares_one_requirement_with_four_platform_delivery_units(self): + package = prepare_case_execution( + self.write_matrix(), + feature="Join channel audio", + target_sdk_version=self.TARGET_SDK_VERSION, + ) + + manifest = package["acceptance_manifest_seed"] + self.assertEqual(manifest["version"], 4) + self.assertEqual(sorted(manifest["platforms"]), PLATFORMS) + self.assertIn("contract", manifest) + self.assertNotIn("roles", manifest) + self.assertEqual(sorted(package["role_contracts"]), ["contract", "implementation", "verification"]) + self.assertEqual(sorted(manifest["contract"]["output"]["platform_targets"]), PLATFORMS) + self.assertEqual( + manifest["requirement"]["target_sdk_versions"], + self.target_sdk_versions(), + ) + self.assertTrue(manifest["release"]["required"]) + self.assertEqual( + manifest["release"]["target_sdk_versions"], + self.target_sdk_versions(), + ) + self.assertNotIn("qa_acceptance", manifest["release"]) + self.assertNotIn("publication_channel", manifest["requirement"]) + self.assertNotIn("publication", manifest["release"]) + for platform in PLATFORMS: + unit = manifest["platforms"][platform] + self.assertEqual(sorted(unit), ["implementation", "verification"]) + self.assertEqual(unit["implementation"]["dispatch"]["mode"], "pending") + self.assertEqual(unit["verification"]["status"], "BLOCKED") + self.assertEqual(validate_manifest(manifest), []) + + def test_platform_defaults_select_official_full_sdk_projects(self): + package = prepare_case_execution( + self.write_matrix(), + feature="Join channel audio", + target_sdk_version=self.TARGET_SDK_VERSION, + ) + targets = package["acceptance_manifest_seed"]["contract"]["output"]["platform_targets"] + + self.assertEqual(targets["android"]["target_project"], "Android/APIExample/") + self.assertEqual(targets["ios"]["target_project"], "iOS/APIExample/") + self.assertEqual(targets["macos"]["target_project"], "macOS/") + self.assertEqual(targets["windows"]["target_project"], "windows/") + self.assertTrue(all(target["required"] for target in targets.values())) + + def test_selects_highest_priority_feature_when_omitted(self): + package = prepare_case_execution( + self.write_matrix(), target_sdk_version=self.TARGET_SDK_VERSION + ) + + self.assertEqual(package["requirement"]["feature"], "Join channel audio") + self.assertEqual(package["requirement"]["key_apis"], ["joinChannel", "setAudioProfile"]) + + def test_package_is_json_serializable(self): + serialized = json.dumps( + prepare_case_execution( + self.write_matrix(), target_sdk_version=self.TARGET_SDK_VERSION + ) + ) + self.assertIn('"version": 4', serialized) + + def test_prepares_new_requirement_not_yet_present_in_matrix(self): + package = prepare_case_execution( + self.write_matrix(), + feature="Spatial audio", + sdk_family="Full RTC", + key_apis=["enableSpatialAudio"], + target_sdk_version=self.TARGET_SDK_VERSION, + ) + + self.assertEqual(package["requirement"]["feature"], "Spatial audio") + self.assertEqual(package["requirement"]["key_apis"], ["enableSpatialAudio"]) + self.assertEqual(sorted(package["acceptance_manifest_seed"]["platforms"]), PLATFORMS) + + def test_requires_target_sdk_version(self): + with self.assertRaisesRegex(ValueError, "target_sdk_version is required"): + prepare_case_execution(self.write_matrix(), feature="Join channel audio") + + def test_platform_sdk_version_overrides_the_shared_baseline(self): + package = prepare_case_execution( + self.write_matrix(), + feature="Join channel audio", + target_sdk_version="4.6.2", + platform_sdk_versions=["android=4.6.3"], + ) + + expected = self.target_sdk_versions( + android="4.6.3", ios="4.6.2", macos="4.6.2", windows="4.6.2" + ) + manifest = package["acceptance_manifest_seed"] + self.assertEqual(package["requirement"]["target_sdk_versions"], expected) + self.assertEqual(manifest["release"]["target_sdk_versions"], expected) + self.assertEqual( + { + check["name"]: check["expected_version"] + for check in manifest["release"]["checks"] + }, + {f"sdk-version-{platform}": version for platform, version in expected.items()}, + ) + + def test_rejects_invalid_platform_sdk_version_overrides(self): + cases = [ + (["android"], "must use platform=x.y.z"), + (["linux=4.6.3"], "unknown platform: linux"), + (["android=next"], "android SDK version must use x.y.z format"), + (["android=4.6.3", "android=4.6.4"], "duplicate platform SDK version: android"), + ] + + for overrides, message in cases: + with self.subTest(overrides=overrides), self.assertRaisesRegex(ValueError, message): + resolve_target_sdk_versions("4.6.2", overrides) + + def test_rejects_invalid_baseline_sdk_version(self): + with self.assertRaisesRegex(ValueError, "target_sdk_version must use x.y.z format"): + resolve_target_sdk_versions("next") + + def test_collects_live_sdk_version_evidence(self): + for distribution in ["Shengwang", "Agora"]: + with self.subTest(distribution=distribution), tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + profile_path = self.write_repository_profile(root, distribution) + + profile = load_repository_profile(profile_path) + checks = collect_sdk_version_checks( + self.target_sdk_versions(), + repo_root=root, + sources=profile["sdk_version_sources"], + ) + + self.assertTrue(all(check["result"] == "PASS" for check in checks)) + self.assertTrue( + all( + set(check["actual_versions"].values()) == {self.TARGET_SDK_VERSION} + for check in checks + ) + ) + + archive = root / "windows/install.ps1" + archive.write_text( + f"{distribution}_Native_SDK_for_Windows_v4.6.2_FULL.zip\n", + encoding="utf-8", + ) + checks = collect_sdk_version_checks( + self.target_sdk_versions(), + repo_root=root, + profile_path=profile_path, + ) + windows = next( + check for check in checks if check["name"] == "sdk-version-windows" + ) + self.assertEqual(windows["result"], "BLOCKED") + self.assertIn("4.6.2", windows["reason"]) + + def test_collects_staggered_platform_sdk_versions(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + profile_path = self.write_repository_profile(root, "Agora") + (root / "Android/gradle.properties").write_text( + "rtc_sdk_version = 4.6.3\n", encoding="utf-8" + ) + for path_text in ["iOS/Podfile", "macOS/Podfile", "windows/install.ps1"]: + path = root / path_text + path.write_text( + path.read_text(encoding="utf-8").replace("4.6.4", "4.6.2"), + encoding="utf-8", + ) + + targets = { + "android": "4.6.3", + "ios": "4.6.2", + "macos": "4.6.2", + "windows": "4.6.2", + } + checks = collect_sdk_version_checks( + targets, repo_root=root, profile_path=profile_path + ) + + self.assertTrue(all(check["result"] == "PASS" for check in checks)) + self.assertEqual( + {check["name"]: check["expected_version"] for check in checks}, + {f"sdk-version-{platform}": version for platform, version in targets.items()}, + ) + + def write_repository_profile(self, root, distribution): + sources = { + "android": [ + { + "path": "Android/gradle.properties", + "kind": "gradle-property", + "key": "rtc_sdk_version", + } + ], + "ios": [ + { + "path": "iOS/Podfile", + "kind": "cocoapods", + "package": f"{distribution}RtcEngine_iOS", + } + ], + "macos": [ + { + "path": "macOS/Podfile", + "kind": "cocoapods", + "package": f"{distribution}RtcEngine_macOS", + } + ], + "windows": [ + { + "path": "windows/install.ps1", + "kind": "archive-name", + "prefix": f"{distribution}_Native_SDK_for_Windows_v", + "suffix": "_FULL.zip", + } + ], + } + contents = { + "Android/gradle.properties": "rtc_sdk_version = 4.6.4\n", + "iOS/Podfile": f"pod '{distribution}RtcEngine_iOS', '4.6.4'\n", + "macOS/Podfile": f"pod '{distribution}RtcEngine_macOS', '4.6.4'\n", + "windows/install.ps1": ( + f"{distribution}_Native_SDK_for_Windows_v4.6.4_FULL.zip\n" + ), + } + for path_text, content in contents.items(): + path = root / path_text + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + profile_path = root / "repository-profile.json" + profile_path.write_text( + json.dumps({"version": 1, "sdk_version_sources": sources}), + encoding="utf-8", + ) + return profile_path + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest.py b/docs/ai-engineering/tools/validate_acceptance_manifest.py new file mode 100644 index 000000000..9207a6267 --- /dev/null +++ b/docs/ai-engineering/tools/validate_acceptance_manifest.py @@ -0,0 +1,1239 @@ +#!/usr/bin/env python3 +"""Validate cross-platform API Examples acceptance manifests.""" + +import hashlib +import json +import re +import shlex +import sys +from pathlib import Path + +from generate_case_backlog import PLATFORM_PROJECTS, parse_matrix_cell + + +REPO_ROOT = Path(__file__).resolve().parents[3] +MANIFEST_VERSION = 4 +PLATFORMS = ["android", "ios", "macos", "windows"] +PLATFORM_ROOTS = { + "android": "Android/", + "ios": "iOS/", + "macos": "macOS/", + "windows": "windows/", +} +FINAL_STATUSES = {"PASS", "PASS WITH RISKS", "BLOCKED"} +RESULT_STATUSES = {"PASS", "FAIL", "BLOCKED", "SKIPPED"} +ROLE_STATUSES = {"PASS", "FAIL", "BLOCKED", "WAIVED"} +MATRIX_STATUSES = {"DONE", "PARTIAL", "MISSING", "N/A", "UNKNOWN"} +REQUIRED_TOP_LEVEL = { + "version", + "final_status", + "requirement", + "contract", + "platforms", + "cross_platform_acceptance", + "release", + "knowledge_updates", +} +REQUIREMENT_FIELDS = {"feature", "sdk_family", "key_apis", "target_sdk_versions"} +RELEASE_FIELDS = {"required", "target_sdk_versions", "checks", "skipped_checks"} +CONTRACT_OUTPUT_FIELDS = { + "scenario", + "key_apis", + "non_goals", + "reference", + "cross_platform_requirements", + "platform_targets", +} +IMPLEMENTATION_OUTPUT_FIELDS = { + "target_project", + "query_cases", + "upsert_case", + "files_changed", + "matrix_updates", +} +VERIFICATION_OUTPUT_FIELDS = { + "result", + "findings", + "parity_result", + "entry_point", + "ux_notes", + "commands", + "build_result", + "skipped_checks", +} +DISPATCH_MODES = {"codex-exec", "pending"} +COMMAND_KINDS = {"build", "test", "static", "runtime"} +REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"} +MODEL_PROVENANCE_FIELDS = ["model_profile", "model", "reasoning_effort", "codex_version"] +LEAD_AGENT_IDS = {"lead", "lead-agent", "lead_agent", "main", "main-agent", "main_agent", "coordinator"} +KNOWLEDGE_UPDATE_FIELDS = [ + "source", + "impact_platforms", + "symptom", + "root_cause", + "guardrail", + "verification", + "updated_at", +] +DURABLE_KNOWLEDGE_PATHS = { + "docs/ai-engineering/knowledge-index.md", + "docs/ai-engineering/release-iteration-gate.md", + "docs/ai-engineering/release-known-issues.md", + "docs/ai-engineering/case-maintenance-matrix.md", + "docs/ai-engineering/repository-profile.json", + "docs/ai-engineering/role-routing.json", +} +PLACEHOLDER_RE = re.compile(r"<[^>\n]+>") +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") +SDK_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") + + +def is_non_empty(value): + return value is not None and value != "" and value != [] + + +def validate_no_placeholders(value, path, errors): + if isinstance(value, str) and PLACEHOLDER_RE.search(value): + errors.append(f"{path} contains placeholder value: {value}") + elif isinstance(value, dict): + for key, child in value.items(): + validate_no_placeholders(child, f"{path}.{key}", errors) + elif isinstance(value, list): + for index, child in enumerate(value): + validate_no_placeholders(child, f"{path}[{index}]", errors) + + +def validate_target_sdk_versions(value, path, errors): + if not isinstance(value, dict): + errors.append(f"{path} must be an object") + return + if set(value) != set(PLATFORMS): + errors.append( + f"{path} must define exactly " + ", ".join(PLATFORMS) + ) + for platform in PLATFORMS: + version = value.get(platform) + if not SDK_VERSION_RE.fullmatch(str(version or "")): + errors.append(f"{path}.{platform} must use x.y.z format") + + +def validate_manifest_shape(manifest, errors): + for key in sorted(REQUIRED_TOP_LEVEL - set(manifest)): + errors.append(f"missing top-level field: {key}") + for key in sorted(set(manifest) - REQUIRED_TOP_LEVEL): + errors.append(f"unsupported top-level field: {key}") + if manifest.get("version") != MANIFEST_VERSION: + errors.append(f"version must be {MANIFEST_VERSION}") + if manifest.get("final_status") not in FINAL_STATUSES: + errors.append(f"final_status must be one of {sorted(FINAL_STATUSES)}") + requirement = manifest.get("requirement") + if not isinstance(requirement, dict): + errors.append("requirement must be an object") + else: + for field in sorted(REQUIREMENT_FIELDS): + if not is_non_empty(requirement.get(field)): + errors.append(f"requirement.{field} is required") + for field in sorted(set(requirement) - REQUIREMENT_FIELDS): + errors.append(f"unsupported requirement field: {field}") + target_sdk_versions = requirement.get("target_sdk_versions") + if is_non_empty(target_sdk_versions): + validate_target_sdk_versions( + target_sdk_versions, "requirement.target_sdk_versions", errors + ) + + +def validate_manifest(manifest): + if not isinstance(manifest, dict): + return ["manifest must be a JSON object"] + errors = [] + seen_agent_ids = {} + validate_no_placeholders(manifest, "manifest", errors) + validate_manifest_shape(manifest, errors) + targets = validate_contract(manifest, errors, seen_agent_ids) + validate_platforms(manifest, targets, errors, seen_agent_ids) + validate_unique_run_ids(manifest, errors) + validate_cross_platform_acceptance(manifest, errors) + validate_release(manifest, errors) + validate_knowledge_updates(manifest, errors) + return errors + + +def validate_contract(manifest, errors, seen_agent_ids): + contract = manifest.get("contract") + output = validate_artifact( + contract, + "contract", + CONTRACT_OUTPUT_FIELDS, + manifest.get("final_status"), + True, + errors, + seen_agent_ids, + ) + if not output: + return {} + for field in ["scenario", "key_apis"]: + if not is_non_empty(output.get(field)): + errors.append(f"contract.output.{field} is required") + requirement_apis = manifest.get("requirement", {}).get("key_apis", []) + contract_apis = output.get("key_apis", []) + if isinstance(requirement_apis, list) and isinstance(contract_apis, list): + missing_apis = [api for api in requirement_apis if api not in contract_apis] + if missing_apis: + errors.append( + "contract.output.key_apis must include every requirement.key_apis value: " + + ", ".join(str(api) for api in missing_apis) + ) + if contract.get("status") == "PASS" and not is_non_empty( + output.get("cross_platform_requirements") + ): + errors.append("contract.output.cross_platform_requirements is required when Contract passes") + reference = output.get("reference") + if not isinstance(reference, dict): + errors.append("contract.output.reference must be an object") + else: + required = bool(reference.get("required")) + source_case = reference.get("source_case") + if required: + if not is_non_empty(source_case): + errors.append("contract.output.reference.source_case is required") + elif "://" not in source_case: + validate_paths([source_case], "contract.output.reference.source_case", errors) + result = reference.get("contract_result") + validate_result(result, "contract.output.reference.contract_result", manifest.get("final_status"), errors) + if required and contract.get("status") == "PASS" and result != "PASS": + errors.append("reference-required work needs contract.output.reference.contract_result=PASS") + + targets = output.get("platform_targets") + if not isinstance(targets, dict): + errors.append("contract.output.platform_targets must be an object") + return {} + for platform in PLATFORMS: + if platform not in targets: + errors.append(f"contract.output.platform_targets missing platform: {platform}") + continue + target = targets[platform] + if not isinstance(target, dict): + errors.append(f"contract.output.platform_targets.{platform} must be an object") + continue + for field in [ + "required", + "target_project", + "key_constraints", + "files_allowed", + "waiver_reason", + ]: + if field not in target: + errors.append(f"contract.output.platform_targets.{platform}.{field} is required") + if target.get("required") is False and not is_non_empty(target.get("waiver_reason")): + errors.append( + f"contract.output.platform_targets.{platform}.waiver_reason is required when the platform is not required" + ) + if contract.get("status") == "PASS" and target.get("required") is True: + if not is_non_empty(target.get("key_constraints")): + errors.append( + f"contract.output.platform_targets.{platform}.key_constraints is required when Contract passes" + ) + if not is_non_empty(target.get("files_allowed")): + errors.append( + f"contract.output.platform_targets.{platform}.files_allowed is required when Contract passes" + ) + target_project = target.get("target_project") + if is_non_empty(target_project): + validate_paths( + [target_project], + f"contract.output.platform_targets.{platform}.target_project", + errors, + ) + if not path_is_within(target_project, PLATFORM_ROOTS[platform]): + errors.append( + f"contract.output.platform_targets.{platform}.target_project must be inside {PLATFORM_ROOTS[platform]}" + ) + validate_paths( + target.get("files_allowed", []), + f"contract.output.platform_targets.{platform}.files_allowed", + errors, + ) + allowed_paths = target.get("files_allowed", []) + for allowed_path in allowed_paths if isinstance(allowed_paths, list) else []: + if not path_is_within(allowed_path, PLATFORM_ROOTS[platform]): + errors.append( + f"contract.output.platform_targets.{platform}.files_allowed must stay inside {PLATFORM_ROOTS[platform]}: {allowed_path}" + ) + for platform in sorted(set(targets) - set(PLATFORMS)): + errors.append(f"contract.output.platform_targets has unsupported platform: {platform}") + return targets + + +def validate_platforms(manifest, targets, errors, seen_agent_ids): + platforms = manifest.get("platforms") + if not isinstance(platforms, dict): + errors.append("platforms must be an object") + return + for platform in PLATFORMS: + if platform not in platforms: + errors.append(f"platforms missing required platform: {platform}") + continue + unit = platforms[platform] + if not isinstance(unit, dict): + errors.append(f"platforms.{platform} must be an object") + continue + target = targets.get(platform, {}) if isinstance(targets, dict) else {} + required = bool(target.get("required", True)) + implementation = validate_artifact( + unit.get("implementation"), + f"platforms.{platform}.implementation", + IMPLEMENTATION_OUTPUT_FIELDS, + manifest.get("final_status"), + required, + errors, + seen_agent_ids, + ) + verification = validate_artifact( + unit.get("verification"), + f"platforms.{platform}.verification", + VERIFICATION_OUTPUT_FIELDS, + manifest.get("final_status"), + required, + errors, + seen_agent_ids, + ) + validate_platform_implementation(platform, target, implementation, verification, manifest, errors) + verification_dispatch = ( + unit.get("verification", {}).get("dispatch", {}) + if isinstance(unit.get("verification"), dict) + else {} + ) + validate_platform_verification( + platform, required, verification, verification_dispatch, manifest, errors + ) + for role in ["implementation", "verification"]: + artifact = unit.get(role, {}) + dispatch = artifact.get("dispatch", {}) if isinstance(artifact, dict) else {} + working_directory = dispatch.get("working_directory") + expected_working_directory = target.get("target_project") + if ( + is_non_empty(working_directory) + and expected_working_directory + and normalize_manifest_path(working_directory).rstrip("/") + != normalize_manifest_path(expected_working_directory).rstrip("/") + ): + errors.append( + f"platforms.{platform}.{role}.dispatch.working_directory must match Contract target {expected_working_directory}" + ) + if not required: + for role in ["implementation", "verification"]: + artifact = unit.get(role, {}) + if artifact.get("status") != "WAIVED": + errors.append(f"platforms.{platform}.{role}.status must be WAIVED when platform is not required") + for platform in sorted(set(platforms) - set(PLATFORMS)): + errors.append(f"platforms has unsupported platform: {platform}") + + +def validate_artifact(artifact, path, required_output, final_status, required, errors, seen_agent_ids): + if not isinstance(artifact, dict): + errors.append(f"{path} must be an object") + return {} + status = artifact.get("status") + if status not in ROLE_STATUSES: + errors.append(f"{path}.status must be one of {sorted(ROLE_STATUSES)}") + elif required and status == "WAIVED": + errors.append( + f"{path}.status=WAIVED is only valid when the platform is not required" + ) + elif required and status in {"FAIL", "BLOCKED"} and final_status != "BLOCKED": + errors.append(f"{path}.status={status} requires final_status=BLOCKED") + if status == "WAIVED" and not is_non_empty(artifact.get("waiver_reason")): + errors.append(f"{path}.waiver_reason is required when status is WAIVED") + for field in ["agent_id", "evidence", "summary"]: + if not is_non_empty(artifact.get(field)): + errors.append(f"{path}.{field} is required") + agent_id = artifact.get("agent_id") + if is_non_empty(agent_id): + normalized = str(agent_id).lower() + if normalized in LEAD_AGENT_IDS: + errors.append(f"{path}.agent_id cannot be the Lead Agent") + elif normalized in seen_agent_ids: + errors.append(f"{path}.agent_id duplicates {seen_agent_ids[normalized]}.agent_id") + else: + seen_agent_ids[normalized] = path + validate_dispatch(path, artifact.get("dispatch"), status, final_status, required, errors) + if path.endswith(".implementation") and required and final_status != "BLOCKED": + dispatch = artifact.get("dispatch", {}) + if not is_non_empty(dispatch.get("repository_delta")): + errors.append(f"{path}.dispatch.repository_delta is required") + else: + validate_run_relative_path( + dispatch["repository_delta"], f"{path}.dispatch.repository_delta", errors + ) + value = dispatch.get("repository_delta_sha256") + if not isinstance(value, str) or not SHA256_RE.fullmatch(value): + errors.append(f"{path}.dispatch.repository_delta_sha256 must be a lowercase sha256 hex digest") + output = artifact.get("output") + if not isinstance(output, dict): + errors.append(f"{path}.output must be an object") + return {} + for field in sorted(required_output - set(output)): + errors.append(f"{path}.output.{field} is required") + return output + + +def validate_dispatch(path, dispatch, status, final_status, required, errors): + field_name = f"{path}.dispatch" + if not isinstance(dispatch, dict): + errors.append(f"{field_name} is required") + return + mode = dispatch.get("mode") + if mode not in DISPATCH_MODES: + errors.append(f"{field_name}.mode must be one of {sorted(DISPATCH_MODES)}") + for field in ["prompt", "artifact"]: + if not is_non_empty(dispatch.get(field)): + errors.append(f"{field_name}.{field} is required") + else: + validate_run_relative_path(dispatch[field], f"{field_name}.{field}", errors) + if is_non_empty(dispatch.get("input_snapshot")): + validate_run_relative_path(dispatch["input_snapshot"], f"{field_name}.input_snapshot", errors) + if required and mode == "pending" and final_status != "BLOCKED" and status != "WAIVED": + errors.append(f"{field_name}.mode=pending requires final_status=BLOCKED") + if final_status == "BLOCKED" or status == "WAIVED" or not required: + return + if mode != "codex-exec": + errors.append(f"{field_name}.mode={mode} cannot pass final acceptance") + for field in [ + "run_id", + "evidence", + "input_snapshot", + "working_directory", + "command_log", + "host_platform", + *MODEL_PROVENANCE_FIELDS, + ]: + if not is_non_empty(dispatch.get(field)): + errors.append(f"{field_name}.{field} is required when final_status is not BLOCKED") + if is_non_empty(dispatch.get("working_directory")): + validate_paths( + [dispatch["working_directory"]], f"{field_name}.working_directory", errors + ) + if is_non_empty(dispatch.get("command_log")): + validate_run_relative_path( + dispatch["command_log"], f"{field_name}.command_log", errors + ) + effort = dispatch.get("reasoning_effort") + if is_non_empty(effort) and effort not in REASONING_EFFORTS: + errors.append(f"{field_name}.reasoning_effort must be one of {sorted(REASONING_EFFORTS)}") + for field in [ + "prompt_sha256", + "artifact_sha256", + "input_snapshot_sha256", + "command_log_sha256", + ]: + value = dispatch.get(field) + if not is_non_empty(value): + errors.append(f"{field_name}.{field} is required when final_status is not BLOCKED") + elif not isinstance(value, str) or not SHA256_RE.fullmatch(value): + errors.append(f"{field_name}.{field} must be a lowercase sha256 hex digest") + + +def validate_platform_implementation(platform, target, output, verification, manifest, errors): + if not output: + return + expected_target = target.get("target_project") if isinstance(target, dict) else None + if expected_target and output.get("target_project") != expected_target: + errors.append( + f"platforms.{platform}.implementation.output.target_project must match contract target {expected_target}" + ) + for field in ["query_cases", "upsert_case"]: + if not is_non_empty(output.get(field)): + errors.append(f"platforms.{platform}.implementation.output.{field} is required") + validate_paths( + output.get("files_changed", []), + f"platforms.{platform}.implementation.output.files_changed", + errors, + require_exists=False, + ) + allowed = target.get("files_allowed", []) if isinstance(target, dict) else [] + for changed_path in output.get("files_changed", []): + if not any(path_is_within(changed_path, allowed_path) for allowed_path in allowed): + errors.append( + f"platforms.{platform}.implementation.output.files_changed is outside Contract files_allowed: {changed_path}" + ) + updates = output.get("matrix_updates", []) + if not isinstance(updates, list): + errors.append(f"platforms.{platform}.implementation.output.matrix_updates must be an array") + return + for index, update in enumerate(updates): + path = f"platforms.{platform}.implementation.output.matrix_updates[{index}]" + if not isinstance(update, dict): + errors.append(f"{path} must be an object") + continue + for field in ["feature", "platform_unit", "from", "to", "to_cell", "evidence"]: + if not is_non_empty(update.get(field)): + errors.append(f"{path}.{field} is required") + if update.get("feature") != manifest.get("requirement", {}).get("feature"): + errors.append(f"{path}.feature must match requirement.feature") + target_project = target.get("target_project") if isinstance(target, dict) else "" + expected_units = { + unit + for unit, project in PLATFORM_PROJECTS.items() + if path_is_within(target_project, project) or path_is_within(project, target_project) + } + if update.get("platform_unit") not in expected_units: + errors.append( + f"{path}.platform_unit must match Contract target {target_project}; " + f"expected one of {sorted(expected_units)}" + ) + if update.get("from") not in MATRIX_STATUSES: + errors.append(f"{path}.from must be one of {sorted(MATRIX_STATUSES)}") + if update.get("to") not in MATRIX_STATUSES: + errors.append(f"{path}.to must be one of {sorted(MATRIX_STATUSES)}") + to_cell = update.get("to_cell") + if is_non_empty(to_cell): + cell_status, _ = parse_matrix_cell(to_cell) + if cell_status != update.get("to"): + errors.append(f"{path}.to_cell status must match to={update.get('to')}") + if any(character in str(to_cell) for character in "\n\r|`"): + errors.append(f"{path}.to_cell contains unsupported Markdown characters") + if update.get("to") == "DONE": + if manifest.get("final_status") != "PASS": + errors.append(f"{update.get('platform_unit')} matrix update to DONE requires final_status=PASS") + if verification.get("build_result") != "PASS": + errors.append( + f"{update.get('platform_unit')} matrix update to DONE requires build_result=PASS" + ) + if verification.get("parity_result") != "PASS": + errors.append( + f"{update.get('platform_unit')} matrix update to DONE requires parity_result=PASS" + ) + if verification.get("skipped_checks"): + errors.append(f"{update.get('platform_unit')} matrix update to DONE cannot include skipped checks") + if isinstance(target, dict) and target.get("required") is False: + if output.get("files_changed"): + errors.append(f"platforms.{platform}.implementation.output.files_changed must be empty when platform is not required") + if updates: + errors.append(f"platforms.{platform}.implementation.output.matrix_updates must be empty when platform is not required") + + +def is_platform_build_command(platform, command, working_directory=None): + if ( + platform not in PLATFORM_ROOTS + or not isinstance(command, str) + or not is_non_empty(working_directory) + or not path_is_within(working_directory, PLATFORM_ROOTS[platform]) + ): + return False + try: + outer_tokens = shlex.split(command) + except ValueError: + return False + if not outer_tokens: + return False + executable = outer_tokens[0].replace("\\", "/").rsplit("/", 1)[-1].lower() + if executable in {"sh", "bash", "zsh"}: + command_option = next( + ( + index + for index, token in enumerate(outer_tokens[:-1]) + if token.startswith("-") and "c" in token + ), + None, + ) + if command_option is None: + return False + command = outer_tokens[command_option + 1] + elif executable in {"cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe"}: + command_option = next( + ( + index + for index, token in enumerate(outer_tokens[:-1]) + if token.lower() in {"/c", "-command", "-c"} + ), + None, + ) + if command_option is None: + return False + command = " ".join(outer_tokens[command_option + 1 :]) + + try: + lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") + lexer.whitespace_split = True + lexer.commenters = "" + tokens = list(lexer) + except ValueError: + return False + segments = [[]] + for token in tokens: + if token in {";", "|", "||", "&"}: + return False + if token == "&&": + if not segments[-1]: + return False + segments.append([]) + else: + segments[-1].append(token) + if not segments[-1]: + return False + if len(segments) != 1: + return False + final = segments[-1] + if command_overrides_path(final): + return False + remaining = command_tokens_without_environment(final) + if not remaining: + return False + raw_executable = remaining[0].replace("\\", "/").lower() + final_executable, arguments = command_executable(final), command_arguments(final) + if platform == "android": + if raw_executable not in {"./gradlew", "gradlew.bat", "./gradlew.bat"}: + return False + normalized_arguments = [argument.lower() for argument in arguments] + blocked_modes = { + "--dry-run", + "-m", + "help", + "tasks", + "properties", + "projects", + "dependencies", + "components", + "model", + "buildenvironment", + } + if blocked_modes.intersection(normalized_arguments) or any( + argument.startswith("--dry-run=") for argument in normalized_arguments + ): + return False + if any( + argument in {"-p", "--project-dir"} or argument.startswith("--project-dir=") + for argument in normalized_arguments + ): + return False + return final_executable in {"gradlew", "gradlew.bat"} and any( + re.search(r"(?:^|:)(?:assemble|build|bundle)[A-Za-z0-9_.-]*$", argument, re.I) + for argument in arguments + ) + if platform in {"ios", "macos"}: + if raw_executable not in {"xcodebuild", "/usr/bin/xcodebuild"}: + return False + normalized_arguments = [argument.lower() for argument in arguments] + if not xcodebuild_has_only_build_arguments(arguments): + return False + for option in ["-project", "-workspace"]: + if option in normalized_arguments: + index = normalized_arguments.index(option) + if index + 1 >= len(arguments) or not is_safe_relative_build_path( + arguments[index + 1] + ): + return False + build_actions = {"build", "archive", "test", "build-for-testing"} + normalized_argument_set = set(normalized_arguments) + if normalized_argument_set & build_actions: + return True + if "clean" in normalized_argument_set: + return False + return bool(normalized_argument_set & {"-project", "-workspace", "-scheme"}) + if final_executable in {"msbuild", "msbuild.exe", "devenv", "devenv.com", "devenv.exe"}: + if "/" in raw_executable: + return False + normalized_arguments = [argument.lower() for argument in arguments] + if any( + argument in {"-version", "/version", "-help", "/help", "/?"} + for argument in normalized_arguments + ): + return False + blocked_prefixes = ( + "/preprocess", + "-preprocess", + "/pp", + "-pp", + "/getproperty", + "-getproperty", + "/getitem", + "-getitem", + "/gettargetresult", + "-gettargetresult", + "/targets", + "-targets", + "/ts", + "-ts", + "/validate", + "-validate", + "/val", + "-val", + ) + if any(argument.startswith(blocked_prefixes) for argument in normalized_arguments): + return False + project_arguments = [ + arguments[index] + for index, argument in enumerate(normalized_arguments) + if argument.endswith((".sln", ".slnx", ".vcxproj", ".proj")) + ] + if not project_arguments or not all( + is_safe_relative_build_path(argument) for argument in project_arguments + ): + return False + if final_executable.startswith("devenv"): + return any( + argument in {"/build", "/rebuild"} for argument in normalized_arguments + ) + targets = [ + argument.split(":", 1)[1] + for argument in normalized_arguments + if argument.startswith(("/t:", "-t:", "/target:", "-target:")) + ] + if targets: + target_names = { + name + for value in targets + for name in re.split(r"[;,]", value) + if name + } + return bool(target_names & {"build", "rebuild"}) + return True + if ( + raw_executable not in {"cmake", "cmake.exe"} + or final_executable not in {"cmake", "cmake.exe"} + or "--build" not in arguments + ): + return False + build_index = arguments.index("--build") + if build_index + 1 >= len(arguments) or not is_safe_relative_build_path( + arguments[build_index + 1] + ): + return False + normalized_arguments = [argument.lower() for argument in arguments] + if "--target" in normalized_arguments: + target_index = normalized_arguments.index("--target") + targets = normalized_arguments[target_index + 1 :] + if targets and all(target in {"clean", "help"} for target in targets): + return False + return True + + +def is_safe_relative_build_path(path_text): + if not isinstance(path_text, str) or not path_text: + return False + normalized = path_text.replace("\\", "/") + path = Path(normalized) + return ( + not path.is_absolute() + and not re.match(r"^[A-Za-z]:/", normalized) + and ".." not in path.parts + ) + + +def xcodebuild_has_only_build_arguments(arguments): + actions = {"build", "archive", "test", "build-for-testing", "clean"} + options_with_values = { + "-project", + "-workspace", + "-scheme", + "-target", + "-configuration", + "-destination", + "-arch", + "-sdk", + "-xcconfig", + "-deriveddatapath", + "-archivepath", + "-resultbundlepath", + "-resultstreampath", + "-packagecachepath", + "-clonedsourcepackagesdirpath", + "-jobs", + "-testplan", + "-only-testing", + "-skip-testing", + "-enablecodecoverage", + "-parallel-testing-enabled", + "-maximum-concurrent-test-device-destinations", + "-maximum-concurrent-test-simulator-destinations", + } + flags = { + "-alltargets", + "-parallelizetargets", + "-quiet", + "-verbose", + "-hideshellscriptenvironment", + "-allowprovisioningupdates", + "-allowprovisioningdeviceregistration", + "-skipunavailableactions", + "-disableautomaticpackageresolution", + "-onlyusepackageversionsfromresolvedfile", + "-skippackageupdates", + "-skippackagepluginvalidation", + "-skipmacrovalidation", + "-showbuildtimingsummary", + } + index = 0 + while index < len(arguments): + argument = arguments[index] + normalized = argument.lower() + if normalized in actions or normalized in flags or ( + "=" in argument and not argument.startswith("-") + ): + index += 1 + continue + if normalized in options_with_values: + if index + 1 >= len(arguments): + return False + index += 2 + continue + return False + return True + + +def command_tokens_without_environment(tokens): + remaining = list(tokens) + if remaining and remaining[0] == "env": + remaining.pop(0) + while remaining and (remaining[0].startswith("-") or "=" in remaining[0]): + remaining.pop(0) + while remaining and "=" in remaining[0] and not remaining[0].startswith(("/", "./")): + remaining.pop(0) + return remaining + + +def command_overrides_path(tokens): + remaining = list(tokens) + if remaining and remaining[0] == "env": + remaining.pop(0) + while remaining and remaining[0].startswith("-"): + remaining.pop(0) + for token in remaining: + if "=" not in token or token.startswith(("/", "./")): + break + name, _, _ = token.partition("=") + if name.upper() == "PATH": + return True + return False + + +def command_executable(tokens): + remaining = command_tokens_without_environment(tokens) + if not remaining: + return "" + return remaining[0].replace("\\", "/").rsplit("/", 1)[-1].lower() + + +def command_arguments(tokens): + remaining = command_tokens_without_environment(tokens) + return remaining[1:] if remaining else [] + + +def validate_platform_verification( + platform, required, output, dispatch, manifest, errors +): + if not output: + return + dispatch = dispatch if isinstance(dispatch, dict) else {} + command_log = dispatch.get("command_log") + host_platform = dispatch.get("host_platform") + for field in ["entry_point", "ux_notes"]: + if required and not is_non_empty(output.get(field)): + errors.append(f"platforms.{platform}.verification.output.{field} is required") + for field in ["result", "parity_result", "build_result"]: + validate_result( + output.get(field), + f"platforms.{platform}.verification.output.{field}", + manifest.get("final_status"), + errors, + allow_skipped=not required, + ) + commands = output.get("commands", []) + if not required: + for field in ["result", "parity_result", "build_result"]: + if output.get(field) != "SKIPPED": + errors.append( + f"platforms.{platform}.verification.output.{field} must be SKIPPED when platform is not required" + ) + if commands: + errors.append( + f"platforms.{platform}.verification.output.commands must be empty when platform is not required" + ) + if required and manifest.get("final_status") != "BLOCKED" and not commands: + errors.append(f"platforms.{platform}.verification.output.commands must not be empty") + for index, command in enumerate(commands): + if not isinstance(command, dict): + errors.append(f"platforms.{platform}.verification.output.commands[{index}] must be an object") + continue + if not command.get("command"): + errors.append(f"platforms.{platform}.verification.output.commands[{index}].command is required") + if command.get("kind") not in COMMAND_KINDS: + errors.append( + f"platforms.{platform}.verification.output.commands[{index}].kind must be one of {sorted(COMMAND_KINDS)}" + ) + elif command.get("kind") == "build" and not is_platform_build_command( + platform, command.get("command"), dispatch.get("working_directory") + ): + errors.append( + f"platforms.{platform}.verification.output.commands[{index}] is not a recognized {platform} build command" + ) + validate_result( + command.get("result"), + f"platforms.{platform}.verification.output.commands[{index}].result", + manifest.get("final_status"), + errors, + ) + if command.get("result") in {"PASS", "FAIL"} and not is_non_empty( + command.get("evidence") + ): + errors.append( + f"platforms.{platform}.verification.output.commands[{index}].evidence is required for executed commands" + ) + elif command.get("result") in {"PASS", "FAIL"} and ( + not command_log + or not str(command.get("evidence")).startswith(f"{command_log}#") + or "exit_code=" not in str(command.get("evidence")) + ): + errors.append( + f"platforms.{platform}.verification.output.commands[{index}].evidence must bind to dispatch.command_log and exit_code" + ) + for index, skipped in enumerate(output.get("skipped_checks", [])): + if not isinstance(skipped, dict) or not skipped.get("reason"): + errors.append( + f"platforms.{platform}.verification.output.skipped_checks[{index}].reason is required" + ) + if required and manifest.get("final_status") != "BLOCKED": + for field in ["result", "parity_result", "build_result"]: + if output.get(field) != "PASS": + errors.append( + f"non-BLOCKED acceptance requires platforms.{platform}.verification.output.{field}=PASS" + ) + if output.get("build_result") == "PASS" and not any( + command.get("kind") == "build" and command.get("result") == "PASS" + for command in commands + if isinstance(command, dict) + ): + errors.append( + f"platforms.{platform}.verification.output.build_result=PASS requires a bound PASS build command" + ) + if platform == "windows" and output.get("build_result") == "PASS" and host_platform != "win32": + errors.append( + "platforms.windows.verification.output.build_result=PASS requires host_platform=win32" + ) + if required and manifest.get("final_status") == "PASS" and output.get("skipped_checks"): + errors.append("final_status=PASS cannot include skipped checks") + + +def validate_cross_platform_acceptance(manifest, errors): + acceptance = manifest.get("cross_platform_acceptance") + if not isinstance(acceptance, dict): + errors.append("cross_platform_acceptance must be an object") + return + result = acceptance.get("result") + validate_result(result, "cross_platform_acceptance.result", manifest.get("final_status"), errors) + if not is_non_empty(acceptance.get("evidence")): + errors.append("cross_platform_acceptance.evidence is required") + if manifest.get("final_status") != "BLOCKED" and result != "PASS": + errors.append("non-BLOCKED acceptance requires cross_platform_acceptance.result=PASS") + + +def validate_result(result, field_name, final_status, errors, allow_skipped=False): + if result not in RESULT_STATUSES: + errors.append(f"{field_name} must be one of {sorted(RESULT_STATUSES)}") + elif result in {"FAIL", "BLOCKED"} and final_status != "BLOCKED": + errors.append(f"{field_name}={result} requires final_status=BLOCKED") + elif result == "SKIPPED" and final_status == "PASS" and not allow_skipped: + errors.append(f"{field_name}=SKIPPED cannot use final_status=PASS") + + +def validate_release(manifest, errors): + release = manifest.get("release") + if not isinstance(release, dict): + errors.append("release must be an object") + return + if release.get("required") is not True: + errors.append("release.required must be true") + for field in sorted(set(release) - RELEASE_FIELDS): + errors.append(f"unsupported release field: {field}") + requirement = manifest.get("requirement", {}) + target_sdk_versions = requirement.get("target_sdk_versions") + release_target_sdk_versions = release.get("target_sdk_versions") + if is_non_empty(release_target_sdk_versions): + validate_target_sdk_versions( + release_target_sdk_versions, "release.target_sdk_versions", errors + ) + if release_target_sdk_versions != target_sdk_versions: + errors.append( + "release.target_sdk_versions must match requirement.target_sdk_versions" + ) + checks = release.get("checks", []) + if not checks: + errors.append("release.checks must include SDK version checks") + check_names = { + check.get("name") for check in checks if isinstance(check, dict) and check.get("name") + } + expected_sdk_checks = {f"sdk-version-{platform}" for platform in PLATFORMS} + for name in sorted(expected_sdk_checks - check_names): + errors.append(f"release.checks missing required check: {name}") + for index, check in enumerate(checks): + if not isinstance(check, dict): + errors.append(f"release.checks[{index}] must be an object") + continue + if not check.get("name"): + errors.append(f"release.checks[{index}].name is required") + result = check.get("result") + validate_result(result, f"release.checks[{index}].result", manifest.get("final_status"), errors) + if result == "PASS" and not check.get("evidence"): + errors.append(f"release.checks[{index}].evidence is required when result is PASS") + if result == "SKIPPED" and not check.get("reason"): + errors.append(f"release.checks[{index}].reason is required when result is SKIPPED") + if result in {"FAIL", "BLOCKED"} and not check.get("reason"): + errors.append(f"release.checks[{index}].reason is required when result is {result}") + if check.get("name") in expected_sdk_checks: + platform = check["name"].removeprefix("sdk-version-") + target_sdk_version = ( + target_sdk_versions.get(platform) + if isinstance(target_sdk_versions, dict) + else None + ) + if target_sdk_version and check.get("expected_version") != target_sdk_version: + errors.append( + f"{check.get('name')} expected_version must match {target_sdk_version}" + ) + actual_versions = check.get("actual_versions") + if not isinstance(actual_versions, dict) or not actual_versions: + errors.append(f"{check.get('name')} actual_versions must be a non-empty object") + elif target_sdk_version and result == "PASS" and any( + version != target_sdk_version for version in actual_versions.values() + ): + errors.append( + f"{check.get('name')} actual versions must all match {target_sdk_version}" + ) + if manifest.get("final_status") != "BLOCKED" and result != "PASS": + errors.append( + f"non-BLOCKED acceptance requires {check.get('name')}=PASS" + ) + for index, skipped in enumerate(release.get("skipped_checks", [])): + if not isinstance(skipped, dict) or not skipped.get("reason"): + errors.append(f"release.skipped_checks[{index}].reason is required") + if manifest.get("final_status") != "BLOCKED" and release.get("skipped_checks"): + errors.append("non-BLOCKED acceptance cannot include skipped release checks") + +def validate_knowledge_updates(manifest, errors): + updates = manifest.get("knowledge_updates", []) + for index, update in enumerate(updates): + if not isinstance(update, dict): + errors.append(f"knowledge_updates[{index}] must be an object") + continue + for field in KNOWLEDGE_UPDATE_FIELDS: + if not is_non_empty(update.get(field)): + errors.append(f"knowledge_updates[{index}].{field} is required") + changed_files = [] + platforms = manifest.get("platforms", {}) + if isinstance(platforms, dict): + for platform in PLATFORMS: + implementation = platforms.get(platform, {}).get("implementation", {}) + output = implementation.get("output", {}) if isinstance(implementation, dict) else {} + changed_files.extend(output.get("files_changed", []) if isinstance(output, dict) else []) + if updates and not any(is_durable_knowledge_path(path) for path in changed_files): + errors.append("knowledge_updates require at least one durable knowledge doc or skill in files_changed") + + +def validate_run_relative_path(path_text, field_name, errors): + if not isinstance(path_text, str): + errors.append(f"{field_name} must be a string") + return + path = Path(path_text) + if path.is_absolute() or ".." in path.parts: + errors.append(f"{field_name} must be relative to the execution workspace") + + +def validate_unique_run_ids(manifest, errors): + seen = {} + for artifact_path, artifact in iter_manifest_artifacts(manifest): + dispatch = artifact.get("dispatch") + if not isinstance(dispatch, dict) or dispatch.get("mode") != "codex-exec": + continue + run_id = dispatch.get("run_id") + if not is_non_empty(run_id): + continue + normalized = str(run_id).lower() + if normalized in seen: + errors.append( + f"{artifact_path}.dispatch.run_id duplicates {seen[normalized]}.dispatch.run_id" + ) + else: + seen[normalized] = artifact_path + + +def resolve_repo_path(path_text): + path = Path(path_text) + resolved = path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve() + try: + resolved.relative_to(REPO_ROOT.resolve()) + except ValueError: + return None + return resolved + + +def validate_paths(values, field_name, errors, require_exists=True): + if not isinstance(values, list): + errors.append(f"{field_name} must be an array") + return + for path_text in values: + if not isinstance(path_text, str) or not path_text: + errors.append(f"{field_name} contains an empty or non-string path") + continue + if "://" in path_text: + continue + resolved = resolve_repo_path(path_text) + if resolved is None: + errors.append(f"{field_name} path must be inside the repository: {path_text}") + elif require_exists and not resolved.exists(): + errors.append(f"{field_name} path does not exist: {path_text}") + + +def is_durable_knowledge_path(path_text): + if not isinstance(path_text, str): + return False + normalized = normalize_manifest_path(path_text) + return ( + normalized in DURABLE_KNOWLEDGE_PATHS + or normalized.endswith("/ARCHITECTURE.md") + or normalized.startswith(".agent/skills/") + or "/.agent/skills/" in normalized + or normalized.startswith(".agents/skills/") + or "/.agents/skills/" in normalized + ) + + +def normalize_manifest_path(path_text): + normalized = str(path_text).replace("\\", "/") + path = Path(normalized) + if path.is_absolute(): + try: + return path.resolve().relative_to(REPO_ROOT).as_posix() + except ValueError: + return path.as_posix() + try: + return (REPO_ROOT / path).resolve().relative_to(REPO_ROOT.resolve()).as_posix() + except ValueError: + return path.as_posix() + + +def path_is_within(path_text, allowed_text): + path = normalize_manifest_path(path_text).rstrip("/") + allowed = normalize_manifest_path(allowed_text).rstrip("/") + return path == allowed or path.startswith(f"{allowed}/") + + +def iter_manifest_artifacts(manifest): + contract = manifest.get("contract") + if isinstance(contract, dict): + yield "contract", contract + platforms = manifest.get("platforms") + if not isinstance(platforms, dict): + return + for platform in PLATFORMS: + unit = platforms.get(platform) + if not isinstance(unit, dict): + continue + for role in ["implementation", "verification"]: + artifact = unit.get(role) + if isinstance(artifact, dict): + yield f"platforms.{platform}.{role}", artifact + + +def sha256_file(path): + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def resolve_evidence_path(root, path_text): + if not isinstance(path_text, str): + return None + path = Path(path_text) + if path.is_absolute() or ".." in path.parts: + return None + resolved = (root / path).resolve() + try: + resolved.relative_to(root.resolve()) + except ValueError: + return None + return resolved + + +def validate_evidence_files(manifest, evidence_root): + errors = [] + root = Path(evidence_root).resolve() + fields = [ + ("prompt", "prompt_sha256"), + ("artifact", "artifact_sha256"), + ("input_snapshot", "input_snapshot_sha256"), + ("command_log", "command_log_sha256"), + ("repository_delta", "repository_delta_sha256"), + ] + for artifact_path, artifact in iter_manifest_artifacts(manifest): + dispatch = artifact.get("dispatch") + if not isinstance(dispatch, dict) or dispatch.get("mode") != "codex-exec": + continue + for path_field, hash_field in fields: + path_text = dispatch.get(path_field) + expected = dispatch.get(hash_field) + if not is_non_empty(path_text) or not is_non_empty(expected): + continue + path = resolve_evidence_path(root, path_text) + field_name = f"{artifact_path}.dispatch.{path_field}" + if path is None: + errors.append(f"{field_name} must resolve inside the execution workspace") + elif not path.is_file(): + errors.append(f"{field_name} does not exist: {path_text}") + elif sha256_file(path) != expected: + errors.append( + f"{artifact_path}.dispatch.{hash_field} does not match {path_text}" + ) + elif path_field == "repository_delta": + try: + delta = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + errors.append(f"{field_name} is not valid JSON: {exc}") + continue + if not isinstance(delta, dict): + errors.append(f"{field_name} must contain a JSON object") + continue + delta_files = { + normalize_manifest_path(value) + for value in delta.get("changed_files", []) + if isinstance(value, str) + } + output = artifact.get("output", {}) + artifact_files = { + normalize_manifest_path(value) + for value in output.get("files_changed", []) + if isinstance(value, str) + } + if delta_files != artifact_files: + errors.append( + f"{artifact_path}.dispatch.repository_delta changed_files do not match output.files_changed" + ) + return errors + + +def main(argv): + if len(argv) != 2: + print("usage: validate_acceptance_manifest.py ", file=sys.stderr) + return 2 + manifest_path = Path(argv[1]) + try: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + except OSError as exc: + print(f"failed to read manifest: {exc}", file=sys.stderr) + return 2 + except json.JSONDecodeError as exc: + print(f"invalid JSON: {exc}", file=sys.stderr) + return 2 + errors = validate_manifest(manifest) + errors.extend(validate_evidence_files(manifest, manifest_path.parent)) + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + print("Acceptance manifest valid") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py new file mode 100644 index 000000000..e37974f26 --- /dev/null +++ b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py @@ -0,0 +1,727 @@ +import copy +import hashlib +import json +import sys +import tempfile +import unittest +from pathlib import Path + + +TOOLS_DIR = Path(__file__).resolve().parent +if str(TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(TOOLS_DIR)) + +from validate_acceptance_manifest import ( + is_durable_knowledge_path, + validate_evidence_files, + validate_manifest, +) + + +PLATFORMS = ["android", "ios", "macos", "windows"] +TARGETS = { + "android": "Android/APIExample/", + "ios": "iOS/APIExample/", + "macos": "macOS/", + "windows": "windows/", +} +SNAPSHOT_SHA256 = "c" * 64 +BUILD_COMMANDS = { + "android": "./gradlew assembleDebug", + "ios": "xcodebuild -scheme APIExample build", + "macos": "xcodebuild -scheme APIExample build", + "windows": "msbuild APIExample.sln /t:Build", +} + + +def dispatch_for(name, profile): + platform = name.split("-")[0] + dispatch = { + "mode": "codex-exec", + "run_id": f"run-{name}", + "prompt": f"role-prompts/{name}.md", + "prompt_sha256": "a" * 64, + "artifact": f"role-artifacts/{name}.json", + "artifact_sha256": "b" * 64, + "evidence": f"Independent {name} run.", + "model_profile": profile, + "model": "test-model", + "reasoning_effort": "high" if profile in {"deep", "review"} else "medium", + "codex_version": "codex-cli test", + "input_snapshot": f"input-snapshots/{name}.json", + "input_snapshot_sha256": SNAPSHOT_SHA256, + "command_log": f"dispatch-logs/{name}.jsonl", + "command_log_sha256": "d" * 64, + "working_directory": TARGETS.get(name.split("-")[0], "."), + "host_platform": "win32" if platform == "windows" else "darwin", + } + if name.endswith("-implementation"): + dispatch["repository_delta"] = f"repository-deltas/{name}.json" + dispatch["repository_delta_sha256"] = "e" * 64 + return dispatch + + +def artifact(name, profile, output): + return { + "agent_id": f"{name}-agent-1", + "dispatch": dispatch_for(name, profile), + "status": "PASS", + "evidence": f"{name} evidence", + "summary": f"{name} completed", + "output": output, + } + + +def base_manifest(): + platform_targets = { + platform: { + "required": True, + "target_project": target, + "key_constraints": [f"Keep changes in {target}"], + "files_allowed": [target], + "waiver_reason": "", + } + for platform, target in TARGETS.items() + } + platforms = {} + for platform, target in TARGETS.items(): + platforms[platform] = { + "implementation": artifact( + f"{platform}-implementation", + "deep", + { + "target_project": target, + "query_cases": "No duplicate case found.", + "upsert_case": "Platform case updated.", + "files_changed": [f"{target}AGENTS.md"], + "matrix_updates": [], + }, + ), + "verification": artifact( + f"{platform}-verification", + "review", + { + "result": "PASS", + "findings": ["No blocking findings."], + "parity_result": "PASS", + "entry_point": "Basic > Join channel audio", + "ux_notes": "Matches adjacent examples.", + "commands": [ + { + "kind": "build", + "command": BUILD_COMMANDS[platform], + "result": "PASS", + "evidence": ( + f"dispatch-logs/{platform}-verification.jsonl#build; exit_code=0" + ), + } + ], + "build_result": "PASS", + "skipped_checks": [], + }, + ), + } + return { + "version": 4, + "final_status": "PASS", + "requirement": { + "feature": "Join channel audio", + "sdk_family": "Full RTC", + "key_apis": ["joinChannel", "setAudioProfile"], + "target_sdk_versions": {platform: "4.6.4" for platform in PLATFORMS}, + }, + "contract": artifact( + "contract", + "standard", + { + "scenario": "Keep the official audio-only join sample aligned on every platform.", + "key_apis": ["joinChannel", "setAudioProfile"], + "non_goals": ["Release packaging"], + "reference": { + "required": True, + "source_case": "Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java", + "contract_result": "PASS", + }, + "cross_platform_requirements": ["Equivalent join, leave, mute, and status behavior"], + "platform_targets": platform_targets, + }, + ), + "platforms": platforms, + "cross_platform_acceptance": { + "result": "PASS", + "evidence": "All four required platform verification artifacts passed.", + "differences": [], + }, + "release": { + "required": True, + "target_sdk_versions": {platform: "4.6.4" for platform in PLATFORMS}, + "checks": [ + { + "name": f"sdk-version-{platform}", + "result": "PASS", + "expected_version": "4.6.4", + "actual_versions": {f"{platform}/version-source": "4.6.4"}, + "evidence": f"{platform} SDK sources match 4.6.4.", + "reason": "", + } + for platform in PLATFORMS + ], + "skipped_checks": [], + }, + "knowledge_updates": [], + } + + +class AcceptanceManifestValidatorTest(unittest.TestCase): + def assert_error_contains(self, manifest, expected): + errors = validate_manifest(manifest) + self.assertTrue(any(expected in error for error in errors), errors) + + def test_accepts_multi_platform_v4_manifest(self): + self.assertEqual(validate_manifest(base_manifest()), []) + + def test_rejects_legacy_single_role_tree(self): + manifest = base_manifest() + manifest["roles"] = {} + + self.assert_error_contains(manifest, "unsupported top-level field: roles") + + def test_requires_all_official_platform_units(self): + manifest = base_manifest() + del manifest["platforms"]["windows"] + + self.assert_error_contains(manifest, "platforms missing required platform: windows") + + def test_required_platform_blocker_forces_global_blocked(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["verification"]["status"] = "BLOCKED" + + self.assert_error_contains( + manifest, "platforms.windows.verification.status=BLOCKED requires final_status=BLOCKED" + ) + + def test_required_role_cannot_be_waived(self): + manifest = base_manifest() + artifact = manifest["platforms"]["android"]["verification"] + artifact["status"] = "WAIVED" + artifact["waiver_reason"] = "No Android worker." + + self.assert_error_contains( + manifest, + "platforms.android.verification.status=WAIVED is only valid when the platform is not required", + ) + + def test_blocked_manifest_accepts_pending_windows_verification(self): + manifest = base_manifest() + manifest["final_status"] = "BLOCKED" + manifest["cross_platform_acceptance"] = { + "result": "BLOCKED", + "evidence": "Windows-host verification is pending.", + "differences": [], + } + verification = manifest["platforms"]["windows"]["verification"] + verification.update( + { + "agent_id": "windows-verification-agent-pending", + "dispatch": { + "mode": "pending", + "prompt": "role-prompts/windows-verification.md", + "artifact": "role-artifacts/windows-verification.json", + }, + "status": "BLOCKED", + "evidence": "Windows target build requires a Windows host.", + "summary": "Windows verification pending.", + "output": { + "result": "BLOCKED", + "findings": [], + "parity_result": "BLOCKED", + "entry_point": "Pending Windows verification.", + "ux_notes": "Pending.", + "commands": [], + "build_result": "BLOCKED", + "skipped_checks": [ + {"name": "Windows MSBuild", "reason": "Current host is macOS."} + ], + }, + } + ) + + self.assertEqual(validate_manifest(manifest), []) + + def test_non_required_platform_accepts_explicit_waiver(self): + manifest = base_manifest() + target = manifest["contract"]["output"]["platform_targets"]["windows"] + target["required"] = False + target["waiver_reason"] = "The requested SDK family is not available on Windows." + for role in ["implementation", "verification"]: + artifact = manifest["platforms"]["windows"][role] + artifact["status"] = "WAIVED" + artifact["waiver_reason"] = target["waiver_reason"] + artifact["dispatch"] = { + "mode": "pending", + "prompt": f"role-prompts/windows-{role}.md", + "artifact": f"role-artifacts/windows-{role}.json", + } + implementation = manifest["platforms"]["windows"]["implementation"]["output"] + implementation["files_changed"] = [] + implementation["matrix_updates"] = [] + verification = manifest["platforms"]["windows"]["verification"]["output"] + verification["result"] = "SKIPPED" + verification["parity_result"] = "SKIPPED" + verification["build_result"] = "SKIPPED" + verification["commands"] = [] + verification["skipped_checks"] = [] + + self.assertEqual(validate_manifest(manifest), []) + + def test_cross_platform_acceptance_must_pass_for_non_blocked_result(self): + manifest = base_manifest() + manifest["cross_platform_acceptance"]["result"] = "BLOCKED" + + self.assert_error_contains(manifest, "non-BLOCKED acceptance requires cross_platform_acceptance.result=PASS") + + def test_contract_and_implementation_target_must_match(self): + manifest = base_manifest() + manifest["platforms"]["ios"]["implementation"]["output"]["target_project"] = "iOS/APIExample-OC/" + + self.assert_error_contains( + manifest, "platforms.ios.implementation.output.target_project must match contract target" + ) + + def test_contract_must_cover_requirement_key_apis(self): + manifest = base_manifest() + manifest["contract"]["output"]["key_apis"] = ["joinChannel"] + + self.assert_error_contains( + manifest, + "contract.output.key_apis must include every requirement.key_apis value: setAudioProfile", + ) + + def test_passing_contract_requires_cross_platform_constraints(self): + manifest = base_manifest() + manifest["contract"]["output"]["cross_platform_requirements"] = [] + + self.assert_error_contains( + manifest, + "contract.output.cross_platform_requirements is required when Contract passes", + ) + + def test_implementation_requires_query_and_upsert_results(self): + manifest = base_manifest() + manifest["platforms"]["android"]["implementation"]["output"]["query_cases"] = "" + + self.assert_error_contains( + manifest, + "platforms.android.implementation.output.query_cases is required", + ) + + def test_required_verification_requires_entry_point_and_ux_notes(self): + manifest = base_manifest() + manifest["platforms"]["ios"]["verification"]["output"]["entry_point"] = "" + + self.assert_error_contains( + manifest, + "platforms.ios.verification.output.entry_point is required", + ) + + def test_duplicate_agent_ids_across_platforms_are_rejected(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["verification"]["agent_id"] = manifest["platforms"]["ios"][ + "verification" + ]["agent_id"] + + self.assert_error_contains(manifest, "duplicates platforms.ios.verification.agent_id") + + def test_duplicate_codex_run_ids_across_roles_are_rejected(self): + manifest = base_manifest() + implementation_run_id = manifest["platforms"]["ios"]["implementation"]["dispatch"][ + "run_id" + ] + manifest["platforms"]["ios"]["verification"]["dispatch"][ + "run_id" + ] = implementation_run_id + + self.assert_error_contains( + manifest, + "platforms.ios.verification.dispatch.run_id duplicates platforms.ios.implementation.dispatch.run_id", + ) + + def test_pass_rejects_skipped_platform_checks(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["verification"]["output"]["skipped_checks"] = [ + {"name": "runtime", "reason": "No device"} + ] + + self.assert_error_contains(manifest, "final_status=PASS cannot include skipped checks") + + def test_done_matrix_update_requires_platform_build_pass(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ + { + "feature": "Join channel audio", + "platform_unit": "Windows", + "from": "MISSING", + "to": "DONE", + "to_cell": "DONE(Basic/JoinChannelAudio/)", + "evidence": "Implemented.", + } + ] + manifest["platforms"]["windows"]["verification"]["output"]["build_result"] = "SKIPPED" + + self.assert_error_contains(manifest, "Windows matrix update to DONE requires build_result=PASS") + + def test_matrix_update_must_match_current_requirement(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ + { + "feature": "Media metadata", + "platform_unit": "Windows", + "from": "MISSING", + "to": "PARTIAL", + "to_cell": "PARTIAL(pending)", + "evidence": "Unrelated update.", + } + ] + + self.assert_error_contains( + manifest, + "platforms.windows.implementation.output.matrix_updates[0].feature must match requirement.feature", + ) + + def test_matrix_update_must_match_contract_target_project(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ + { + "feature": "Join channel audio", + "platform_unit": "Android full", + "from": "MISSING", + "to": "PARTIAL", + "to_cell": "PARTIAL(wrong platform)", + "evidence": "Wrong platform update.", + } + ] + + self.assert_error_contains( + manifest, + "platforms.windows.implementation.output.matrix_updates[0].platform_unit must match Contract target", + ) + + def test_matrix_update_cell_status_must_match_structured_status(self): + manifest = base_manifest() + manifest["final_status"] = "PASS WITH RISKS" + manifest["platforms"]["android"]["verification"]["output"]["skipped_checks"] = [ + {"name": "device-smoke", "reason": "Device unavailable."} + ] + manifest["platforms"]["android"]["implementation"]["output"]["matrix_updates"] = [ + { + "feature": "Join channel audio", + "platform_unit": "Android full", + "from": "MISSING", + "to": "PARTIAL", + "to_cell": "DONE(fake/path)", + "evidence": "Runtime smoke is pending.", + } + ] + + self.assert_error_contains( + manifest, + "platforms.android.implementation.output.matrix_updates[0].to_cell status must match to=PARTIAL", + ) + + def test_release_cannot_be_disabled(self): + manifest = base_manifest() + manifest["release"]["required"] = False + + self.assert_error_contains(manifest, "release.required must be true") + + def test_rejects_removed_publication_fields(self): + manifest = base_manifest() + manifest["requirement"]["publication_channel"] = "legacy" + manifest["release"]["publication"] = {} + + self.assert_error_contains(manifest, "unsupported requirement field: publication_channel") + self.assert_error_contains(manifest, "unsupported release field: publication") + + def test_release_target_versions_must_match_requirement(self): + manifest = base_manifest() + manifest["release"]["target_sdk_versions"]["android"] = "4.6.3" + + self.assert_error_contains( + manifest, "release.target_sdk_versions must match requirement.target_sdk_versions" + ) + + def test_target_sdk_versions_require_all_platform_semver_triplets(self): + manifest = base_manifest() + manifest["requirement"]["target_sdk_versions"]["android"] = "next" + manifest["release"]["target_sdk_versions"]["android"] = "next" + + self.assert_error_contains( + manifest, + "requirement.target_sdk_versions.android must use x.y.z format", + ) + + manifest = base_manifest() + manifest["requirement"]["target_sdk_versions"]["android"] = "" + manifest["release"]["target_sdk_versions"]["android"] = "" + self.assert_error_contains( + manifest, + "requirement.target_sdk_versions.android must use x.y.z format", + ) + + manifest = base_manifest() + del manifest["requirement"]["target_sdk_versions"]["windows"] + del manifest["release"]["target_sdk_versions"]["windows"] + self.assert_error_contains( + manifest, + "requirement.target_sdk_versions must define exactly android, ios, macos, windows", + ) + + manifest = base_manifest() + manifest["requirement"]["target_sdk_versions"]["linux"] = "4.6.4" + manifest["release"]["target_sdk_versions"]["linux"] = "4.6.4" + self.assert_error_contains( + manifest, + "requirement.target_sdk_versions must define exactly android, ios, macos, windows", + ) + + def test_release_sdk_check_requires_all_sources_to_match_target(self): + manifest = base_manifest() + check = manifest["release"]["checks"][0] + check["actual_versions"]["android/version-source"] = "4.6.3" + + self.assert_error_contains(manifest, "sdk-version-android actual versions must all match 4.6.4") + + def test_rejects_external_ci_and_qa_metadata(self): + manifest = base_manifest() + manifest["release"]["qa_acceptance"] = {} + + self.assert_error_contains(manifest, "unsupported release field: qa_acceptance") + + def test_non_blocked_release_rejects_skipped_checks(self): + manifest = base_manifest() + manifest["final_status"] = "PASS WITH RISKS" + manifest["release"]["skipped_checks"] = [ + {"name": "qa-device-smoke", "reason": "Device unavailable"} + ] + + self.assert_error_contains( + manifest, + "non-BLOCKED acceptance cannot include skipped release checks", + ) + + def test_build_pass_requires_bound_command_evidence(self): + manifest = base_manifest() + del manifest["platforms"]["android"]["verification"]["output"]["commands"][0][ + "evidence" + ] + + self.assert_error_contains( + manifest, + "platforms.android.verification.output.commands[0].evidence is required", + ) + + def test_successful_non_build_command_cannot_satisfy_build_result(self): + manifest = base_manifest() + command = manifest["platforms"]["android"]["verification"]["output"]["commands"][0] + command["command"] = "true" + + self.assert_error_contains( + manifest, + "platforms.android.verification.output.commands[0] is not a recognized android build command", + ) + + def test_build_tool_name_in_echo_cannot_satisfy_build_result(self): + manifest = base_manifest() + command = manifest["platforms"]["android"]["verification"]["output"]["commands"][0] + command["command"] = "echo ./gradlew assembleDebug" + + self.assert_error_contains( + manifest, + "platforms.android.verification.output.commands[0] is not a recognized android build command", + ) + + def test_non_build_tool_modes_cannot_satisfy_build_result(self): + for platform, command_text in [ + ("android", "./gradlew --dry-run assembleDebug"), + ("android", "./gradlew --dry-run=true assembleDebug"), + ("android", "./gradlew help --task assembleDebug"), + ("ios", "xcodebuild -version"), + ("ios", "xcodebuild -scheme APIExample -showTestPlans"), + ("ios", "xcodebuild -scheme APIExample -resolvePackageDependencies"), + ("ios", "xcodebuild -scheme APIExample -checkFirstLaunchStatus"), + ("macos", "xcodebuild -showsdks"), + ("windows", "msbuild -version"), + ("windows", "msbuild APIExample.sln /t:Clean"), + ("windows", "msbuild APIExample.sln /preprocess:out.xml"), + ("windows", "msbuild APIExample.sln /targets"), + ("windows", "msbuild APIExample.sln /validate"), + ("windows", "cmake --build build --target help"), + ("android", "../fake/gradlew assembleDebug"), + ("ios", "/tmp/xcodebuild -scheme APIExample build"), + ("windows", "PATH=./fake msbuild APIExample.sln /t:Build"), + ]: + with self.subTest(platform=platform): + manifest = base_manifest() + command = manifest["platforms"][platform]["verification"]["output"]["commands"][0] + command["command"] = command_text + + self.assert_error_contains( + manifest, + f"platforms.{platform}.verification.output.commands[0] is not a recognized {platform} build command", + ) + + def test_build_command_cannot_escape_contract_working_directory(self): + manifest = base_manifest() + command = manifest["platforms"]["ios"]["verification"]["output"]["commands"][0] + command["command"] = ( + "xcodebuild -project ../APIExample-Audio/APIExample.xcodeproj " + "-scheme APIExample build" + ) + + self.assert_error_contains( + manifest, + "platforms.ios.verification.output.commands[0] is not a recognized ios build command", + ) + + def test_xcode_default_build_command_from_project_rules_is_accepted(self): + manifest = base_manifest() + command = manifest["platforms"]["macos"]["verification"]["output"]["commands"][0] + command["command"] = ( + "xcodebuild -workspace APIExample.xcworkspace -scheme APIExample " + "-configuration Release" + ) + + self.assertEqual(validate_manifest(manifest), []) + + def test_windows_build_pass_requires_windows_host_provenance(self): + manifest = base_manifest() + manifest["platforms"]["windows"]["verification"]["dispatch"]["host_platform"] = "darwin" + + self.assert_error_contains( + manifest, + "platforms.windows.verification.output.build_result=PASS requires host_platform=win32", + ) + + def test_rejects_platform_target_outside_its_platform_root(self): + manifest = base_manifest() + target = manifest["contract"]["output"]["platform_targets"]["android"] + target["target_project"] = "iOS/APIExample/" + target["files_allowed"] = ["iOS/APIExample/"] + + self.assert_error_contains( + manifest, + "contract.output.platform_targets.android.target_project must be inside Android/", + ) + + def test_rejects_platform_target_that_escapes_root_with_parent_segments(self): + manifest = base_manifest() + target = manifest["contract"]["output"]["platform_targets"]["android"] + target["target_project"] = "Android/../iOS/APIExample/" + target["files_allowed"] = ["Android/../iOS/APIExample/"] + + self.assert_error_contains( + manifest, + "contract.output.platform_targets.android.target_project must be inside Android/", + ) + + def test_rejects_dispatch_path_outside_workspace(self): + manifest = base_manifest() + manifest["platforms"]["android"]["implementation"]["dispatch"]["prompt"] = "../prompt.md" + + self.assert_error_contains(manifest, "must be relative to the execution workspace") + + def test_files_changed_accepts_deleted_path_inside_contract_scope(self): + manifest = base_manifest() + manifest["platforms"]["android"]["implementation"]["output"]["files_changed"] = [ + "Android/APIExample/deleted-case.java" + ] + + self.assertEqual(validate_manifest(manifest), []) + + def test_knowledge_update_requires_a_durable_file_change(self): + manifest = base_manifest() + for platform in PLATFORMS: + manifest["platforms"][platform]["implementation"]["output"]["files_changed"] = ["AGENTS.md"] + manifest["knowledge_updates"] = [ + { + "source": "pilot", + "impact_platforms": PLATFORMS, + "symptom": "Build blocked", + "root_cause": "Host mismatch", + "guardrail": "Use target CI", + "verification": "CI build", + "updated_at": "2026-07-10", + } + ] + + self.assert_error_contains(manifest, "knowledge_updates require at least one durable knowledge") + + def test_durable_knowledge_accepts_both_skill_directory_conventions(self): + self.assertTrue(is_durable_knowledge_path(".agent/skills/review-case/SKILL.md")) + self.assertTrue( + is_durable_knowledge_path( + "Android/APIExample/.agents/skills/review-case/SKILL.md" + ) + ) + + def test_evidence_files_are_rehashed_from_manifest_directory(self): + manifest = base_manifest() + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + artifacts = [manifest["contract"]] + for platform in PLATFORMS: + artifacts.extend(manifest["platforms"][platform].values()) + for index, item in enumerate(artifacts): + dispatch = item["dispatch"] + for path_field, hash_field in [ + ("prompt", "prompt_sha256"), + ("artifact", "artifact_sha256"), + ("input_snapshot", "input_snapshot_sha256"), + ("command_log", "command_log_sha256"), + ("repository_delta", "repository_delta_sha256"), + ]: + if path_field not in dispatch: + continue + path = root / dispatch[path_field] + path.parent.mkdir(parents=True, exist_ok=True) + if path_field == "repository_delta": + content = json.dumps( + {"changed_files": item["output"]["files_changed"]} + ).encode() + else: + content = f"{index}:{path_field}\n".encode() + path.write_bytes(content) + dispatch[hash_field] = hashlib.sha256(content).hexdigest() + + self.assertEqual(validate_evidence_files(manifest, root), []) + prompt = root / manifest["contract"]["dispatch"]["prompt"] + prompt.write_text("tampered\n", encoding="utf-8") + + errors = validate_evidence_files(manifest, root) + self.assertTrue(any("contract.dispatch.prompt_sha256 does not match" in error for error in errors)) + + def test_evidence_validation_rejects_delta_content_mismatch(self): + manifest = base_manifest() + implementation = manifest["platforms"]["android"]["implementation"] + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + delta_path = root / implementation["dispatch"]["repository_delta"] + delta_path.parent.mkdir(parents=True) + delta_path.write_text( + json.dumps({"changed_files": ["Android/APIExample/other.java"]}), + encoding="utf-8", + ) + implementation["dispatch"]["repository_delta_sha256"] = hashlib.sha256( + delta_path.read_bytes() + ).hexdigest() + + errors = validate_evidence_files(manifest, root) + + self.assertTrue( + any("repository_delta changed_files do not match" in error for error in errors), + errors, + ) + + +if __name__ == "__main__": + unittest.main() From 8b12b74ec15628a8b10cc9a60c813d217c8752ed Mon Sep 17 00:00:00 2001 From: alienzh Date: Fri, 31 Jul 2026 17:34:10 +0800 Subject: [PATCH 11/49] Codex/4.7.0 release iteration (#479) * chore: update RTC SDK version to 4.7.0 * feat: add Simplified Chinese localizations * fix: complete iOS Chinese localization --------- Co-authored-by: zhangwei --- Android/AGENTS.md | 2 +- .../app/src/main/res/values-zh/strings.xml | 90 +++ Android/APIExample-Audio/gradle.properties | 2 +- .../app/src/main/res/values-zh/strings.xml | 115 ++++ Android/APIExample-Compose/gradle.properties | 2 +- .../app/src/main/res/values-zh/arrays.xml | 47 ++ .../app/src/main/res/values-zh/strings.xml | 440 ++++++++++++++ Android/APIExample/gradle.properties | 2 +- .../project.pbxproj | 40 +- .../zh-Hans.lproj/AudioMixing.strings | 87 ++- .../zh-Hans.lproj/CustomAudioRender.strings | 13 +- .../zh-Hans.lproj/CustomAudioSource.strings | 13 +- .../CustomPcmAudioSource.strings | 19 +- .../zh-Hans.lproj/PrecallTest.strings | 27 +- .../zh-Hans.lproj/RawAudioData.strings | 9 +- .../zh-Hans.lproj/RhythmPlayer.strings | 29 +- .../zh-Hans.lproj/VoiceChanger.strings | 115 ++-- .../JoinChannelAudioToken.strings | 69 ++- .../zh-Hans.lproj/JoinChannelAudio.strings | 52 +- .../zh-Hans.lproj/Localizable.strings | 286 ++++----- .../zh-Hans.lproj/Main.strings | 51 +- iOS/APIExample-Audio/Podfile | 2 +- iOS/APIExample-Audio/README.zh.md | 64 ++ .../APIExample-OC.xcodeproj/project.pbxproj | 12 +- .../zh-Hans.lproj/AudioMixing.strings | 87 ++- .../zh-Hans.lproj/ContentInspect.strings | 13 +- .../zh-Hans.lproj/LiveStreaming.strings | 69 ++- .../zh-Hans.lproj/MediaChannelRelay.strings | 47 +- .../Multipath/zh-Hans.lproj/Multipath.strings | 32 + .../zh-Hans.lproj/RTMPStreaming.strings | 24 +- .../Simulcast/zh-Hans.lproj/Simulcast.strings | 41 ++ .../zh-Hans.lproj/StreamEncryption.strings | 27 +- iOS/APIExample-OC/Podfile | 2 +- iOS/APIExample-OC/README.zh.md | 64 ++ .../zh-Hans.lproj/Localizable.strings | 340 ++++++----- .../project.pbxproj | 24 +- .../{ => en.lproj}/Localizable.strings | 54 +- .../zh-Hans.lproj/Localizable.strings | 283 +++++++++ iOS/APIExample-SwiftUI/ARCHITECTURE.md | 2 +- iOS/APIExample-SwiftUI/Podfile | 2 +- .../APIExample.xcodeproj/project.pbxproj | 268 ++------- .../ARKit/zh-Hans.lproj/ARKit.strings | 17 +- .../zh-Hans.lproj/AgoraBeauty.strings | 98 ++- .../zh-Hans.lproj/AudioMixing.strings | 87 ++- .../zh-Hans.lproj/AudioRouterPlayer.strings | 32 +- .../zh-Hans.lproj/AudioWaveform.strings | 41 +- .../zh-Hans.lproj/ContentInspect.strings | 13 +- .../zh-Hans.lproj/CreateDataStream.strings | 21 +- .../zh-Hans.lproj/CustomAudioRender.strings | 13 +- .../zh-Hans.lproj/CustomAudioSource.strings | 13 +- .../CustomPcmAudioSource.strings | 23 +- .../zh-Hans.lproj/CustomVideoRender.strings | 17 +- .../CustomVideoSourcePush.strings | 18 +- .../CustomVideoSourcePushMulti.strings | 35 +- .../zh-Hans.lproj/FaceCapture.strings | 25 +- .../zh-Hans.lproj/JoinMultiChannel.strings | 24 +- .../KtvCopyrightMusic.storyboard | 4 +- .../en.lproj/KtvCopyrightMusic.strings | 6 - .../zh-Hans.lproj/KtvCopyrightMusic.strings | 6 + .../zh-Hans.lproj/LiveStreaming.strings | 97 ++- .../zh-Hans.lproj/LocalCompositeGraph.strings | 25 +- .../zh-Hans.lproj/MediaChannelRelay.strings | 57 +- .../zh-Hans.lproj/MediaPlayer.strings | 71 ++- .../Multipath/zh-Hans.lproj/Multipath.strings | 32 + .../zh-Hans.lproj/MutliCamera.strings | 30 +- .../PictureInPicture/PictureInPicture.strings | 12 - .../zh-Hans.lproj/PrecallTest.strings | 37 +- .../zh-Hans.lproj/RTMPStreaming.strings | 25 +- .../zh-Hans.lproj/RawAudioData.strings | 17 +- .../zh-Hans.lproj/RawVideoData.strings | 17 +- .../zh-Hans.lproj/RhythmPlayer.strings | 29 +- .../RtePlayer/zh-Hans.lproj/RtePlayer.strings | 20 +- .../zh-Hans.lproj/ScreenShare.strings | 60 +- .../zh-Hans.lproj/SimpleFilter.strings | 21 +- .../Simulcast/zh-Hans.lproj/Simulcast.strings | 41 ++ .../{ => Base.lproj}/SpatialAudio.storyboard | 0 .../zh-Hans.lproj/SpatialAudio.strings | 20 + .../zh-Hans.lproj/StreamEncryption.strings | 25 +- .../zh-Hans.lproj/FUBeautify.strings | 47 +- .../zh-Hans.lproj/ThirdBeautify.strings | 13 +- .../TransparentRender.storyboard | 0 .../zh-Hans.lproj/TransparentRender.strings | 5 + .../zh-Hans.lproj/VideoMetadata.strings | 13 +- .../zh-Hans.lproj/VideoProcess.strings | 104 ++-- .../zh-Hans.lproj/VoiceChanger.strings | 115 ++-- .../zh-Hans.lproj/JoinChannelAudio.strings | 61 +- .../JoinChannelVideoRecorder.strings | 27 +- .../JoinChannelVideoToken.strings | 34 +- .../zh-Hans.lproj/JoinChannelVideo.strings | 25 +- .../zh-Hans.lproj/Localizable.strings | 510 ++++++++-------- .../APIExample/zh-Hans.lproj/Main.strings | 51 +- iOS/APIExample/Podfile | 2 +- iOS/APIExample/README.zh.md | 64 ++ macOS/APIExample.xcodeproj/project.pbxproj | 124 +--- .../zh-Hans.lproj/VideoProcess.strings | 79 +-- .../zh-Hans.lproj/AudioMixing.strings | 96 ++- .../zh-Hans.lproj/ChannelMediaRelay.strings | 27 +- .../zh-Hans.lproj/CreateDataStream.strings | 15 +- .../zh-Hans.lproj/CustomAudioRender.strings | 28 +- .../zh-Hans.lproj/CustomAudioSource.strings | 31 +- .../zh-Hans.lproj/CustomVideoRender.strings | 28 +- .../CustomVideoSourcePush.strings | 29 +- .../CustomVideoSourcePushMulti.strings | 33 +- .../zh-Hans.lproj/FaceCapture.strings | 28 +- .../zh-Hans.lproj/JoinMultiChannel.strings | 34 +- .../zh-Hans.lproj/LiveStreaming.strings | 30 +- .../zh-Hans.lproj/MediaPlayer.strings | 46 +- .../zh-Hans.lproj/MultiCameraSourece.strings | 40 +- .../Multipath/zh-Hans.lproj/Multipath.strings | 11 + .../zh-Hans.lproj/PrecallTest.strings | 62 +- .../zh-Hans.lproj/QuickSwitchChannel.strings | 15 +- .../zh-Hans.lproj/RTMPStreaming.strings | 35 +- .../RtePlayer/zh-Hans.lproj/RtePlayer.strings | 10 +- .../zh-Hans.lproj/ScreenShare.strings | 54 +- .../Simulcast/zh-Hans.lproj/Simulcast.strings | 23 + .../zh-Hans.lproj/SpatialAudio.strings | 71 ++- .../zh-Hans.lproj/StreamEncryption.strings | 31 +- .../zh-Hans.lproj/VideoProcess.strings | 61 +- .../zh-Hans.lproj/VoiceChanger.strings | 62 +- .../zh-Hans.lproj/JoinChannelAudio.strings | 26 +- .../JoinChannelVideoRecorder.strings | 28 +- .../JoinChannelVideoToken.strings | 36 +- .../zh-Hans.lproj/JoinChannelVideo.strings | 28 +- .../zh-Hans.lproj/Localizable.strings | 567 +++++++++--------- macOS/ARCHITECTURE.md | 3 +- macOS/Podfile | 2 +- macOS/README.zh.md | 54 ++ windows/APIExample/APIExample/zh-cn.ini | Bin 11804 -> 18336 bytes windows/APIExample/install.ps1 | 2 +- 129 files changed, 4096 insertions(+), 2920 deletions(-) create mode 100644 Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml create mode 100644 Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml create mode 100644 Android/APIExample/app/src/main/res/values-zh/arrays.xml create mode 100644 Android/APIExample/app/src/main/res/values-zh/strings.xml create mode 100644 iOS/APIExample-Audio/README.zh.md create mode 100644 iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings create mode 100644 iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings create mode 100644 iOS/APIExample-OC/README.zh.md rename iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/{ => en.lproj}/Localizable.strings (87%) create mode 100644 iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings rename iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/{ => Base.lproj}/KtvCopyrightMusic.storyboard (96%) delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/en.lproj/KtvCopyrightMusic.strings create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/zh-Hans.lproj/KtvCopyrightMusic.strings create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/PictureInPicture/PictureInPicture.strings create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings rename iOS/APIExample/APIExample/Examples/Advanced/SpatialAudio/{ => Base.lproj}/SpatialAudio.storyboard (100%) create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/SpatialAudio/zh-Hans.lproj/SpatialAudio.strings rename iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/{ => Base.lproj}/TransparentRender.storyboard (100%) create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/zh-Hans.lproj/TransparentRender.strings create mode 100644 iOS/APIExample/README.zh.md create mode 100644 macOS/APIExample/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings create mode 100644 macOS/APIExample/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings create mode 100644 macOS/README.zh.md mode change 100755 => 100644 windows/APIExample/APIExample/zh-cn.ini diff --git a/Android/AGENTS.md b/Android/AGENTS.md index 77d601ec8..58c7db6d1 100644 --- a/Android/AGENTS.md +++ b/Android/AGENTS.md @@ -10,7 +10,7 @@ Android platform entry point. Read this first, then go to the relevant project's | `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`) +SDK version: each project's `gradle.properties` → `rtc_sdk_version` (currently `4.7.0`) ## Project Selection diff --git a/Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml b/Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..1458b29ca --- /dev/null +++ b/Android/APIExample-Audio/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,90 @@ + + + Agora API Example(Audio) + 频道ID + 加入 + 播放 + 暂停 + 离开 + 停止 + 听筒 + 扬声器 + 打开麦克风 + 关闭麦克风 + + + 设置 + SDK版本 + V%s + 混音音量 + 3D音效环绕周期 + 下一步 + 发布麦克风 + 发布本地音频 + + 音频互动直播 + 音频互动直播(Token验证) + 通话前质量检测 + 快速切换频道 + 音频文件混音 + 美声与音效 + 自定义音频采集 + 自定义音频渲染 + 原始音频数据 + 空间音效 + 请授予权限 + 此示例演示在使用RTC通话中音频路由对第三方播放器的影响。 + 此示例演示了如何使用SDK加入频道进行纯语音通话的功能。 + 此示例演示了如何使用SDK加入带Token的频道进行纯语音通话的功能。 + 此示例演示了如何使用SDK在进入频道前检测网络质量状况。 + 此示例演示了在语音通话过程中使用MediaOption控制发布麦克风和自采集音频流的功能。 + 此示例演示了在语音通话过程中如何自定义远端音频流渲染器的功能。 + 此示例演示了在音视频通话过程中播放并管理audio effect和audio mixing文件。 + 此示例演示了在音视频通话过程中如何使用API提供的一些人声效果,或使用API自行组合出想要的人声效果。 + 此示例演示了在音频通话过程中如何通过回调获取裸数据的功能。 + 此示例演示了音频通话过程中如何使用虚拟节拍器。 + 音频回写 + 此示例演示了如何使用空间音效。 + 开始 + 点击开始 + 恢复播放 + 混音发布音量 + 混音播放音量 + 音效音量 + 请插入耳机体验3d音频效果 + + 虚拟节拍器 + Beats per Measure + Beats per Minute + Token无效 + Token已过期 + + 私有云 + IP地址 + 请输入IP地址 + 日志上报 + 日志服务域名/IP + 请输入日志服务域名/IP + 日志服务端口 + 请输入日志服务端口 + 日志服务路径 + 请输入日志服务路径 + 使用Https + 音频频谱 + + 区域 + + 音障 + 房间 + 静音 + 保存 + AppID为空! + 默认 + 扬声器 + 听筒 + 耳机 + 耳机(TypeC) + 蓝牙耳机 + 请打开通知权限,防止后台录音中断 + 此示例演示了如何使用SDK进行语音通话的功能。 + diff --git a/Android/APIExample-Audio/gradle.properties b/Android/APIExample-Audio/gradle.properties index be8a46849..97f6f46d5 100644 --- a/Android/APIExample-Audio/gradle.properties +++ b/Android/APIExample-Audio/gradle.properties @@ -23,4 +23,4 @@ android.nonFinalResIds=false # read enable simple filter section on README first before set this flag to TRUE simpleFilter = false -rtc_sdk_version = 4.6.3 \ No newline at end of file +rtc_sdk_version = 4.7.0 diff --git a/Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml b/Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..006bac3ee --- /dev/null +++ b/Android/APIExample-Compose/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,115 @@ + + 视频互动直播 + 音频互动直播 + RTC实时直播 + 视频互动直播(Token验证) + 旁路推流CDN + 音视频元数据 + 美声与音效 + 自定义音频采集 + 自定义音频渲染 + 自定义视频采集 + 自定义视频渲染 + 原始音频数据 + 原始视频数据 + 加入多频道 + 媒体流加密 + 音频文件混音 + 通话前质量检测 + 本地/远端录制 + 媒体播放器 + 屏幕共享 + 视频增强组件 + 本地合图 + 创建数据流 + 虚拟节拍器 + 跨频道媒体流转发 + 画中画 + 空间音频 + + 加密方式 + 加密密钥 + 发布本地音频 + 发布麦克风 + 退出 + 加入 + 暂停 + 恢复 + Ex频道截图 + 离开Ex频道 + 加入Ex频道 + 离开选项 + 是否停止录音 + 确定 + 取消 + 开启极速直播 + 水印 + 小流 + 编码类型 + 自动选择 + 硬编 + 软编 + B帧 + 垫片 + 透明背景 + 视频元数据 + 发送 + 推送 + 停止 + 播放 + 开始录制 + 停止录制 + 切换摄像头 + 音频回写 + 开始 + 音效音量 + 混音音量 + 混音播放音量 + 混音发布音量 + Tip:开始测试后请对着麦克风讲话,讲话声音在%d秒后播放则测试正常。 + 推流地址 + 关闭推流 + 开始推流 + 是否转码 + 屏幕共享本地预览 + 屏幕共享本地音频 + 只共享屏幕 + 请移动红色图标体验3d音频效果 + 静音 + 美颜 + 美白 + 红润 + 锐利 + 平滑 + 暗光增强 + 色彩增强 + 强度 + 肤色保护 + 视频降噪 + 虚拟背景 + 图片 + 颜色 + 毛玻璃 + 视频 + 均衡降噪模式 + 强降噪模式 + 低延时强降噪模式 + 频道名 + 离开 + 设置 + 分辨率 + 帧率 + 方向 + 区域 + 截图 + 开始连麦 + 关闭连麦 + 权限已授予 + 权限被拒绝 + 启用视频 + 视频场景 + General + Meeting + 1V1 + Live Show + diff --git a/Android/APIExample-Compose/gradle.properties b/Android/APIExample-Compose/gradle.properties index 6ba5018a5..fad88fe86 100644 --- a/Android/APIExample-Compose/gradle.properties +++ b/Android/APIExample-Compose/gradle.properties @@ -22,4 +22,4 @@ kotlin.code.style=official # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -rtc_sdk_version = 4.6.3 \ No newline at end of file +rtc_sdk_version = 4.7.0 diff --git a/Android/APIExample/app/src/main/res/values-zh/arrays.xml b/Android/APIExample/app/src/main/res/values-zh/arrays.xml new file mode 100644 index 000000000..79cf8c59e --- /dev/null +++ b/Android/APIExample/app/src/main/res/values-zh/arrays.xml @@ -0,0 +1,47 @@ + + + + + 自动选择 + 硬编 + 软编 + + + + 关闭 + 类型1 + 类型2 + + + 关闭 + 黑色 + 棕色 + + + 无 + 1号色 + 2号色 + 3号色 + 4号色 + 5号色 + + + + 关闭 + 类型1 + 类型2 + + + 关闭 + 类型1 + 类型2 + 类型3 + + + 关闭 + 类型1 + 类型2 + 类型3 + 类型4 + + diff --git a/Android/APIExample/app/src/main/res/values-zh/strings.xml b/Android/APIExample/app/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..7003858dd --- /dev/null +++ b/Android/APIExample/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,440 @@ + + + Agora API Example + 拉流 + 停止拉流 + 推流 + 停止推流 + 链接地址 + 频道ID + 请输入Rte Url + 目标频道ID + 加密密钥 + 加入 + 打开模糊 + 打开 + 播放 + 暂停 + 停止发流 + 关闭模糊 + 离开 + 停止 + 请授予权限 + 听筒 + 扬声器 + 打开麦克风 + 关闭超级分辨率 + 关闭麦克风 + 打开超级分辨率 + 发送元数据 + 请确保你已经按照QuickSwitchChannel类中的注释所说的那样建立频道,否则你将看到一片空白。 + 等待主播中 + 频道:%s + 你可以左右滑动切换频道 + 请选择你的角色 + 主播 + 观众 + 本地视频 + 屏幕分享 + 发布摄像头 + 摄像头 + 渲染模式:%s + 本地视频源不可用,请将MP4视频文件拷贝到 %s/ 并且命名为 localvideo.mp4. + 本地音频源不可用,请将PCM音频文件拷贝到 %s/ 并且命名为 bgm.pcm. + 系统版本过低,无法使用此功能;请使用Android5.0的系统。 + 分辨率 + 帧率 + 视频朝向 + 自适应 + 固定横向 + 固定纵向 + + + 设置 + SDK版本 + V%s + 已发送:%s + 接收到:%s + 关闭背景音乐 + 开启背景音乐 + 开启音效 + 关闭音效 + 混音音量 + 码率 + 确定 + 3D人声 + 电音 + 3D音效环绕周期 + 开启连麦 + 关闭连麦 + 开启极速直播 + 关闭极速直播 + 下一步 + AI降噪 + 音频耳返 + 发布音频 + 发布麦克风 + 发布本地音频 + 本地播放 + 本地视图 + 超低延迟直播 + 预加载 + + 音频互动直播 + 视频互动直播 + 视频互动直播(Token验证) + 调整通话音量 + 跨频道媒体流转发 + 通话前质量检测 + 快速切换频道 + 超级分辨率 + 设置音频编码属性 + 音频文件混音 + 美声与音效 + 媒体播放器 + 音频路由(第三方播放器) + 音频频谱 + 区域访问限制 + 加入多频道 + 推流到CDN + 旁路推流CDN + 输入在线媒体流 + 自定义视频采集 + 多路视频采集 + 视频源切换 + 多视频源 + 屏幕共享 + 自定义音频采集 + 自定义音频渲染 + 自定义视频渲染 + 原始视频数据 + 插入元数据 + 媒体流加密(自定义) + 媒体流加密 + 设置视频编码属性 + RTC实时直播 + 双进程屏幕共享 + 现实增强集成 + 创建数据流 + 原始音频数据 + 插件系统 + 空间音效 + 视频鉴黄 + 画中画 + 第三方美颜 + Agora 美颜 2.0 + KTV版权音乐 + 本地/远端录制 + 本地合图 + 获取面捕数据 + 同源多流 + 网络传输高级功能 + + 此示例演示了如何使用SDK加入频道进行纯语音通话的功能。 + 此示例演示了如何使用SDK加入频道进行音视频通话的功能。 + 此示例演示了如何使用SDK加入带token的频道进行音视频通话的功能。 + 此示例演示了如何使用SDK在频道中调整通话音量的功能。 + 此示例演示了如何使用SDK在进入频道前检测网络质量状况。 + 此示例演示了在语音通话过程中使用MediaOption控制发布麦克风和自采集音频流的功能。 + 此示例演示了在语音通话过程中如何自定义远端音频流渲染器的功能。 + 此示例演示了在音视频通话过程中如何自定义远端视频流渲染器的功能。 + 此示例演示了在音视频通话过程中如何通过回调获取裸数据,以及在数据被处理后如何返回给SDK的功能。\nPS:裸数据包括视频帧和音频帧。 + 此示例演示了在音视频通话过程中如何以主动Push的方式进行视频自采集的功能。 + 此示例演示了在音视频通话过程中如何使用CreateVideoTrack的方式进行视频自采集的功能。 + 此示例演示了在音视频通话过程中如何快速切换频道的功能。 + 此示例演示了在音视频通话过程中如何加入多个频道功能。 + 此示例演示了在音视频通话过程中如何输入在线媒体流并渲染的功能。 + 此示例演示了在音视频通话过程中如何使用旁路推流的功能。\nPS:需要配合RTMP服务器实现功能。 + 此示例演示了在音视频通话过程中如何进行音视频帧的加解密的方法。 + 此示例演示了在音视频通话过程中如何以MediaIO的方式进行视频自采集和如何进行屏幕分享的功能。 + 此示例演示了在音视频通话过程中如何进行屏幕共享的方法。 + 此示例演示了在音视频通话过程中如何进行本地合图的方法。 + 此示例演示了在音视频通话过程中,视频如何在摄像头流和屏幕分享流之间灵活切换。 + 此示例演示了在音视频通话过程中如何发送meta信息的方法。 + 此示例演示了在音视频通话过程中如何通过AreaCode指定SDK访问限制。 + 此示例演示了在音视频通话过程中如何通过setAudioProfile设置profile来适配最佳音频性能。 + 此示例演示了在音视频通话过程中播放并管理audio effect和audio mixing文件。 + 此示例演示了在音视频通话过程中如何使用API提供的一些人声效果,或使用API自行组合出想要的人声效果。 + 此示例演示了在音视频通话过程中如何集成和使用Agora SDK的媒体播放器。 + 此示例演示在使用RTC通话中音频路由对第三方播放器的影响。 + 此示例演示了在音视频通话过程中如何实现波形图 + 此示例演示了在音视频通话过程中如何使用Agora SDK的媒体录制器录制本地或远端的视频。 + 此示例演示了在音视频通话过程中如何将A频道的主播流转发到B频道,实现主播PK。 + 此示例演示了在音视频通话过程中如何进行音视频帧的加解密的方法。 + 此示例演示了在音视频通话过程中如何通过VideoEncoderConfiguration来适配最佳视频性能。 + 此示例演示了在音视频通话过程中如何使用超级分辨率接口显著提升远端视频显示效果。 + 此示例演示了如何使用极速直播功能再音视频通话中进行连麦。 + 此示例演示了在音视频通话过程中如何利用双进程实现同时分享摄像头和屏幕共享。 + 此示例演示了如何将一个基于Google ARCore构建的AR画面实时分享给远端观众。 + 此示例演示了在音视频通话过程中如何伴随视频帧发送数据流信息的方法。 + 此示例演示了在音频通话过程中如何通过回调获取裸数据的功能。 + 此示例演示了一个简单音视频插件是如何使用的。 + 此示例演示了音频通话过程中如何使用虚拟节拍器。 + 此示例演示了音频通话过程中SDK内置的各类视频增强功能。 + 音频回写 + 截图 + Ex频道截图 + 屏幕共享 + 屏幕共享本地预览 + 屏幕共享本地音频 + 外置 MediaProjection + 只共享屏幕 + 屏幕共享本地视频 + 此示例演示了如何使用空间音效。 + 混音控制 + 开始 + 点击开始 + 恢复播放 + 混音发布音量 + 混音播放音量 + 音效控制 + 音效音量 + 混音 + 请移动红色图标体验3d音频效果 + 您现在可以移动喇叭到不同位置以体验空间音效 + 你会听到一段音乐, 10秒后这段音乐会通过空间音效的方式播放 + 此示例演示了如何使用SDK进行视频鉴黄的功能。 + 此示例演示了如何在画中画中使用sdk进行远端视频显示的功能。 + 此示例演示了如何集成第三方美颜sdk + 此示例演示了如何集成 Agora 美颜 2.0 + 此示例演示如何使用ktv api实现一个场景demo + 此示例演示如何使用面捕功能获取面捕数据 + 此示例演示如何使用透明渲染功能播放Mp4视频 + 此示例演示如何使用URL拉流来极速直播 + 此示例演示如何使用同源多流 + 此示例演示如何使用Multipath + + 本地 + SDK处理前 + SDK处理后 + 主播加入 + 观众加入 + 输入 Agora 频道名 + 推流模式 + 输入CDN推流地址 + RTC推流 + 开始推流 + 麦克风音量 + CDN频道 + 启动中 + 停止推流 + + 倍速(%.1f) + 声道切换 + 左声道 + 右声道 + 自动 + 混响 + 音轨切换(%d) + 上一个 + 下一个 + 开始音视频测试 + 开始音频测试 + 停止音视频测试 + 停止音频测试 + 网络回路测试 + 网络延迟测试 + 录制倒计时 + 播放倒计时 + 开始麦克风录制 + 视频增强组件 + 虚拟节拍器 + Beats per Measure + Beats per Minute + 滤镜 + 滤镜风格 + 滤镜强度 + 美妆 + 美妆风格 + 美妆滤镜 + 美妆滤镜强度 + 请先打开美妆 + 美型 + 美型部位 + 部位强度 + 美型风格 + 风格强度 + 眉毛类型 + 眉毛颜色 + 眉毛强度 + 眼睫毛类型 + 眼睫毛颜色 + 眼睫毛强度 + 眼影类型 + 眼影颜色 + 眼影强度 + 瞳孔类型 + 瞳孔颜色 + 瞳孔强度 + 腮红类型 + 腮红颜色 + 腮红强度 + 唇彩类型 + 唇彩颜色 + 唇彩强度 + 面部类型 + 面部强度 + 卧蚕类型 + 卧蚕强度 + 基础美颜 + 美颜 + 美白 + 红润 + 锐利 + 平滑 + 亮度 + 亮度明暗对比度 + 低 + 正常 + 高 + 眼袋淡化 + 亮眼 + 法令纹 + 美牙 + 暗光增强 + 暗光增强(RTC) + 色彩增强 + 色彩增强(RTC) + 视频降噪 + 视频降噪(RTC) + 强度 + 肤色保护 + 虚拟背景 + 视频正常 + 视频性感 + 视频色情 + 请通过控制台webhook获取鉴黄结果 + 切换摄像头 + 你可以用摄像头对准图片检验鉴黄效果 + + 远端截图 + 本地截图 + 当前没有远端用户,请加入频道并等待远端用户加入 + Agora-simple-filter插件没有开启,请看README.md了解怎么开启插件后再运行. + 图片 + 颜色 + 毛玻璃 + 视频 + 区域 + 水印 + 小流 + 创建采集Track + 创建采集Track(编码) + 销毁Track + 音障 + 房间 + 您的手机没有授予悬浮窗权限,请开启后再试 + 现在去开启 + 暂不开启 + 显示悬浮窗 + Token不存在 + Token已过期 + 商汤美颜 + 商汤库没有集成!请按README集成后再编译 + 单输入(仅NV21) + 双输入(NV21和纹理) + 相芯美颜 + 美体 + 相芯库没有集成!请按README集成后再编译运行 + 字节美颜 + 字节库没有集成!请按README集成后再编译运行 + + 私有云 + IP地址 + 请输入IP地址 + 日志上报 + 日志服务域名/IP + 请输入日志服务域名/IP + 日志服务端口 + 请输入日志服务端口 + 日志服务路径 + 请输入日志服务路径 + 使用Https + 加入Ex频道 + 离开Ex频道 + 离开选项 + 确认 + 提示 + 取消 + 停止音频录制 + 停止所有音效 + 停止混音 + 麦克风录制已停止! + B帧 + 编码类型 + 在默认浏览器打开 + 刷新 + 保存 + 重置 + AppID为空! + 商汤美颜 + 播放器错误: %s + 播放器播放结束。 + 是否重新打开url? + 首帧出图优化 + 开启后无法关闭,并且需要主辅两端都开启时才生效 + 开始录制 + 停止录制 + 透明背景 + 转发音频流 + 用户ID + View背景色(只在居中渲染模式下有效果) + 渲染模式 + 平铺 + 居中 + 拉伸 + 默认 + 耳机 + 听筒 + 耳机(无麦克风) + 扬声器 + 外部扬声器(尚不支持) + 蓝牙耳机 + USB 设备(Type-C) + 第三方播放器 + 当前设备不支持! + 垫片 + 自定义 + 请先参考README配置stream encrypt! + 播放音轨 + 推送音轨 + 音轨%d + 请联系 Agora 客服获取面捕证书并配置到 AUTHENTICATION 常量上 + 请打开通知权限,防止后台录音中断 + 请打开通知权限,防止后台共享屏幕中断 + 视频元数据 + 发送 + 音频元数据 + 前置 + 后置 + 其他 + 默认摄像头 + 广角摄像头 + 超宽摄像头 + 长焦摄像头 + 进入画中画模式 + 透明渲染 + 极速直播Url拉流 + 本地帧率,范围:1-60 + 远端帧率,范围:1-60 + 场景 + 截图时机 + 请先加入频道 + 外部EglContext + 外部EglContext设置 + 频道号为空 + 目标频道号为空 + 是否转码 + 此示例演示了如何使用SDK进行语音通话的功能。 + 静音 + 声音模糊 + 空气传播模拟 + 衰减系数 + 角色 + 主播 + 观众 + Multipath + Multipath Mode + Multipath Type + 设置层级配置(最多3层) + diff --git a/Android/APIExample/gradle.properties b/Android/APIExample/gradle.properties index b1a8ddbdd..384f9d744 100644 --- a/Android/APIExample/gradle.properties +++ b/Android/APIExample/gradle.properties @@ -25,4 +25,4 @@ simpleFilter = false # read enable stream encrypt section on README first before set this flag to TRUE streamEncrypt = false -rtc_sdk_version = 4.6.3 +rtc_sdk_version = 4.7.0 diff --git a/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj b/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj index 5cf3fcbf2..cdd261fb7 100644 --- a/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj +++ b/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj @@ -44,7 +44,6 @@ 576EA59025AEDD3C000B3D79 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6709B23B2806B0EA000BCC58 /* RawAudioData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6709B23A2806B0EA000BCC58 /* RawAudioData.swift */; }; 6709B23C2806BB4A000BCC58 /* RawAudioData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6709B23E2806BB4A000BCC58 /* RawAudioData.storyboard */; }; - 671BD6B827E1DB2D0076D5E1 /* CustomAudioRender.strings in Resources */ = {isa = PBXBuildFile; fileRef = 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */; }; 671BD6B927E1DB2D0076D5E1 /* CustomAudioRender.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */; }; 671BD6BA27E1DB2D0076D5E1 /* CustomAudioRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671BD6B727E1DB2D0076D5E1 /* CustomAudioRender.swift */; }; 67B8C7B52805757200195106 /* MediaUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 67B8C7B42805757200195106 /* MediaUtils.m */; }; @@ -59,7 +58,6 @@ A7BD7660247CC6920062A6B3 /* UITypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7BD765F247CC6920062A6B3 /* UITypeAlias.swift */; }; A7CA48C424553CF700507435 /* Popover.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A7CA48C224553CF600507435 /* Popover.storyboard */; }; DD8A1F812CA54F90001CEC51 /* AgoraPCMPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A1F802CA54F90001CEC51 /* AgoraPCMPlayer.swift */; }; - E70EE9E728F6A6FF00EE9E7F /* JoinChannelAudioToken.strings in Resources */ = {isa = PBXBuildFile; fileRef = E70EE9E228F6A6FF00EE9E7F /* JoinChannelAudioToken.strings */; }; E70EE9E828F6A6FF00EE9E7F /* JoinChannelAudioToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E70EE9E428F6A6FF00EE9E7F /* JoinChannelAudioToken.swift */; }; E70EE9E928F6A6FF00EE9E7F /* JoinChannelAudioToken.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E70EE9E528F6A6FF00EE9E7F /* JoinChannelAudioToken.storyboard */; }; E72055EC28F94C400030E6D1 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72055EB28F94C400030E6D1 /* Util.swift */; }; @@ -68,7 +66,6 @@ E74877CA28A2611C00CA2F58 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E74877C928A2611C00CA2F58 /* ToastView.swift */; }; E748789E28ADE03400CA2F58 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E748789C28ADE03400CA2F58 /* VideoView.swift */; }; E748789F28ADE03400CA2F58 /* VideoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E748789D28ADE03400CA2F58 /* VideoView.xib */; }; - E74878A628ADE21200CA2F58 /* RhythmPlayer.strings in Resources */ = {isa = PBXBuildFile; fileRef = E74878A128ADE21200CA2F58 /* RhythmPlayer.strings */; }; E74878A728ADE21200CA2F58 /* RhythmPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E74878A328ADE21200CA2F58 /* RhythmPlayer.swift */; }; E74878A828ADE21200CA2F58 /* RhythmPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E74878A428ADE21200CA2F58 /* RhythmPlayer.storyboard */; }; E7A49CBC29023BD300F06DD4 /* AlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A49CBB29023BD300F06DD4 /* AlertManager.swift */; }; @@ -414,7 +411,6 @@ 671BD6B227E1DB2D0076D5E1 /* CustomAudioRender */ = { isa = PBXGroup; children = ( - 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */, 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */, 671BD6B727E1DB2D0076D5E1 /* CustomAudioRender.swift */, ); @@ -480,7 +476,6 @@ E70EE9E128F6A6FF00EE9E7F /* JoinChannelAudio(Token) */ = { isa = PBXGroup; children = ( - E70EE9E228F6A6FF00EE9E7F /* JoinChannelAudioToken.strings */, E70EE9E428F6A6FF00EE9E7F /* JoinChannelAudioToken.swift */, E70EE9E528F6A6FF00EE9E7F /* JoinChannelAudioToken.storyboard */, ); @@ -500,7 +495,6 @@ E74878A028ADE21200CA2F58 /* RhythmPlayer */ = { isa = PBXGroup; children = ( - E74878A128ADE21200CA2F58 /* RhythmPlayer.strings */, E74878A328ADE21200CA2F58 /* RhythmPlayer.swift */, E74878A428ADE21200CA2F58 /* RhythmPlayer.storyboard */, ); @@ -591,8 +585,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E74878A628ADE21200CA2F58 /* RhythmPlayer.strings in Resources */, - E70EE9E728F6A6FF00EE9E7F /* JoinChannelAudioToken.strings in Resources */, 03BEED0B251C4446005E78F4 /* audiomixing.mp3 in Resources */, 033A9F89252D8B6C00BC26E1 /* PrecallTest.storyboard in Resources */, 033A9F2A252D737900BC26E1 /* Localizable.strings in Resources */, @@ -610,7 +602,6 @@ 67CB2F0D27EB318200CB19D2 /* SpatialAudio.storyboard in Resources */, 033A9F4D252D89DB00BC26E1 /* CustomAudioSource.storyboard in Resources */, 033A9F30252D860100BC26E1 /* JoinChannelAudio.storyboard in Resources */, - 671BD6B827E1DB2D0076D5E1 /* CustomAudioRender.strings in Resources */, 03D13BD92448758B00B599B3 /* Assets.xcassets in Resources */, E748789F28ADE03400CA2F58 /* VideoView.xib in Resources */, E74878A828ADE21200CA2F58 /* RhythmPlayer.storyboard in Resources */, @@ -805,18 +796,11 @@ name = RawAudioData.storyboard; sourceTree = ""; }; - 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */ = { - isa = PBXVariantGroup; - children = ( - 671BD6B427E1DB2D0076D5E1 /* zh-Hans */, - ); - name = CustomAudioRender.strings; - sourceTree = ""; - }; 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */ = { isa = PBXVariantGroup; children = ( 671BD6B627E1DB2D0076D5E1 /* Base */, + 671BD6B427E1DB2D0076D5E1 /* zh-Hans */, ); name = CustomAudioRender.storyboard; sourceTree = ""; @@ -829,34 +813,20 @@ name = Popover.storyboard; sourceTree = ""; }; - E70EE9E228F6A6FF00EE9E7F /* JoinChannelAudioToken.strings */ = { - isa = PBXVariantGroup; - children = ( - E70EE9E328F6A6FF00EE9E7F /* zh-Hans */, - ); - name = JoinChannelAudioToken.strings; - sourceTree = ""; - }; E70EE9E528F6A6FF00EE9E7F /* JoinChannelAudioToken.storyboard */ = { isa = PBXVariantGroup; children = ( E70EE9E628F6A6FF00EE9E7F /* Base */, + E70EE9E328F6A6FF00EE9E7F /* zh-Hans */, ); name = JoinChannelAudioToken.storyboard; sourceTree = ""; }; - E74878A128ADE21200CA2F58 /* RhythmPlayer.strings */ = { - isa = PBXVariantGroup; - children = ( - E74878A228ADE21200CA2F58 /* zh-Hans */, - ); - name = RhythmPlayer.strings; - sourceTree = ""; - }; E74878A428ADE21200CA2F58 /* RhythmPlayer.storyboard */ = { isa = PBXVariantGroup; children = ( E74878A528ADE21200CA2F58 /* Base */, + E74878A228ADE21200CA2F58 /* zh-Hans */, ); name = RhythmPlayer.storyboard; sourceTree = ""; @@ -1005,7 +975,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1075,7 +1045,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 7a6635018..6ab9e2105 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -1,66 +1,65 @@ +/* Element = "label"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "混音播放音量"; -/* Class = "UILabel"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ -"07c-He-s8j.text" = "Mixing Playback Volume"; +/* Element = "button"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ +"1zo-J9-vQy.normalTitle" = "暂停"; -/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ -"1zo-J9-vQy.normalTitle" = "Pause"; +/* Element = "label"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ +"4Y1-AZ-KwW.text" = "混音控制"; -/* Class = "UILabel"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ -"4Y1-AZ-KwW.text" = "Audio Mixing Controls"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ +"54l-lw-iap.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ -"54l-lw-iap.normalTitle" = "Stop"; +/* Element = "label"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ +"5o8-Cv-WLg.text" = "音效控制"; -/* Class = "UILabel"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ -"5o8-Cv-WLg.text" = "Audio Effect Controls"; +/* Element = "button"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ +"CRH-0X-9T4.normalTitle" = "恢复播放"; -/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ -"CRH-0X-9T4.normalTitle" = "Resume"; +/* Element = "label"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "混音音量"; -/* Class = "UILabel"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ -"DJt-Y7-fkM.text" = "Mixing Volume"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "button"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ +"J8R-TU-x8W.normalTitle" = "开始"; -/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ -"J8R-TU-x8W.normalTitle" = "Start"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; -/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ -"Q0E-5B-IED.text" = "Audio Scenario"; +/* Element = "label"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "混音发布音量"; -/* Class = "UILabel"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ -"VMe-lv-SUb.text" = "Mixing Publish Volume"; - -/* Class = "UILabel"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ +/* Element = "label"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ "cJ6-0Q-fAp.text" = "00 : 00"; -/* Class = "UILabel"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ -"e6E-so-zA5.text" = "Effect Volume"; +/* Element = "label"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ +"e6E-so-zA5.text" = "音效音量"; -/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ -"iUn-XK-AS2.text" = "Audio Profile"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频属性配置"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ -"jRA-VE-1PM.normalTitle" = "Resume"; +/* Element = "button"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ +"jRA-VE-1PM.normalTitle" = "恢复播放"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ -"jxp-ZN-2yG.title" = "Join Channel Audio"; +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ -"m2n-wi-5Xx.normalTitle" = "Play"; +/* Element = "button"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ +"m2n-wi-5Xx.normalTitle" = "播放"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ -"nzY-OP-Heo.normalTitle" = "Stop"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ +"nzY-OP-Heo.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ -"u26-Qh-itu.normalTitle" = "Pause"; +/* Element = "button"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ +"u26-Qh-itu.normalTitle" = "暂停"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings index e1332f8a1..c4c41c43f 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings @@ -1,9 +1,8 @@ +/* Element = "viewController"; title = "Custom Audio Render"; ObjectID = "EbX-sK-6UJ"; */ +"EbX-sK-6UJ.title" = "音频自渲染"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "EbX-sK-6UJ"; */ -"EbX-sK-6UJ.title" = "Custom Audio Render"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings index d1f524d0d..868bfaafb 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings @@ -1,9 +1,8 @@ +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "FCW-Np-auB"; */ +"FCW-Np-auB.title" = "音频自采集"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "FCW-Np-auB"; */ -"FCW-Np-auB.title" = "Custom Audio Source"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings index 7431a98aa..b19931d22 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings @@ -1,15 +1,8 @@ +/* Element = "button"; normalTitle = "Join"; ObjectID = "6VE-rp-0Wl"; */ +"6VE-rp-0Wl.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "6VE-rp-0Wl"; */ -"6VE-rp-0Wl.normalTitle" = "Join Channel"; +/* Element = "label"; text = "Push PCM"; ObjectID = "Dbo-LJ-5lv"; */ +"Dbo-LJ-5lv.text" = "播放 PCM"; -/* Class = "UILabel"; text = "Microphone"; ObjectID = "Cwr-m3-kGh"; */ -"Cwr-m3-kGh.text" = "Microphone"; - -/* Class = "UILabel"; text = "Push PCM"; ObjectID = "Dbo-LJ-5lv"; */ -"Dbo-LJ-5lv.text" = "Push PCM"; - -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "SdL-FU-6ht"; */ -"SdL-FU-6ht.placeholder" = "Enter channel name"; - -/* Class = "UILabel"; text = "Play Audio"; ObjectID = "mZd-9B-Gf5"; */ -"mZd-9B-Gf5.text" = "Play Local Audio"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "SdL-FU-6ht"; */ +"SdL-FU-6ht.placeholder" = "输入频道名"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings index 6793f84c7..c56e1bc45 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings @@ -1,24 +1,17 @@ - -/* Class = "UILabel"; text = "Lastmile Network Pretest"; ObjectID = "3PN-IA-Upy"; */ -"3PN-IA-Upy.text" = "Lastmile Network Test"; - -/* Class = "UILabel"; text = "10"; ObjectID = "4WV-kQ-0aJ"; */ +/* Element = "label"; text = "10"; ObjectID = "4WV-kQ-0aJ"; */ "4WV-kQ-0aJ.text" = "10"; -/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "CVA-Q1-OGl"; */ -"CVA-Q1-OGl.normalTitle" = "Start"; - -/* Class = "UILabel"; text = "Now you should hear what you said..."; ObjectID = "MdV-HB-V93"; */ -"MdV-HB-V93.text" = "Now you should hear what you said in the last 10 seconds..."; +/* Element = "label"; text = "Now you should hear what you said..."; ObjectID = "MdV-HB-V93"; */ +"MdV-HB-V93.text" = "现在你应该能听到前10秒的声音..."; -/* Class = "UILabel"; text = "10"; ObjectID = "caY-D3-ysY"; */ +/* Element = "label"; text = "10"; ObjectID = "caY-D3-ysY"; */ "caY-D3-ysY.text" = "10"; -/* Class = "UILabel"; text = "Echo Pretest"; ObjectID = "e83-fp-COE"; */ -"e83-fp-COE.text" = "Audio Network Echo Test"; +/* Element = "label"; text = "Echo Pretest"; ObjectID = "e83-fp-COE"; */ +"e83-fp-COE.text" = "音频网络回路测试"; -/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "eol-rm-UUy"; */ -"eol-rm-UUy.normalTitle" = "Start"; +/* Element = "button"; normalTitle = "Start"; ObjectID = "eol-rm-UUy"; */ +"eol-rm-UUy.normalTitle" = "开始"; -/* Class = "UILabel"; text = "Please say something.."; ObjectID = "tFL-Md-flt"; */ -"tFL-Md-flt.text" = "Please say something.."; +/* Element = "label"; text = "Please say something.."; ObjectID = "tFL-Md-flt"; */ +"tFL-Md-flt.text" = "尝试说一些话.."; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings index ef72db427..f64291e05 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings @@ -1,6 +1,5 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GGI-Cu-Sy1"; */ +"GGI-Cu-Sy1.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GGI-Cu-Sy1"; */ -"GGI-Cu-Sy1.placeholder" = "Enter channel name"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "yhH-81-equ"; */ -"yhH-81-equ.normalTitle" = "Join Channel"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "yhH-81-equ"; */ +"yhH-81-equ.normalTitle" = "加入频道"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/zh-Hans.lproj/RhythmPlayer.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/zh-Hans.lproj/RhythmPlayer.strings index 41c975df1..4b01e151c 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/zh-Hans.lproj/RhythmPlayer.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/zh-Hans.lproj/RhythmPlayer.strings @@ -1,21 +1,20 @@ +/* Element = "button"; normalTitle = "Join"; ObjectID = "1q7-gO-naf"; */ +"1q7-gO-naf.normalTitle" = "加入"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "1q7-gO-naf"; */ -"1q7-gO-naf.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "4u1-SO-0Lt"; */ +"4u1-SO-0Lt.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "4u1-SO-0Lt"; */ -"4u1-SO-0Lt.normalTitle" = "Stop"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "IId-X0-RBj"; */ +"IId-X0-RBj.placeholder" = "请输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "IId-X0-RBj"; */ -"IId-X0-RBj.placeholder" = "Enter channel name"; +/* Element = "label"; text = "Beats per Minute"; ObjectID = "c9d-M8-gnA"; */ +"c9d-M8-gnA.text" = "每分钟节拍数"; -/* Class = "UILabel"; text = "Beats per Minute"; ObjectID = "c9d-M8-gnA"; */ -"c9d-M8-gnA.text" = "Beats per Minute"; +/* Element = "label"; text = "Beats per Measure"; ObjectID = "j6H-vY-Yns"; */ +"j6H-vY-Yns.text" = "每小节节拍数"; -/* Class = "UILabel"; text = "Beats per Measure"; ObjectID = "j6H-vY-Yns"; */ -"j6H-vY-Yns.text" = "Beats per Measure"; +/* Element = "viewController"; title = "Simple Filter"; ObjectID = "ubR-Ki-mJL"; */ +"ubR-Ki-mJL.title" = "虚拟节拍器"; -/* Class = "UIViewController"; title = "Simple Filter"; ObjectID = "ubR-Ki-mJL"; */ -"ubR-Ki-mJL.title" = "Virtual Metronome"; - -/* Class = "UIButton"; normalTitle = "Play"; ObjectID = "x1X-Gr-KYW"; */ -"x1X-Gr-KYW.normalTitle" = "Play"; +/* Element = "button"; normalTitle = "Play"; ObjectID = "x1X-Gr-KYW"; */ +"x1X-Gr-KYW.normalTitle" = "播放"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings index 5a897d8e1..54d9e7f24 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings @@ -1,51 +1,98 @@ +/* Element = "label"; text = "ReverbValue"; ObjectID = "0ym-dC-M2U"; */ +"0ym-dC-M2U.text" = "混响值"; -/* Class = "UILabel"; text = "BandGain"; ObjectID = "1Y2-Oo-uXg"; */ -"1Y2-Oo-uXg.text" = "Band Gain"; +/* Element = "label"; text = "Chat Beautifier"; ObjectID = "28o-tM-wcJ"; */ +"28o-tM-wcJ.text" = "语聊美声"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "3nP-Fh-fTT"; */ -"3nP-Fh-fTT.normalTitle" = "Button"; +/* Element = "label"; text = "Voice Conversion"; ObjectID = "3AP-5U-535"; */ +"3AP-5U-535.text" = "语音转换"; -/* Class = "UILabel"; text = "ReverbValue"; ObjectID = "5YF-rQ-jpm"; */ -"5YF-rQ-jpm.text" = "Reverb Value"; +/* Element = "label"; text = "BandFreq"; ObjectID = "6Fs-Xa-ZZd"; */ +"6Fs-Xa-ZZd.text" = "波段频率"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "9l7-uP-OVe"; */ -"9l7-uP-OVe.normalTitle" = "Button"; +/* Element = "button"; configuration.title = "BALANCED"; ObjectID = "7n8-3r-Ce8"; */ +"7n8-3r-Ce8.configuration.title" = "(默认)均衡降噪模式"; -/* Class = "UILabel"; text = "ReverbKey"; ObjectID = "CNw-fg-lwN"; */ -"CNw-fg-lwN.text" = "Reverb Property"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "7n8-3r-Ce8"; */ +"7n8-3r-Ce8.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "Cnn-JW-gFk"; */ -"Cnn-JW-gFk.normalTitle" = "Button"; +/* Element = "label"; text = "Style Transformation"; ObjectID = "86e-GB-8dT"; */ +"86e-GB-8dT.text" = "风格转换"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "label"; text = "AINSModel"; ObjectID = "8HC-H2-kxQ"; */ +"8HC-H2-kxQ.text" = "AI 降噪模式"; -/* Class = "UILabel"; text = "Customize Voice Effects"; ObjectID = "Glv-Ys-lDb"; */ -"Glv-Ys-lDb.text" = "Customize Voice Effects"; +/* Element = "label"; text = "Room Acoustics"; ObjectID = "D5S-zC-yH4"; */ +"D5S-zC-yH4.text" = "室内声学"; -/* Class = "UIButton"; normalTitle = "Change Voice"; ObjectID = "IgY-un-YoE"; */ -"IgY-un-YoE.normalTitle" = "Change Voice"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "Fzr-9u-xYR"; */ +"Fzr-9u-xYR.normalTitle" = "截图"; -/* Class = "UIViewController"; title = "VoiceChanger"; ObjectID = "Isk-Yn-Sjt"; */ -"Isk-Yn-Sjt.title" = "Voice Beautifier/Effects"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UILabel"; text = "Pitch"; ObjectID = "J9L-hL-6cu"; */ -"J9L-hL-6cu.text" = "Pitch"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "JlT-dt-wGb"; */ +"JlT-dt-wGb.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "Ry4-kh-SCQ"; */ -"Ry4-kh-SCQ.normalTitle" = "Button"; +/* Element = "label"; text = "N/A"; ObjectID = "PNk-wE-Qwv"; */ +"PNk-wE-Qwv.text" = "不适用"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "bHF-lw-k2f"; */ -"bHF-lw-k2f.normalTitle" = "Button"; +/* Element = "label"; text = "Pitch"; ObjectID = "Rza-0C-OeF"; */ +"Rza-0C-OeF.text" = "音调"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "dJC-qv-NZE"; */ -"dJC-qv-NZE.normalTitle" = "Button"; +/* Element = "label"; text = "ReverbKey"; ObjectID = "Sff-hC-KgZ"; */ +"Sff-hC-KgZ.text" = "混响属性"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "label"; text = "Pitch Correction"; ObjectID = "TKQ-E0-tL2"; */ +"TKQ-E0-tL2.text" = "高音修正"; -/* Class = "UILabel"; text = "BandFreq"; ObjectID = "m8A-hY-ceu"; */ -"m8A-hY-ceu.text" = "Band Frequency"; +/* Element = "label"; text = "Voice AI Tuner"; ObjectID = "U7m-qC-Efb"; */ +"U7m-qC-Efb.text" = "AI 音色调节"; -/* Class = "UILabel"; text = "Voice Beautifier & Effects Preset"; ObjectID = "qQS-En-EUC"; */ -"qQS-En-EUC.text" = "Voice Beautifier & Effects Preset"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "YHG-YI-kRP"; */ +"YHG-YI-kRP.normalTitle" = "截图"; + +/* Element = "button"; normalTitle = "Change Voice"; ObjectID = "d0c-fe-ELJ"; */ +"d0c-fe-ELJ.normalTitle" = "变声"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "dm0-GV-3am"; */ +"dm0-GV-3am.normalTitle" = "截图"; + +/* Element = "label"; text = "Customize Voice Effects"; ObjectID = "g02-yj-XcN"; */ +"g02-yj-XcN.text" = "自定义音效"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "gyg-kR-fNp"; */ +"gyg-kR-fNp.normalTitle" = "截图"; + +/* Element = "label"; text = "N/A"; ObjectID = "j5u-0S-mBh"; */ +"j5u-0S-mBh.text" = "不适用"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "jlA-wY-PwX"; */ +"jlA-wY-PwX.normalTitle" = "截图"; + +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Element = "label"; text = "Voice Changer"; ObjectID = "m4n-jw-bEd"; */ +"m4n-jw-bEd.text" = "美声/音效"; + +/* Element = "label"; text = "BandGain"; ObjectID = "rtj-ix-uVc"; */ +"rtj-ix-uVc.text" = "波段区间增益"; + +/* Element = "label"; text = "Timbre Transformation"; ObjectID = "s4X-Z2-Npk"; */ +"s4X-Z2-Npk.text" = "音色转换"; + +/* Element = "label"; text = "Voice Beautifier & Effects Preset"; ObjectID = "tRN-v8-ddi"; */ +"tRN-v8-ddi.text" = "美声/音效预设"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "vr8-dh-GeV"; */ +"vr8-dh-GeV.normalTitle" = "截图"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "wL9-fx-76f"; */ +"wL9-fx-76f.normalTitle" = "截图"; + +/* Element = "label"; text = "Voice Formant"; ObjectID = "yst-66-AHA"; */ +"yst-66-AHA.text" = "语音共振峰"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "zDI-bd-cnc"; */ +"zDI-bd-cnc.normalTitle" = "截图"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/zh-Hans.lproj/JoinChannelAudioToken.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/zh-Hans.lproj/JoinChannelAudioToken.strings index c1548454f..ca849227c 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/zh-Hans.lproj/JoinChannelAudioToken.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/zh-Hans.lproj/JoinChannelAudioToken.strings @@ -1,38 +1,59 @@ +/* Element = "label"; text = "PlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "播放音量"; -/* Class = "UILabel"; text = "PlaybackVolume"; ObjectID = "07c-He-s8j"; */ -"07c-He-s8j.text" = "Playback Volume"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "8sL-pn-gKs"; */ +"8sL-pn-gKs.text" = "音频使用场景"; -/* Class = "UILabel"; text = "RecordingVolume"; ObjectID = "DJt-Y7-fkM"; */ -"DJt-Y7-fkM.text" = "Recording Volume"; +/* Element = "label"; text = "channel name"; ObjectID = "DIm-MK-PkS"; */ +"DIm-MK-PkS.text" = "频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "label"; text = "RecordingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "采集音量"; -/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ -"Q0E-5B-IED.text" = "Audio Scenario"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UILabel"; text = "InEar Monitoring Volume"; ObjectID = "VMe-lv-SUb"; */ -"VMe-lv-SUb.text" = "In-ear Monitoring Volume"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; -/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ -"iUn-XK-AS2.text" = "Audio Profile"; +/* Element = "label"; text = "AppID"; ObjectID = "QXh-3k-cMB"; */ +"QXh-3k-cMB.text" = "AppID"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; +/* Element = "label"; text = "Token"; ObjectID = "UfA-fN-3hm"; */ +"UfA-fN-3hm.text" = "Token"; -/* Class = "UILabel"; text = "InEar Monitoring"; ObjectID = "iru-5f-bbo"; */ -"iru-5f-bbo.text" = "In-ear Monitoring"; +/* Element = "label"; text = "InEar Monitoring Volume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "耳返音量"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ -"jxp-ZN-2yG.title" = "Voice Call/Live Broadcasting"; +/* Element = "navigationItem"; title = "Title"; ObjectID = "WSA-CX-1rO"; */ +"WSA-CX-1rO.title" = "标题"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "textField"; placeholder = "Please the input APPID"; ObjectID = "Xvj-7d-EMy"; */ +"Xvj-7d-EMy.placeholder" = "请输入 App ID"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "button"; configuration.title = "Button"; ObjectID = "ZSJ-cf-f63"; */ +"ZSJ-cf-f63.configuration.title" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "ZSJ-cf-f63"; */ +"ZSJ-cf-f63.normalTitle" = "截图"; -"dqn-Pg-ubc.placeholder" = "Enter Token"; +/* Element = "textField"; placeholder = "Enter Token"; ObjectID = "dqn-Pg-ubc"; */ +"dqn-Pg-ubc.placeholder" = "输入 Token"; -"Xvj-7d-EMy.placeholder" = "Enter APPID"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频参数配置"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "截图"; + +/* Element = "label"; text = "InEar Monitoring"; ObjectID = "iru-5f-bbo"; */ +"iru-5f-bbo.text" = "耳返"; + +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; + +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "截图"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings index cb5a03547..e3079124a 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings @@ -1,33 +1,41 @@ +/* Element = "label"; text = "PlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "播放音量"; -/* Class = "UILabel"; text = "PlaybackVolume"; ObjectID = "07c-He-s8j"; */ -"07c-He-s8j.text" = "Playback Volume"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "8sL-pn-gKs"; */ +"8sL-pn-gKs.text" = "音频使用场景"; -/* Class = "UILabel"; text = "RecordingVolume"; ObjectID = "DJt-Y7-fkM"; */ -"DJt-Y7-fkM.text" = "Recording Volume"; +/* Element = "label"; text = "RecordingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "采集音量"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ -"Q0E-5B-IED.text" = "Audio Scenario"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; -/* Class = "UILabel"; text = "InEar Monitoring Volume"; ObjectID = "VMe-lv-SUb"; */ -"VMe-lv-SUb.text" = "In-ear Monitoring Volume"; +/* Element = "label"; text = "InEar Monitoring Volume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "耳返音量"; -/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ -"iUn-XK-AS2.text" = "Audio Profile"; +/* Element = "button"; configuration.title = "Button"; ObjectID = "ZSJ-cf-f63"; */ +"ZSJ-cf-f63.configuration.title" = "Button"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "ZSJ-cf-f63"; */ +"ZSJ-cf-f63.normalTitle" = "截图"; -/* Class = "UILabel"; text = "InEar Monitoring"; ObjectID = "iru-5f-bbo"; */ -"iru-5f-bbo.text" = "In-ear Monitoring"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频参数配置"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ -"jxp-ZN-2yG.title" = "Voice Call/Live Broadcasting"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "label"; text = "InEar Monitoring"; ObjectID = "iru-5f-bbo"; */ +"iru-5f-bbo.text" = "耳返"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; + +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "截图"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings index fb0219a0c..45f1991e2 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings @@ -6,145 +6,155 @@ Copyright © 2020 Agora Corp. All rights reserved. */ -"Join a channel (Token)" = "Voice Call/Live Broadcasting (Token)"; -"Join a channel (Audio)" = "Voice Call/Live Broadcasting"; -"Live Streaming" = "RTC Live Broadcasting/Host/Audience"; -"RTMP Streaming" = "RTMP Streaming"; -"Media Injection" = "Media Injection"; -"Video Metadata" = "SEI Message"; -"Voice Changer" = "Voice Effects"; -"Custom Audio Source" = "Custom Audio Source"; -"Custom Audio Source(PCM)" = "Custom Audio Source(PCM)"; -"Custom Audio Render" = "Custom Audio Render"; -"Custom Video Source(MediaIO)" = "Custom Video Source(MediaIO)"; -"Custom Video Source(Push)" = "Custom Video Source(Push)"; -"Custom Video Render" = "Custom Video Render(Metal)"; -"Quick Switch Channel" = "Quick Switch Channel"; -"Join Multiple Channels" = "Join Multiple Channels"; -"Stream Encryption" = "Stream Encryption"; -"Audio Mixing" = "Audio Mixing"; -"Raw Media Data" = "Raw Media Data"; -"Raw Video Data" = "Raw Video Data"; -"Simple Filter Extension" = "Plugin System"; -"Precall Test" = "Pre-call Network/Device Test"; -"Media Player" = "Media Player"; -"Screen Share" = "Screen Share"; -"Super Resolution" = "Super Resolution"; -"Media Channel Relay" = "Channel Media Relay"; -"Set Resolution" = "Set Video Resolution"; -"Set Fps" = "Set Video Frame Rate"; -"Set Orientation" = "Set Video Orientation"; -"Set Chat Beautifier" = "Set Chat Beautifier"; -"Set Timbre Transformation" = "Set Timbre Transformation"; -"Set Voice Changer" = "Set Voice Changer"; -"Set Style Transformation" = "Set Style Transformation"; -"Set Room Acoustics" = "Set Room Acoustics"; -"Set Band Frequency" = "Set Band Frequency"; -"Set Reverb Key" = "Set Reverb Properties"; -"Set Encryption Mode" = "Set Encryption Mode"; -"fixed portrait" = "Fixed Portrait"; -"fixed landscape" = "Fixed Landscape"; -"adaptive" = "Adaptive"; -"Local Host" = "Local Preview"; -"Remote Host" = "Remote Video"; -"Set Audio Profile" = "Set Audio Profile"; -"Set Audio Scenario" = "Set Audio Scenario"; -"Default" = "Default"; -"Music Standard" = "Music Standard"; -"Music Standard Stereo" = "Music Standard Stereo"; -"Music High Quality" = "Music High Quality"; -"Music High Quality Stereo" = "Music High Quality Stereo"; -"Speech Standard" = "Speech Standard"; -"Chat Room Gaming" = "Chat Room Gaming"; -"Education" = "Education"; -"Game Streaming" = "High Quality Chat Room"; -"High Defination" = "High Quality Music"; -"Chat Room Entertainment" = "Game Chat"; -"Show Room" = "Show Room"; -"Cancel" = "Cancel"; -"Off" = "Original"; -"FemaleFresh" = "Chat Beautifier: Fresh(Female)"; -"FemaleVitality" = "Chat Beautifier: Vitality(Female)"; -"MaleMagnetic" = "Chat Beautifier: Magnetic(Male)"; -"Vigorous" = "Vigorous"; -"Deep" = "Deep"; -"Mellow" = "Mellow"; -"Falsetto" = "Falsetto"; -"Full" = "Full"; -"Clear" = "Clear"; -"Resounding" = "Resounding"; -"Ringing" = "Ringing"; -"Spacial" = "Spacial"; -"Ethereal" = "Ethereal"; -"Old Man" = "Old Man"; -"Baby Boy" = "Baby Boy"; -"Baby Girl" = "Baby Girl"; -"ZhuBaJie" = "ZhuBaJie"; -"Hulk" = "Hulk"; -"FxUncle" = "Uncle"; -"FxSister" = "Sister"; -"Pop" = "Pop"; -"Pop(Old Version)" = "Pop(Old Version)"; +"Join a channel (Token)" = "实时语音通话/直播(Token验证)"; +"Join a channel (Audio)" = "实时语音通话/直播"; +"Live Streaming" = "RTC实时直播/主播/观众"; +"RTMP Streaming" = "RTMP旁路推流"; +"Video Metadata" = "SEI消息"; +"Voice Changer" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source(Push)" = "视频自采集(Push)"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Precall Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Media Channel Relay" = "跨频道流转发"; +"Set Resolution" = "设置视频分辨率"; +"Set Fps" = "设置视频帧率"; +"Set Orientation" = "设置视频朝向"; +"Set Chat Beautifier" = "设置语聊美声"; +"Set Timbre Transformation" = "设置音色变换"; +"Set Voice Changer" = "设置变声音效"; +"Set Style Transformation" = "设置曲风音效"; +"Set Room Acoustics" = "设置空间音效"; +"Set Band Frequency" = "设置波段频率"; +"Set Reverb Key" = "设置混响属性"; +"Set Encryption Mode" = "设置加密模式"; +"fixed portrait" = "固定纵向"; +"fixed landscape" = "固定横向"; +"adaptive" = "自适应"; +"Local Host" = "本地预览"; +"Remote Host" = "远端视频"; +"Set Audio Profile" = "设置音频参数配置"; +"Set Audio Scenario" = "设置音频使用场景"; +"Default" = "默认"; +"Music Standard" = "标准音乐"; +"Music Standard Stereo" = "标准双声道音乐"; +"Music High Quality" = "高音质音乐"; +"Music High Quality Stereo" = "高音质双声道音乐"; +"Speech Standard" = "标准人声"; +"Chat Room Gaming" = "娱乐语聊房"; +"Education" = "教育"; +"Game Streaming" = "高音质语聊房"; +"High Defination" = "高音质音乐"; +"Chat Room Entertainment" = "游戏开黑"; +"Show Room" = "秀场"; +"Cancel" = "取消"; +"Off" = "原声"; +"FemaleFresh" = "语聊美声: 清新(女)"; +"FemaleVitality" = "语聊美声: 活力(女)"; +"MaleMagnetic" = "语聊美声: 磁性(男)"; +"Vigorous" = "浑厚"; +"Deep" = "低沉"; +"Mellow" = "圆润"; +"Falsetto" = "假音"; +"Full" = "饱满"; +"Clear" = "清澈"; +"Resounding" = "高亢"; +"Ringing" = "嘹亮"; +"Spacial" = "空旷"; +"Ethereal" = "空灵"; +"Old Man" = "老男孩"; +"Baby Boy" = "小男孩"; +"Baby Girl" = "小女孩"; +"ZhuBaJie" = "猪八戒"; +"Hulk" = "绿巨人"; +"FxUncle" = "大叔"; +"FxSister" = "小姐姐"; +"Pop" = "流行"; +"Pop(Old Version)" = "流行(旧版)"; "R&B" = "R&B"; -"R&B(Old Version)" = "R&B(Old Version)"; -"Rock" = "Rock"; -"HipHop" = "HipHop"; -"Vocal Concert" = "Vocal Concert"; -"Vocal Concert(Old Version)" = "Vocal Concert(Old Version)"; +"R&B(Old Version)" = "R&B(旧版)"; +"Rock" = "摇滚"; +"HipHop" = "嘻哈"; +"Vocal Concert" = "演唱会"; +"Vocal Concert(Old Version)" = "演唱会(旧版)"; "KTV" = "KTV"; -"KTV(Old Version)" = "KTV(Old Version)"; -"Studio" = "Studio"; -"Studio(Old Version)" = "Studio(Old Version)"; -"Phonograph" = "Phonograph"; -"Virtual Stereo" = "Virtual Stereo"; -"Dry Level" = "Dry Level"; -"Wet Level" = "Early Reflection Signal Strength"; -"Room Size" = "Room Size"; -"Wet Delay" = "Early Reflection Signal Delay"; -"Strength" = "Reverb Sustain Strength"; -"ARKit is not available on this device." = "ARKit is not available on this device."; -"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "This app requires world tracking, which is available only on iOS devices with the A9 processor or later."; -"Move Camera to find a planar\n(Shown as Red Rectangle)" = "Move Camera to find a planar\n(Shown as Red Rectangle)"; -"Tap to place remote video canvas" = "Tap to place remote video canvas"; -"Resolution" = "Resolution"; -"Frame Rate" = "Frame Rate"; -"Orientation" = "Video Orientation"; -"Broadcaster" = "Broadcaster"; -"Audience" = "Audience"; -"Pick Role" = "Pick Role"; -"Create Data Stream" = "Create Data Stream"; -"Raw Audio Data" = "Raw Audio Data"; -"Group Video Chat" = "Group Video Chat"; -"Screen Share + Camera" = "Screen Share + Camera"; -"please input channel name!" = "Please input channel name!"; -"please input AppId!" = "Please input AppId!"; -"Video Process" = "Video Enhancement"; -"Rhythm Player" = "Virtual Metronome"; -"Spatial Audio" = "Spatial Audio"; -"Please insert headphones to experience the spatial audio effect.Now you can move the speaker icon to experience the spatial audio effect" = "Please insert headphones to experience the spatial audio effect. Now you can move the speaker icon to experience the spatial audio effect"; -"Content Inspect" = "Content Moderation"; -"Video Neutral" = "Video Normal"; -"Video Sexy" = "Video Sexy"; -"Video Porn" = "Video Porn"; -"SwitchCamera" = "Switch Camera"; -"remote user has not joined, and cannot take a screenshot" = "Remote user has not joined, cannot take screenshot"; -"Screenshot successful" = "Screenshot successful"; -"please input Token!" = "Please input Token!"; -"Quick input APPID and Token methods" = "Quick input APPID and Token methods"; -"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n II: use https://cl1p.net/ online clipboard:\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2.Paste in anything you want.\n\n3.On another computer enter the same URL and get your stuff." = "I: If the phone and Mac are logged into the same Apple account, after copying on Mac it will automatically sync to other devices with the same account. The phone can directly tap the input box to paste.\n\n II: Use https://cl1p.net/ online clipboard:\n\n1. Enter a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2. Paste anything you want.\n\n3. On another computer enter the same URL and get your content."; +"KTV(Old Version)" = "KTV(旧版)"; +"Studio" = "录音棚"; +"Studio(Old Version)" = "录音棚(旧版)"; +"Phonograph" = "留声机"; +"Virtual Stereo" = "虚拟立体声"; +"Dry Level" = "原始声音强度"; +"Wet Level" = "早期反射信号强度"; +"Room Size" = "房间尺寸"; +"Wet Delay" = "早期反射信号延迟"; +"Strength" = "混响持续强度"; +"ARKit is not available on this device." = "当前设备不支持ARKit"; +"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "AR功能仅在内置A9处理器后的iOS机型支持"; +"Move Camera to find a planar\n(Shown as Red Rectangle)" = "移动相机以找到一个平面\n(以红色方块显示)"; +"Tap to place remote video canvas" = "点击屏幕以放置视频画布"; +"Resolution" = "分辨率"; +"Frame Rate" = "帧率"; +"Orientation" = "视频朝向"; +"Broadcaster" = "主播"; +"Audience" = "观众"; +"Pick Role" = "选择角色"; +"Create Data Stream" = "创建数据流"; +"Raw Audio Data" = "音频裸数据"; +"Screen Share + Camera" = "屏幕共享 + 摄像头"; +"please input channel name!" = "请输入频道名!"; +"please input AppId!" = "请输入APPID"; +"Video Process" = "视频增强"; +"Rhythm Player" = "虚拟节拍器"; +"Spatial Audio" = "空间音效"; +"Please insert headphones to experience the spatial audio effect.Now you can move the speaker icon to experience the spatial audio effect" = "请插入耳机体验空间音效效果;现在您可以移动喇叭图标到不同的位置,体验空间音效效果"; +"Content Inspect" = "视频鉴黄"; +"Video Neutral" = "视频正常"; +"Video Sexy" = "视频性感"; +"Video Porn" = "视频色情"; +"SwitchCamera" = "切换摄像头"; +"remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; +"Screenshot successful" = "截图成功"; +"please input Token!" = "请输入Token"; +"Quick input APPID and Token methods" = "快捷输入APPID和Token方法"; +"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n II: use https://cl1p.net/ online clipboard:\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2.Paste in anything you want.\n\n3.On another computer enter the same URL and get your stuff." = "I: 如果手机和Mac登录的同一个Apple账号, Mac复制后会自动同步到其它相同账号终端, 手机可直接点击输入框粘贴即可.\n\n II: 使用https://cl1p.net/在线剪切板:\n\n1.输入以cl1p.net开头的URL。示例cl1p.net/uqztgjnqcalmd。\n\n2.粘贴任何你想要的东西。\n\n3.在另一台计算机上,输入相同的URL并获取您的东西。"; -"AGGRESSIVE" = "Aggressive Noise Reduction"; -"BALANCED" = "(Default) Balanced Noise Reduction"; -"ULTRALOWLATENCY" = "Ultra Low Latency Noise Reduction"; +"AGGRESSIVE" = "强降噪模式"; +"BALANCED" = "(默认)均衡降噪模式"; +"ULTRALOWLATENCY" = "低延时强降噪模式"; -"Set_Voice_AI_Tuner"="Set AI Voice Tuner"; -"AI_Tunner_Mature_Male"="Mature Male Voice"; -"AI_Tunner_Fresh_Male"="Fresh Male Voice"; -"AI_Tunner_Elegant_Female"="Elegant Female Voice"; -"AI_Tunner_Sweet_Female"="Sweet Female Voice"; -"AI_Tunner_Warm_Male_Singing"="Warm Male Singing"; -"AI_Tunner_Gentle_Female_Singing"="Gentle Female Singing"; -"AI_Tunner_Husky_Male_Singing"="Husky Male Singing"; -"AI_Tunner_Warm_Elegant_Female_Singing"="Warm Elegant Female Singing"; -"AI_Tunner_Powerful_Male_Singing"="Powerful Male Singing"; -"AI_Tunner_Dreamy_Female_Singing"="Dreamy Female Singing"; +"Set_Voice_AI_Tuner"= "设置AI调音器"; +"AI_Tunner_Mature_Male"= "大叔声"; +"AI_Tunner_Fresh_Male"= "清新男音"; +"AI_Tunner_Elegant_Female"= "御姐音"; +"AI_Tunner_Sweet_Female"= "萝莉音"; +"AI_Tunner_Warm_Male_Singing"= "暖男歌声"; +"AI_Tunner_Gentle_Female_Singing"= "温柔女歌声"; +"AI_Tunner_Husky_Male_Singing"= "烟嗓叔音歌声"; +"AI_Tunner_Warm_Elegant_Female_Singing"= "温暖御姐歌声"; +"AI_Tunner_Powerful_Male_Singing"= "力量男歌声"; +"AI_Tunner_Dreamy_Female_Singing"= "梦幻女歌声"; + +"3D Voice" = "3D 人声"; +"Pitch Correction" = "音高校正"; +"Neutral" = "中性"; +"Sweet" = "甜美"; +"Solid" = "浑厚"; +"Bass" = "低沉"; +"Please move the red icon to experience the 3D audio effect" = "请移动红色图标体验 3D 音频效果"; +"Mute" = "静音"; +"Voice Blur" = "人声模糊"; +"Airborne Simulation" = "空气传播模拟"; +"Attenuatuin" = "衰减"; +"Set AINS Mode" = "设置 AI 降噪模式"; +"Set Pitch Correction" = "设置音高校正"; +"Set Voice Conversion" = "设置音色转换"; diff --git a/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Main.strings b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Main.strings index 67e6d7224..9bc994117 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Main.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Main.strings @@ -1,27 +1,44 @@ +/* Element = "label"; text = "Label"; ObjectID = "2VP-yo-86d"; */ +"2VP-yo-86d.text" = "标签"; -/* Class = "UIViewController"; title = "Agora API Examples"; ObjectID = "BYZ-38-t0r"; */ -"BYZ-38-t0r.title" = "Agora API Examples"; +/* Element = "barButtonItem"; title = "Settings"; ObjectID = "492-pb-Xmc"; */ +"492-pb-Xmc.title" = "设置"; -/* Class = "UILabel"; text = "0"; ObjectID = "GRE-S2-EUw"; */ +/* Element = "label"; text = "Label"; ObjectID = "9tp-Wu-WoW"; */ +"9tp-Wu-WoW.text" = "标签"; + +/* Element = "viewController"; title = "Agora API Examples"; ObjectID = "BYZ-38-t0r"; */ +"BYZ-38-t0r.title" = "Agora API 示例"; + +/* Element = "label"; text = "Label"; ObjectID = "GJJ-sr-QDR"; */ +"GJJ-sr-QDR.text" = "标签"; + +/* Element = "label"; text = "0"; ObjectID = "GRE-S2-EUw"; */ "GRE-S2-EUw.text" = "0"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Element = "label"; text = "Label"; ObjectID = "MbC-re-uMa"; */ +"MbC-re-uMa.text" = "标签"; + +/* Element = "navigationItem"; title = "Join Channel"; ObjectID = "O4p-Hd-Lr5"; */ +"O4p-Hd-Lr5.title" = "加入频道"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "O4p-Hd-Lr5"; */ -"O4p-Hd-Lr5.title" = "Join Channel"; +/* Element = "label"; text = "Label"; ObjectID = "Ruy-K9-CLg"; */ +"Ruy-K9-CLg.text" = "标签"; -/* Class = "UILabel"; text = "Label"; ObjectID = "Ruy-K9-CLg"; */ -"Ruy-K9-CLg.text" = "Label"; +/* Element = "navigationItem"; title = "Agora API Example"; ObjectID = "Ygc-Og-WKK"; */ +"Ygc-Og-WKK.title" = "Agora API 示例"; -/* Class = "UINavigationItem"; title = "Agora API Example"; ObjectID = "Ygc-Og-WKK"; */ -"Ygc-Og-WKK.title" = "Agora API Example"; +/* Element = "viewController"; title = "Log View Controller"; ObjectID = "ekP-NH-UjU"; */ +"ekP-NH-UjU.title" = "日志"; -/* Class = "UIViewController"; title = "Log View Controller"; ObjectID = "ekP-NH-UjU"; */ -"ekP-NH-UjU.title" = "Log View Controller"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "iib-g5-GmB"; */ +"iib-g5-GmB.title" = "视频实时通话"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "iib-g5-GmB"; */ -"iib-g5-GmB.title" = "Join Channel Video"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "textField"; placeholder = "Please enter"; ObjectID = "vLd-NQ-B4e"; */ +"vLd-NQ-B4e.placeholder" = "请输入"; diff --git a/iOS/APIExample-Audio/Podfile b/iOS/APIExample-Audio/Podfile index a256d1e0d..2830cfbe6 100644 --- a/iOS/APIExample-Audio/Podfile +++ b/iOS/APIExample-Audio/Podfile @@ -7,7 +7,7 @@ target 'APIExample-Audio' do pod 'Floaty', '~> 4.2.0' pod 'AGEVideoLayout', '~> 1.0.2' - pod 'AgoraAudio_iOS', '4.6.2' + pod 'AgoraAudio_iOS', '4.7.0' # pod 'sdk', :path => 'sdk.podspec' end diff --git a/iOS/APIExample-Audio/README.zh.md b/iOS/APIExample-Audio/README.zh.md new file mode 100644 index 000000000..7ad708d4f --- /dev/null +++ b/iOS/APIExample-Audio/README.zh.md @@ -0,0 +1,64 @@ +# API Example iOS + +*[English](README.md) | 中文* + +本项目提供一组 Agora RTC 音频 API 示例,帮助你了解各项 API 的使用方式。 + +## 问题描述 + +在 iOS 14.0 及以上版本中,应用首次使用 Agora RTC SDK 时,系统会请求访问本地网络设备: + +![](../pictures/ios_14_privacy.png) + +[解决方案](https://docs.agora.io/en/help/integration-issues/local_network_privacy) + +## 环境准备 + +- Xcode 13.0 或更高版本 +- iPhone 或 iPad 真机 +- 不支持 iOS 模拟器 + +## 快速开始 + +以下步骤说明如何安装依赖、配置项目并运行示例。 + +### 安装依赖 + +进入当前示例目录,然后安装 CocoaPods 依赖。CocoaPods 的安装方式请参考[官方指南](https://guides.cocoapods.org/using/getting-started.html)。 + +```bash +cd iOS/APIExample-Audio +pod install +``` + +确认 `APIExample-Audio.xcworkspace` 已成功生成。 + +### 获取 App ID + +1. 登录 [Agora Console](https://console.agora.io/) 并创建项目。 +2. 在项目详情页复制 App ID。 +3. 使用 Xcode 打开 `APIExample-Audio.xcworkspace`。 +4. 编辑 `APIExample-Audio/Common/KeyCenter.swift`,填入 App ID: + + ```swift + static let AppId: String = "" + static let Certificate: String? = nil + ``` + +如果项目未启用 App Certificate,请保持 `Certificate` 为 `nil`。App Certificate 属于敏感信息,生产环境应在服务端生成 Token,不应将证书写入客户端或提交到 Git。 + +连接 iPhone 或 iPad 真机后,即可在 Xcode 中构建并运行项目。 + +## 联系我们 + +- 常见问题请查看 [Agora FAQ](https://docs.agora.io/en/faq) +- 更多示例请查看 [AgoraIO](https://github.com/AgoraIO) +- 复杂场景示例请查看 [AgoraIO Use Cases](https://github.com/AgoraIO-usecase) +- 社区维护项目请查看 [AgoraIO Community](https://github.com/AgoraIO-Community) +- 完整 API 文档请查看 [Agora Documentation](https://docs.agora.io/en/) +- 集成问题可以在 [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) 提问 +- 示例问题请提交到 [API-Examples Issues](https://github.com/AgoraIO/API-Examples/issues) + +## 代码许可 + +本项目采用 MIT License。 diff --git a/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj b/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj index 1469fa683..2b2a2baa0 100644 --- a/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj +++ b/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj @@ -187,6 +187,8 @@ AB8474132DF9CD7900B4DB73 /* Simulcast.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Simulcast.storyboard; path = Base.lproj/Simulcast.storyboard; sourceTree = ""; }; AB84741C2DF9CD9300B4DB73 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Multipath.strings; sourceTree = ""; }; AB84741E2DF9CD9B00B4DB73 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Simulcast.strings; sourceTree = ""; }; + ABB90ACA2DF728F8007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Multipath.strings"; sourceTree = ""; }; + ABB90ACC2DF7293C007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Simulcast.strings"; sourceTree = ""; }; B653B1910F3D2DB664FFCA69 /* Pods-Agora-ScreenShare-Extension-OC.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScreenShare-Extension-OC.debug.xcconfig"; path = "Target Support Files/Pods-Agora-ScreenShare-Extension-OC/Pods-Agora-ScreenShare-Extension-OC.debug.xcconfig"; sourceTree = ""; }; BBBCB16091148F518866D952 /* Pods-SimpleFilter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleFilter.release.xcconfig"; path = "Target Support Files/Pods-SimpleFilter/Pods-SimpleFilter.release.xcconfig"; sourceTree = ""; }; C4F0E8C20AFE260912A60198 /* Pods-Agora-ScreenShare-Extension-OC.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScreenShare-Extension-OC.release.xcconfig"; path = "Target Support Files/Pods-Agora-ScreenShare-Extension-OC/Pods-Agora-ScreenShare-Extension-OC.release.xcconfig"; sourceTree = ""; }; @@ -1395,6 +1397,7 @@ children = ( AB84740D2DF9CD7900B4DB73 /* Multipath.storyboard */, AB84741C2DF9CD9300B4DB73 /* en */, + ABB90ACA2DF728F8007FDDEB /* zh-Hans */, ); name = Multipath.storyboard; sourceTree = ""; @@ -1404,6 +1407,7 @@ children = ( AB8474132DF9CD7900B4DB73 /* Simulcast.storyboard */, AB84741E2DF9CD9B00B4DB73 /* en */, + ABB90ACC2DF7293C007FDDEB /* zh-Hans */, ); name = Simulcast.storyboard; sourceTree = ""; @@ -1628,7 +1632,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-oc"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1670,7 +1674,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-oc"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1703,7 +1707,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-oc.Agora-ScreenShare-Extension-OC"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1732,7 +1736,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-oc.Agora-ScreenShare-Extension-OC"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 7a6635018..6ab9e2105 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -1,66 +1,65 @@ +/* Element = "label"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "混音播放音量"; -/* Class = "UILabel"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ -"07c-He-s8j.text" = "Mixing Playback Volume"; +/* Element = "button"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ +"1zo-J9-vQy.normalTitle" = "暂停"; -/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ -"1zo-J9-vQy.normalTitle" = "Pause"; +/* Element = "label"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ +"4Y1-AZ-KwW.text" = "混音控制"; -/* Class = "UILabel"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ -"4Y1-AZ-KwW.text" = "Audio Mixing Controls"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ +"54l-lw-iap.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ -"54l-lw-iap.normalTitle" = "Stop"; +/* Element = "label"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ +"5o8-Cv-WLg.text" = "音效控制"; -/* Class = "UILabel"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ -"5o8-Cv-WLg.text" = "Audio Effect Controls"; +/* Element = "button"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ +"CRH-0X-9T4.normalTitle" = "恢复播放"; -/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ -"CRH-0X-9T4.normalTitle" = "Resume"; +/* Element = "label"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "混音音量"; -/* Class = "UILabel"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ -"DJt-Y7-fkM.text" = "Mixing Volume"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "button"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ +"J8R-TU-x8W.normalTitle" = "开始"; -/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ -"J8R-TU-x8W.normalTitle" = "Start"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; -/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ -"Q0E-5B-IED.text" = "Audio Scenario"; +/* Element = "label"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "混音发布音量"; -/* Class = "UILabel"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ -"VMe-lv-SUb.text" = "Mixing Publish Volume"; - -/* Class = "UILabel"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ +/* Element = "label"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ "cJ6-0Q-fAp.text" = "00 : 00"; -/* Class = "UILabel"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ -"e6E-so-zA5.text" = "Effect Volume"; +/* Element = "label"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ +"e6E-so-zA5.text" = "音效音量"; -/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ -"iUn-XK-AS2.text" = "Audio Profile"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频属性配置"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ -"jRA-VE-1PM.normalTitle" = "Resume"; +/* Element = "button"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ +"jRA-VE-1PM.normalTitle" = "恢复播放"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ -"jxp-ZN-2yG.title" = "Join Channel Audio"; +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ -"m2n-wi-5Xx.normalTitle" = "Play"; +/* Element = "button"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ +"m2n-wi-5Xx.normalTitle" = "播放"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ -"nzY-OP-Heo.normalTitle" = "Stop"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ +"nzY-OP-Heo.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ -"u26-Qh-itu.normalTitle" = "Pause"; +/* Element = "button"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ +"u26-Qh-itu.normalTitle" = "暂停"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings index 56ea7e36b..854a33f37 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings @@ -1,9 +1,8 @@ +/* Element = "label"; text = "Please obtain the pornographic identification results through the console webhook."; ObjectID = "9PU-6P-kJP"; */ +"9PU-6P-kJP.text" = "请通过控制台webhook获取鉴黄结果"; -/* Class = "UILabel"; text = "You can use the camera point to picture and check content inspect effect"; ObjectID = "9PU-6P-kJP"; */ -"9PU-6P-kJP.text" = "Please get content moderation results through console webhook"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "Nsf-MZ-fEg"; */ +"Nsf-MZ-fEg.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "Nsf-MZ-fEg"; */ -"Nsf-MZ-fEg.normalTitle" = "Join Channel"; - -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "PmB-t9-yOK"; */ -"PmB-t9-yOK.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "PmB-t9-yOK"; */ +"PmB-t9-yOK.placeholder" = "输入频道名"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings index 78350d5c9..d3b2f1bd4 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings @@ -1,48 +1,53 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "label"; text = "Ultra Low Latency"; ObjectID = "Lzz-2R-G7f"; */ +"Lzz-2R-G7f.text" = "极速直播"; -/* Class = "UILabel"; text = "Ultra Low Latency"; ObjectID = "Lzz-2R-G7f"; */ -"Lzz-2R-G7f.text" = "Ultra Low Latency"; +/* Element = "button"; configuration.title = "default color"; ObjectID = "Q0N-nV-bez"; */ +"Q0N-nV-bez.configuration.title" = "默认颜色"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "Q0N-nV-bez"; */ -"Q0N-nV-bez.normalTitle" = "Default Background Color"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "Q0N-nV-bez"; */ +"Q0N-nV-bez.normalTitle" = "默认背景色"; -/* Class = "UIButton"; configuration.title = "preload channel"; ObjectID = "S19-UR-C2c"; */ -"S19-UR-C2c.configuration.title" = "Preload"; +/* Element = "button"; configuration.title = "preload channel"; ObjectID = "S19-UR-C2c"; */ +"S19-UR-C2c.configuration.title" = "预加载"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "S19-UR-C2c"; */ -"S19-UR-C2c.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "S19-UR-C2c"; */ +"S19-UR-C2c.normalTitle" = "默认背景色"; -/* Class = "UILabel"; text = "Co-host"; ObjectID = "XcJ-am-UAb"; */ -"XcJ-am-UAb.text" = "Co-host"; +/* Element = "label"; text = "Co-host"; ObjectID = "XcJ-am-UAb"; */ +"XcJ-am-UAb.text" = "连麦"; -/* Class = "UILabel"; text = "Dual Stream"; ObjectID = "auZ-cF-RlC"; */ -"auZ-cF-RlC.text" = "Dual Stream"; +/* Element = "label"; text = "Large and small flow"; ObjectID = "auZ-cF-RlC"; */ +"auZ-cF-RlC.text" = "大小流"; -/* Class = "UISegmentedControl"; b7R-QN-CCh.segmentTitles[0] = "Auto"; ObjectID = "b7R-QN-CCh"; */ -"b7R-QN-CCh.segmentTitles[0]" = "Auto"; +/* Element = "segmentedControl"; segmentTitles[n] = "auto"; ObjectID = "b7R-QN-CCh"; */ +"b7R-QN-CCh.segmentTitles[0]" = "自动"; -/* Class = "UISegmentedControl"; b7R-QN-CCh.segmentTitles[1] = "Software Encoding"; ObjectID = "b7R-QN-CCh"; */ -"b7R-QN-CCh.segmentTitles[1]" = "Software Encoding"; +/* Element = "segmentedControl"; segmentTitles[n] = "soft"; ObjectID = "b7R-QN-CCh"; */ +"b7R-QN-CCh.segmentTitles[1]" = "软编"; -/* Class = "UISegmentedControl"; b7R-QN-CCh.segmentTitles[2] = "Hardware Encoding"; ObjectID = "b7R-QN-CCh"; */ -"b7R-QN-CCh.segmentTitles[2]" = "Hardware Encoding"; +/* Element = "segmentedControl"; segmentTitles[n] = "hard"; ObjectID = "b7R-QN-CCh"; */ +"b7R-QN-CCh.segmentTitles[2]" = "硬编"; -/* Class = "UILabel"; text = "(Default: High Stream)"; ObjectID = "fim-uQ-jK3"; */ -"fim-uQ-jK3.text" = "(Default: High Stream)"; +/* Element = "label"; text = "(Default: Mainstream)"; ObjectID = "fim-uQ-jK3"; */ +"fim-uQ-jK3.text" = "(默认:大流)"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UILabel"; text = "B Frame"; ObjectID = "m3M-Tw-nzd"; */ -"m3M-Tw-nzd.text" = "B Frame"; +/* Element = "label"; text = "B frame"; ObjectID = "m3M-Tw-nzd"; */ +"m3M-Tw-nzd.text" = "B 帧"; -/* Class = "UILabel"; text = "Watermark"; ObjectID = "nSq-Vu-o9H"; */ -"nSq-Vu-o9H.text" = "Watermark"; +/* Element = "label"; text = "watermark"; ObjectID = "nSq-Vu-o9H"; */ +"nSq-Vu-o9H.text" = "水印"; -/* Class = "UILabel"; text = "First Frame Optimization"; ObjectID = "ohV-am-Acd"; */ -"ohV-am-Acd.text" = "First Frame Optimization"; +/* Element = "label"; text = "First Frame Optimization"; ObjectID = "ohV-am-Acd"; */ +"ohV-am-Acd.text" = "首帧出图"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "w4q-aT-JBc"; */ -"w4q-aT-JBc.normalTitle" = "Screenshot"; +/* Element = "button"; configuration.title = "screenshot"; ObjectID = "w4q-aT-JBc"; */ +"w4q-aT-JBc.configuration.title" = "截图"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "w4q-aT-JBc"; */ +"w4q-aT-JBc.normalTitle" = "默认背景色"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings index a70e7394a..d61008d3c 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings @@ -1,27 +1,38 @@ +/* Element = "textField"; placeholder = "Enter target relay channel name"; ObjectID = "3YW-GH-fI0"; */ +"3YW-GH-fI0.placeholder" = "输入流转发目标频道名"; -/* Class = "UITextField"; placeholder = "Enter target relay channel name"; ObjectID = "3YW-GH-fI0"; */ -"3YW-GH-fI0.placeholder" = "Enter target relay channel name"; +/* Element = "label"; text = "Send stream to another channel"; ObjectID = "Dpm-6U-Jeq"; */ +"Dpm-6U-Jeq.text" = "发送流到另一个频道"; -/* Class = "UILabel"; text = "Send stream to another channel"; ObjectID = "Dpm-6U-Jeq"; */ -"Dpm-6U-Jeq.text" = "Send stream to another channel"; +/* Element = "button"; configuration.title = "Pause"; ObjectID = "G86-Jn-fzE"; */ +"G86-Jn-fzE.configuration.title" = "暂停"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "G86-Jn-fzE"; */ -"G86-Jn-fzE.normalTitle" = "Pause"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "G86-Jn-fzE"; */ +"G86-Jn-fzE.normalTitle" = "暂停"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "K2D-T9-l6m"; */ -"K2D-T9-l6m.normalTitle" = "Relay"; +/* Element = "button"; configuration.title = "Relay"; ObjectID = "K2D-T9-l6m"; */ +"K2D-T9-l6m.configuration.title" = "转发"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "NsB-pv-Uey"; */ -"NsB-pv-Uey.normalTitle" = "Resume"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "K2D-T9-l6m"; */ +"K2D-T9-l6m.normalTitle" = "暂停"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; configuration.title = "Resume"; ObjectID = "NsB-pv-Uey"; */ +"NsB-pv-Uey.configuration.title" = "恢复"; -/* Class = "UIViewController"; title = "Media Channel Relay"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "Media Channel Relay"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "NsB-pv-Uey"; */ +"NsB-pv-Uey.normalTitle" = "暂停"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "rua-c8-U0p"; */ -"rua-c8-U0p.normalTitle" = "Stop"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Element = "viewController"; title = "Media Channel Relay"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "跨频道流转发"; + +/* Element = "button"; configuration.title = "Stop"; ObjectID = "rua-c8-U0p"; */ +"rua-c8-U0p.configuration.title" = "停止"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "rua-c8-U0p"; */ +"rua-c8-U0p.normalTitle" = "暂停"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings new file mode 100644 index 000000000..0f96ad50b --- /dev/null +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings @@ -0,0 +1,32 @@ +/* Element = "label"; text = "Mode"; ObjectID = "CQP-o3-nsl"; */ +"CQP-o3-nsl.text" = "模式"; + +/* Element = "segmentedControl"; segmentTitles[n] = "dynamic"; ObjectID = "CnA-Yb-o6A"; */ +"CnA-Yb-o6A.segmentTitles[0]" = "动态"; + +/* Element = "segmentedControl"; segmentTitles[n] = "duplicate"; ObjectID = "CnA-Yb-o6A"; */ +"CnA-Yb-o6A.segmentTitles[1]" = "复制"; + +/* Element = "label"; text = "Role"; ObjectID = "G2E-c3-kzq"; */ +"G2E-c3-kzq.text" = "角色"; + +/* Element = "label"; text = "Mode"; ObjectID = "Nh9-6c-VAa"; */ +"Nh9-6c-VAa.text" = "模式"; + +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "VTF-4P-n1I"; */ +"VTF-4P-n1I.placeholder" = "输入频道名称"; + +/* Element = "label"; text = "--"; ObjectID = "mcr-bl-bnc"; */ +"mcr-bl-bnc.text" = "--"; + +/* Element = "button"; normalTitle = "Join"; ObjectID = "sd1-uc-igv"; */ +"sd1-uc-igv.normalTitle" = "加入频道"; + +/* Element = "segmentedControl"; segmentTitles[n] = "broadcaster"; ObjectID = "vn7-ux-djR"; */ +"vn7-ux-djR.segmentTitles[0]" = "主播"; + +/* Element = "segmentedControl"; segmentTitles[n] = "audience"; ObjectID = "vn7-ux-djR"; */ +"vn7-ux-djR.segmentTitles[1]" = "观众"; + +/* Element = "label"; text = "Enable"; ObjectID = "yx2-9h-YtM"; */ +"yx2-9h-YtM.text" = "开/关"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings index 754a51f16..2f4ccf616 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings @@ -1,18 +1,20 @@ +/* Element = "button"; configuration.title = "Publish"; ObjectID = "EYp-Uv-8h9"; */ +"EYp-Uv-8h9.configuration.title" = "发布"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "EYp-Uv-8h9"; */ -"EYp-Uv-8h9.normalTitle" = "Push Stream"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "EYp-Uv-8h9"; */ +"EYp-Uv-8h9.normalTitle" = "推流"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UILabel"; text = "Transcoding"; ObjectID = "mdz-Cd-XO9"; */ -"mdz-Cd-XO9.text" = "Transcoding"; +/* Element = "label"; text = "Transcoding"; ObjectID = "mdz-Cd-XO9"; */ +"mdz-Cd-XO9.text" = "转码"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "Join Channel Video"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "视频实时通话"; -/* Class = "UITextField"; text = "rtmp://push.webdemo.agoraio.cn/lbhd/test"; ObjectID = "yNO-I8-w8H"; */ +/* Element = "textField"; text = "rtmp://push.webdemo.agoraio.cn/lbhd/test"; ObjectID = "yNO-I8-w8H"; */ "yNO-I8-w8H.text" = "rtmp://push.webdemo.agoraio.cn/lbhd/test"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings new file mode 100644 index 000000000..c623b8274 --- /dev/null +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings @@ -0,0 +1,41 @@ +/* Element = "label"; text = "Set Layers Config(Maxmum 3)"; ObjectID = "22s-iU-9VA"; */ +"22s-iU-9VA.text" = "配置多流(最多3个)"; + +/* Element = "segmentedControl"; segmentTitles[n] = "broadcaster"; ObjectID = "6b2-UN-Hug"; */ +"6b2-UN-Hug.segmentTitles[0]" = "主播"; + +/* Element = "segmentedControl"; segmentTitles[n] = "audience"; ObjectID = "6b2-UN-Hug"; */ +"6b2-UN-Hug.segmentTitles[1]" = "观众"; + +/* Element = "label"; text = "Layer2:540p15fps"; ObjectID = "E1y-tK-d1x"; */ +"E1y-tK-d1x.text" = "Layer2:540p15fps"; + +/* Element = "label"; text = "Layer4:270p15fps"; ObjectID = "E6v-QW-JID"; */ +"E6v-QW-JID.text" = "Layer4:270p15fps"; + +/* Element = "button"; normalTitle = "Join"; ObjectID = "No8-8e-RHg"; */ +"No8-8e-RHg.normalTitle" = "加入频道"; + +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "O71-Co-nqA"; */ +"O71-Co-nqA.placeholder" = "输入频道名称"; + +/* Element = "segmentedControl"; segmentTitles[n] = "layer1"; ObjectID = "Zza-Qr-9E6"; */ +"Zza-Qr-9E6.segmentTitles[0]" = "layer1"; + +/* Element = "segmentedControl"; segmentTitles[n] = "layer2"; ObjectID = "Zza-Qr-9E6"; */ +"Zza-Qr-9E6.segmentTitles[1]" = "layer2"; + +/* Element = "segmentedControl"; segmentTitles[n] = "layer3"; ObjectID = "Zza-Qr-9E6"; */ +"Zza-Qr-9E6.segmentTitles[2]" = "layer3"; + +/* Element = "segmentedControl"; segmentTitles[n] = "layer4"; ObjectID = "Zza-Qr-9E6"; */ +"Zza-Qr-9E6.segmentTitles[3]" = "layer4"; + +/* Element = "label"; text = "Layer1:720p30fps"; ObjectID = "e2t-fd-9mw"; */ +"e2t-fd-9mw.text" = "Layer1:720p30fps"; + +/* Element = "label"; text = "Role"; ObjectID = "pGL-vq-LWk"; */ +"pGL-vq-LWk.text" = "角色"; + +/* Element = "label"; text = "Layer3:360p15fps"; ObjectID = "s9a-8r-AFH"; */ +"s9a-8r-AFH.text" = "Layer3:360p15fps"; diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings index 0226186f8..6b7a89f0a 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings @@ -1,21 +1,20 @@ - -/* Class = "UIButton"; configuration.title = "Button"; ObjectID = "99i-oC-tHx"; */ +/* Element = "button"; configuration.title = "Button"; ObjectID = "99i-oC-tHx"; */ "99i-oC-tHx.configuration.title" = "Button"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "99i-oC-tHx"; */ -"99i-oC-tHx.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "99i-oC-tHx"; */ +"99i-oC-tHx.normalTitle" = "截图"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter encryption secret"; ObjectID = "gkN-x2-moD"; */ -"gkN-x2-moD.placeholder" = "Enter encryption secret"; +/* Element = "textField"; placeholder = "Enter encryption secret"; ObjectID = "gkN-x2-moD"; */ +"gkN-x2-moD.placeholder" = "加密密码"; -/* Class = "UILabel"; text = "Encryption Mode"; ObjectID = "hXZ-v4-LRg"; */ -"hXZ-v4-LRg.text" = "Encryption Mode"; +/* Element = "label"; text = "Encryption Mode"; ObjectID = "hXZ-v4-LRg"; */ +"hXZ-v4-LRg.text" = "加密方式"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIViewController"; title = "Stream Encryption"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "Stream Encryption"; +/* Element = "viewController"; title = "Stream Encryption"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "码流加密"; diff --git a/iOS/APIExample-OC/Podfile b/iOS/APIExample-OC/Podfile index d6eb3505e..7d45b16f5 100644 --- a/iOS/APIExample-OC/Podfile +++ b/iOS/APIExample-OC/Podfile @@ -2,7 +2,7 @@ # platform :ios, '9.0' def common_pods - pod 'AgoraRtcEngine_iOS', '4.6.2' + pod 'AgoraRtcEngine_iOS', '4.7.0' # pod 'sdk', :path => 'sdk.podspec' end diff --git a/iOS/APIExample-OC/README.zh.md b/iOS/APIExample-OC/README.zh.md new file mode 100644 index 000000000..235e48f6f --- /dev/null +++ b/iOS/APIExample-OC/README.zh.md @@ -0,0 +1,64 @@ +# API Example iOS + +*[English](README.md) | 中文* + +本项目提供一组使用 Objective-C 编写的 Agora RTC API 示例,帮助你了解各项 API 的使用方式。 + +## 问题描述 + +在 iOS 14.0 及以上版本中,应用首次使用 Agora RTC SDK 时,系统会请求访问本地网络设备: + +![](../pictures/ios_14_privacy.png) + +[解决方案](https://docs.agora.io/en/help/integration-issues/local_network_privacy) + +## 环境准备 + +- Xcode 13.0 或更高版本 +- iPhone 或 iPad 真机 +- 不支持 iOS 模拟器 + +## 快速开始 + +以下步骤说明如何安装依赖、配置项目并运行示例。 + +### 安装依赖 + +进入当前示例目录,然后安装 CocoaPods 依赖。CocoaPods 的安装方式请参考[官方指南](https://guides.cocoapods.org/using/getting-started.html)。 + +```bash +cd iOS/APIExample-OC +pod install +``` + +确认 `APIExample-OC.xcworkspace` 已成功生成。 + +### 获取 App ID + +1. 登录 [Agora Console](https://console.agora.io/) 并创建项目。 +2. 在项目详情页复制 App ID。 +3. 使用 Xcode 打开 `APIExample-OC.xcworkspace`。 +4. 编辑 `APIExample-OC/Common/KeyCenter.m`,填入 App ID: + + ```objective-c + static NSString * const APPID = @""; + static NSString * const Certificate = nil; + ``` + +如果项目未启用 App Certificate,请保持 `Certificate` 为 `nil`。App Certificate 属于敏感信息,生产环境应在服务端生成 Token,不应将证书写入客户端或提交到 Git。 + +连接 iPhone 或 iPad 真机后,即可在 Xcode 中构建并运行项目。 + +## 联系我们 + +- 常见问题请查看 [Agora FAQ](https://docs.agora.io/en/faq) +- 更多示例请查看 [AgoraIO](https://github.com/AgoraIO) +- 复杂场景示例请查看 [AgoraIO Use Cases](https://github.com/AgoraIO-usecase) +- 社区维护项目请查看 [AgoraIO Community](https://github.com/AgoraIO-Community) +- 完整 API 文档请查看 [Agora Documentation](https://docs.agora.io/en/) +- 集成问题可以在 [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) 提问 +- 示例问题请提交到 [API-Examples Issues](https://github.com/AgoraIO/API-Examples/issues) + +## 代码许可 + +本项目采用 MIT License。 diff --git a/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings index ab17a8e77..1bae82890 100644 --- a/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings @@ -6,166 +6,186 @@ Copyright © 2020 Agora Corp. All rights reserved. */ -"Join a channel (Token)" = "Join a channel (Token)"; -"Join a channel (Video)" = "Join a channel (Video)"; -"Join a channel (Audio)" = "Join a channel (Audio)"; -"Local or remote recording" = "Local or remote recording"; -"Live Streaming" = "Live Streaming"; -"RTMP Streaming" = "RTMP Streaming"; -"Media Injection" = "Media Injection"; -"Video Metadata" = "Video Metadata"; -"Voice Changer" = "Voice Changer"; -"Custom Audio Source" = "Custom Audio Source"; -"Custom Audio Source(PCM)" = "Custom Audio Source(PCM)"; -"Custom Audio Render" = "Custom Audio Render"; -"Custom Video Source" = "Custom Video Source"; -"Custom Video Source(Push)" = "Custom Video Source(Push)"; -"Custom Video Source(Multi)" = "Custom Video Source(Multi)"; -"Custom Video Render" = "Custom Video Render(Metal)"; -"Quick Switch Channel" = "Quick Switch Channel"; -"Join Multiple Channels" = "Join Multiple Channels"; -"Stream Encryption" = "Stream Encryption"; -"LocalCompositeGraph" = "LocalCompositeGraph"; -"Audio Mixing" = "Audio Mixing"; -"Raw Media Data" = "Raw Media Data"; -"Raw Video Data" = "Raw Video Data"; -"Simple Filter Extension" = "Simple Filter Extension"; -"Precall Test" = "Precall Test"; -"Media Player" = "Media Player"; -"Screen Share" = "Screen Share"; -"Super Resolution" = "Super Resolution"; -"Media Channel Relay" = "Media Channel Relay"; -"Set Resolution" = "Set Resolution"; -"Set Fps" = "Set Fps"; -"Set Orientation" = "Set Orientation"; -"Set Chat Beautifier" = "Set Chat Beautifier"; -"Set Timbre Transformation" = "Set Timbre Transformation"; -"Set Voice Changer" = "Set Voice Changer"; -"Set Style Transformation" = "Set Style Transformation"; -"Set Room Acoustics" = "Set Room Acoustics"; -"Set Band Frequency" = "Set Band Frequency"; -"Set Reverb Key" = "Set Reverb Key"; -"Set Encryption Mode" = "Set Encryption Mode"; -"fixed portrait" = "Fixed Portrait"; -"fixed landscape" = "Fixed Landscape"; -"adaptive" = "Adaptive"; -"Local Host" = "Local Host"; -"Remote Host" = "Remote Host"; -"Set Audio Profile" = "Set Audio Profile"; -"Set Audio Scenario" = "Set Audio Scenario"; -"Default" = "Default"; -"Music Standard" = "Music Standard"; -"Music Standard Stereo" = "Music Standard Stereo"; -"Music High Quality" = "Music High Quality"; -"Music High Quality Stereo" = "Music High Quality Stereo"; -"Speech Standard" = "Speech Standard"; -"Chat Room Gaming" = "Chat Room Gaming"; -"Education" = "Education"; -"Game Streaming" = "Game Streaming"; -"High Defination" = "High Definition"; -"Chat Room Entertainment" = "Chat Room Entertainment"; -"Show Room" = "Show Room"; -"Cancel" = "Cancel"; -"Sure" = "Confirm"; -"Stop" = "Stop"; -"Off" = "Original Sound"; -"FemaleFresh" = "Chat Beautifier: Fresh(Female)"; -"FemaleVitality" = "Chat Beautifier: Vitality(Female)"; -"MaleMagnetic" = "Chat Beautifier: Magnetic(Male)"; -"Vigorous" = "Vigorous"; -"Deep" = "Deep"; -"Mellow" = "Mellow"; -"Falsetto" = "Falsetto"; -"Full" = "Full"; -"Clear" = "Clear"; -"Resounding" = "Resounding"; -"Ringing" = "Ringing"; -"Spacial" = "Spacial"; -"Ethereal" = "Ethereal"; -"Old Man" = "Old Man"; -"Baby Boy" = "Baby Boy"; -"Baby Girl" = "Baby Girl"; -"ZhuBaJie" = "ZhuBaJie"; -"Hulk" = "Hulk"; -"FxUncle" = "Uncle"; -"FxSister" = "Sister"; -"Pop" = "Pop"; -"Pop(Old Version)" = "Pop(Old Version)"; +"Join a channel (Token)" = "实时视频通话/直播(Token验证)"; +"Join a channel (Video)" = "实时视频通话/直播"; +"Join a channel (Audio)" = "实时语音通话/直播"; +"Local or remote recording" = "本地/远端录制"; +"Live Streaming" = "RTC实时直播/主播/观众"; +"RTMP Streaming" = "RTMP旁路推流"; +"Video Metadata" = "SEI消息"; +"Voice Changer" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source" = "视频自采集"; +"Custom Video Source(Push)" = "视频自采集(Push)"; +"Custom Video Source(Multi)" = "多路视频自采集"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"LocalCompositeGraph" = "本地合图屏幕共享"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Precall Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Media Channel Relay" = "跨频道流转发"; +"Set Resolution" = "设置视频分辨率"; +"Set Fps" = "设置视频帧率"; +"Set Orientation" = "设置视频朝向"; +"Set Chat Beautifier" = "设置语聊美声"; +"Set Timbre Transformation" = "设置音色变换"; +"Set Voice Changer" = "设置变声音效"; +"Set Style Transformation" = "设置曲风音效"; +"Set Room Acoustics" = "设置空间音效"; +"Set Band Frequency" = "设置波段频率"; +"Set Reverb Key" = "设置混响属性"; +"Set Encryption Mode" = "设置加密模式"; +"fixed portrait" = "固定纵向"; +"fixed landscape" = "固定横向"; +"adaptive" = "自适应"; +"Local Host" = "本地预览"; +"Remote Host" = "远端视频"; +"Set Audio Profile" = "设置音频参数配置"; +"Set Audio Scenario" = "设置音频使用场景"; +"Default" = "默认"; +"Music Standard" = "标准音乐"; +"Music Standard Stereo" = "标准双声道音乐"; +"Music High Quality" = "高音质音乐"; +"Music High Quality Stereo" = "高音质双声道音乐"; +"Speech Standard" = "标准人声"; +"Chat Room Gaming" = "娱乐语聊房"; +"Education" = "教育"; +"Game Streaming" = "高音质语聊房"; +"High Defination" = "高音质音乐"; +"Chat Room Entertainment" = "游戏开黑"; +"Show Room" = "秀场"; +"Cancel" = "取消"; +"Sure" = "确定"; +"Stop" = "停止"; +"Off" = "原声"; +"FemaleFresh" = "语聊美声: 清新(女)"; +"FemaleVitality" = "语聊美声: 活力(女)"; +"MaleMagnetic" = "语聊美声: 磁性(男)"; +"Vigorous" = "浑厚"; +"Deep" = "低沉"; +"Mellow" = "圆润"; +"Falsetto" = "假音"; +"Full" = "饱满"; +"Clear" = "清澈"; +"Resounding" = "高亢"; +"Ringing" = "嘹亮"; +"Spacial" = "空旷"; +"Ethereal" = "空灵"; +"Old Man" = "老男孩"; +"Baby Boy" = "小男孩"; +"Baby Girl" = "小女孩"; +"ZhuBaJie" = "猪八戒"; +"Hulk" = "绿巨人"; +"FxUncle" = "大叔"; +"FxSister" = "小姐姐"; +"Pop" = "流行"; +"Pop(Old Version)" = "流行(旧版)"; "R&B" = "R&B"; -"R&B(Old Version)" = "R&B(Old Version)"; -"Rock" = "Rock"; -"HipHop" = "HipHop"; -"Vocal Concert" = "Vocal Concert"; -"Vocal Concert(Old Version)" = "Vocal Concert(Old Version)"; +"R&B(Old Version)" = "R&B(旧版)"; +"Rock" = "摇滚"; +"HipHop" = "嘻哈"; +"Vocal Concert" = "演唱会"; +"Vocal Concert(Old Version)" = "演唱会(旧版)"; "KTV" = "KTV"; -"KTV(Old Version)" = "KTV(Old Version)"; -"Studio" = "Studio"; -"Studio(Old Version)" = "Studio(Old Version)"; -"Phonograph" = "Phonograph"; -"Virtual Stereo" = "Virtual Stereo"; -"Dry Level" = "Dry Level"; -"Wet Level" = "Early Reflection Signal Strength"; -"Room Size" = "Room Size"; -"Wet Delay" = "Early Reflection Signal Delay"; -"Strength" = "Reverb Sustain Strength"; -"ARKit is not available on this device." = "ARKit is not available on this device."; -"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "This app requires world tracking, which is available only on iOS devices with the A9 processor or later."; -"Move Camera to find a planar\n(Shown as Red Rectangle)" = "Move Camera to find a planar\n(Shown as Red Rectangle)"; -"Tap to place remote video canvas" = "Tap to place remote video canvas"; -"Resolution" = "Resolution"; -"Frame Rate" = "Frame Rate"; -"Orientation" = "Orientation"; -"Broadcaster" = "Broadcaster"; -"Audience" = "Audience"; -"Pick Role" = "Pick Role"; -"Create Data Stream" = "Create Data Stream"; -"Raw Audio Data" = "Raw Audio Data"; -"Group Video Chat" = "Group Video Chat"; -"Screen Share + Camera" = "Screen Share + Camera"; -"please input channel name!" = "Please input channel name!"; -"Video Process" = "Video Process"; -"Rhythm Player" = "Rhythm Player"; -"Spatial Audio" = "Spatial Audio"; -"Please move the red icon to experience the 3D audio effect" = "Please move the red icon to experience the 3D audio effect"; -"Content Inspect" = "Content Inspect"; -"Video Neutral" = "Video Neutral"; -"Video Sexy" = "Video Sexy"; -"Video Porn" = "Video Porn"; -"SwitchCamera" = "Switch Camera"; -"remote user has not joined, and cannot take a screenshot" = "Remote user has not joined, cannot take a screenshot"; -"Screenshot successful" = "Screenshot successful"; -"Picture In Picture(require iOS15)" = "Picture In Picture(require iOS15)"; -"please input Token!" = "Please input Token!"; -"please input AppId!" = "Please input AppId!"; -"stopMicrophoneRecording" = "Stop Microphone Recording"; -"Ktv copyright music" = "KTV Copyright Music"; -"Mutli Camera(iOS13.0+)" = "Multi Camera(iOS13.0+)"; -"Multipath" = "Multipath"; -"Simulcast" = "Simulcast"; -"Open Rear Camera" = "Open Rear Camera"; -"Close Rear Camera" = "Close Rear Camera"; -"Third Beautify" = "Third Party Beautification"; -"Sense Beautify" = "SenseTime Beautification"; -"FU Beautify" = "FaceUnity Beautification"; -"Byted Effect" = "ByteDance Beautification"; -"license authorization failed, please check whether the license file is correct" = "License authorization failed, please check if the license file is correct"; -"Quick input APPID and Token methods" = "Quick input APPID and Token methods"; -"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n II: use https://cl1p.net/ online clipboard:\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2.Paste in anything you want.\n\n3.On another computer enter the same URL and get your stuff." = "I: If the phone and Mac are logged into the same Apple account, after copying on Mac it will automatically sync to other terminals with the same account. The phone can directly click the input box to paste.\n\n II: Use https://cl1p.net/ online clipboard:\n\n1.Enter a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2.Paste in anything you want.\n\n3.On another computer enter the same URL and get your stuff."; +"KTV(Old Version)" = "KTV(旧版)"; +"Studio" = "录音棚"; +"Studio(Old Version)" = "录音棚(旧版)"; +"Phonograph" = "留声机"; +"Virtual Stereo" = "虚拟立体声"; +"Dry Level" = "原始声音强度"; +"Wet Level" = "早期反射信号强度"; +"Room Size" = "房间尺寸"; +"Wet Delay" = "早期反射信号延迟"; +"Strength" = "混响持续强度"; +"ARKit is not available on this device." = "当前设备不支持ARKit"; +"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "AR功能仅在内置A9处理器后的iOS机型支持"; +"Move Camera to find a planar\n(Shown as Red Rectangle)" = "移动相机以找到一个平面\n(以红色方块显示)"; +"Tap to place remote video canvas" = "点击屏幕以放置视频画布"; +"Resolution" = "分辨率"; +"Frame Rate" = "帧率"; +"Orientation" = "视频朝向"; +"Broadcaster" = "主播"; +"Audience" = "观众"; +"Pick Role" = "选择角色"; +"Create Data Stream" = "创建数据流"; +"Raw Audio Data" = "音频裸数据"; +"Screen Share + Camera" = "屏幕共享 + 摄像头"; +"please input channel name!" = "请输入频道名!"; +"Video Process" = "视频增强"; +"Rhythm Player" = "虚拟节拍器"; +"Spatial Audio" = "空间音效"; +"Please move the red icon to experience the 3D audio effect" = "请移动红色图标体验3D音频效果"; +"Content Inspect" = "视频鉴黄"; +"Video Neutral" = "视频正常"; +"Video Sexy" = "视频性感"; +"Video Porn" = "视频色情"; +"SwitchCamera" = "切换摄像头"; +"remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; +"Screenshot successful" = "截图成功"; +"Picture In Picture(require iOS15)" = "画中画(iOS15支持)"; +"please input Token!" = "请输入Token"; +"please input AppId!" = "请输入APPID"; +"stopMicrophoneRecording" = "停止麦克风采集"; +"Ktv copyright music" = "KTV版权音乐"; +"Mutli Camera(iOS13.0+)" = "多路摄像头(iOS13.0+)"; +"Multipath" = "网络传输高级功能"; +"Simulcast" = "同源多流"; +"Open Rear Camera" = "打开后摄像头"; +"Close Rear Camera" = "关闭后摄像头"; +"license authorization failed, please check whether the license file is correct" = "license授权失败, 请检查license文件是否正确"; +"Quick input APPID and Token methods" = "快捷输入APPID和Token方法"; +"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n II: use https://cl1p.net/ online clipboard:\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2.Paste in anything you want.\n\n3.On another computer enter the same URL and get your stuff." = "I: 如果手机和Mac登录的同一个Apple账号, Mac复制后会自动同步到其它相同账号终端, 手机可直接点击输入框粘贴即可.\n\n II: 使用https://cl1p.net/在线剪切板:\n\n1.输入以cl1p.net开头的URL。示例cl1p.net/uqztgjnqcalmd。\n\n2.粘贴任何你想要的东西。\n\n3.在另一台计算机上,输入相同的URL并获取您的东西。"; -"After this function is enabled, it cannot be disabled and takes effect only when both the primary and secondary ends are enabled" = "After this function is enabled, it cannot be disabled and takes effect only when both the primary and secondary ends are enabled"; -"ARKit is not available on this device." = "ARKit is not available on this device."; -"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "This app requires world tracking, which is available only on iOS devices with the A9 processor or later."; -"Move Camera to find a planar\n(Shown as Red Rectangle)" = "Move Camera to find a planar\n(Shown as Red Rectangle)"; -"Tap to place remote video canvas" = "Tap to place remote video canvas"; -"Recording" = "Start Recording"; -"Stop Recording" = "Stop Recording"; -"PIP Support iOS 15+" = "PIP Support iOS 15+"; -"Red" = "Red"; -"Blue" = "Blue"; -"Pink" = "Pink"; -"Purple" = "Purple"; -"Yellow" = "Yellow"; -"preload Channel" = "Preload Channel"; -"cancel preload" = "Cancel Preload"; -"push" = "Push"; -"stop" = "Stop"; +"After this function is enabled, it cannot be disabled and takes effect only when both the primary and secondary ends are enabled" = "开启后无法关闭,并且需要主辅两端都开启时才生效"; +"Recording" = "开始录制"; +"Stop Recording" = "停止录制"; +"PIP Support iOS 15+" = "画中画只支持iOS15及以上版本"; +"Red" = "红色"; +"Blue" = "蓝色"; +"Pink" = "粉色"; +"Purple" = "紫色"; +"Yellow" = "黄色"; +"preload Channel" = "预加载"; +"cancel preload" = "取消预加载"; +"push" = "推流"; +"stop" = "停止"; + +"3D Voice" = "3D 人声"; +"Pitch Correction" = "音高校正"; +"Neutral" = "中性"; +"Sweet" = "甜美"; +"Solid" = "浑厚"; +"Bass" = "低沉"; +"Mute" = "静音"; +"Voice Blur" = "人声模糊"; +"Airborne Simulation" = "空气传播模拟"; +"Attenuation" = "衰减"; +"Set Pitch Correction" = "设置音高校正"; +"Set Voice Conversion" = "设置音色转换"; +"Default: high stream" = "默认:高码率流"; +"Dual stream enabled" = "已启用双流"; +"Ensure that you enable the RTMP Converter service at Agora Dashboard before using this function." = "使用此功能前,请确认已在 Agora 控制台启用 RTMP Converter 服务。"; +"Maxmum 3 layers can be selected" = "最多可选择 3 层"; +"No Player Loaded" = "未加载播放器"; +"No remote user" = "暂无远端用户"; +"Second Camera" = "第二路摄像头"; +"AES128GCM" = "AES128GCM"; +"AES256GCM" = "AES256GCM"; +"31Hz" = "31Hz"; +"62Hz" = "62Hz"; +"125Hz" = "125Hz"; +"250Hz" = "250Hz"; +"500Hz" = "500Hz"; +"1kHz" = "1kHz"; +"2kHz" = "2kHz"; +"4kHz" = "4kHz"; +"8kHz" = "8kHz"; +"16kHz" = "16kHz"; diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj index 29719b49b..27955f88e 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj @@ -175,7 +175,8 @@ 4C9306A02CB961150085EFF9 /* agora-logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "agora-logo.png"; sourceTree = ""; }; 4C9306A12CB961150085EFF9 /* sample.yuv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sample.yuv; sourceTree = ""; }; 4C9306A22CB961150085EFF9 /* frames0.yuv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = frames0.yuv; sourceTree = ""; }; - 4C9306A32CB961150085EFF9 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + C0DE47012CB961150085EFF9 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + C0DE47022CB961150085EFF9 /* zh-Hans */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 4C9306A42CB961150085EFF9 /* AR.scnassets */ = {isa = PBXFileReference; lastKnownFileType = wrapper.scnassets; path = AR.scnassets; sourceTree = ""; }; 4C9306A52CB961150085EFF9 /* audiomixing.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = audiomixing.mp3; sourceTree = ""; }; 4C9306A62CB961150085EFF9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -1014,6 +1015,7 @@ knownRegions = ( en, Base, + "zh-Hans", ); mainGroup = 4C9306802CB9607E0085EFF9; productRefGroup = 4C93068A2CB9607E0085EFF9 /* Products */; @@ -1262,6 +1264,18 @@ }; /* End PBXTargetDependency section */ +/* Begin PBXVariantGroup section */ + 4C9306A32CB961150085EFF9 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + C0DE47012CB961150085EFF9 /* en */, + C0DE47022CB961150085EFF9 /* zh-Hans */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin XCBuildConfiguration section */ 4C9306952CB9607F0085EFF9 /* Debug */ = { isa = XCBuildConfiguration; @@ -1405,7 +1419,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-swiftUI"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1444,7 +1458,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-swiftUI"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1474,7 +1488,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-swiftUI.Agora-ScreenShare-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1505,7 +1519,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples-swiftUI.Agora-ScreenShare-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings similarity index 87% rename from iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/Localizable.strings rename to iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings index 586b06e8c..4312635ce 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/Localizable.strings +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings @@ -1,10 +1,11 @@ -/* - Localization.strings - APIExample +/* + Localizable.strings + APIExample-SwiftUI - Created by 张乾泽 on 2020/10/7. - Copyright © 2020 Agora Corp. All rights reserved. + English keys follow the current SwiftUI source. Simplified Chinese values + are maintained in the parallel zh-Hans localization. */ + "Join" = "Join Channel"; "anti shake" = "Anti-shake"; "Enter channel name" = "Enter channel name"; @@ -16,7 +17,6 @@ "Live Streaming" = "RTC Live Streaming/Host/Audience"; "RTMP Streaming" = "RTMP Streaming"; "Transcoding" = "Transcoding"; -"Media Injection" = "Media Injection"; "Video Metadata" = "SEI Message"; "Voice Changer" = "Voice Effects/Beautifier"; "Custom Audio Source" = "Custom Audio Source"; @@ -31,13 +31,11 @@ "Join Multiple Channels" = "Join Multiple Channels"; "Stream Encryption" = "Media Stream Encryption"; "Audio Mixing" = "Audio File Mixing"; -"Raw Media Data" = "Raw Audio/Video Data"; "Raw Video Data" = "Raw Video Data"; "Simple Filter Extension" = "Plugin System"; "Precall Test" = "Pre-call Network/Device Test"; "Media Player" = "Media Player"; "Screen Share" = "Screen Share"; -"Super Resolution" = "Super Resolution"; "Media Channel Relay" = "Cross Channel Stream Relay"; "Set Resolution" = "Set Video Resolution"; "Set Fps" = "Set Video Frame Rate"; @@ -112,8 +110,8 @@ "Wet Level" = "Early Reflection Level"; "Room Size" = "Room Size"; "Wet Delay" = "Early Reflection Delay"; -"Strength" = "Reverb Intensity"; -"ARKit is not available on this device." = "ARKit is not available on this device."; +"Strength" = "Strength"; +"ARKit is not available on this device." = "ARKit is not available on this device"; "This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "AR features are only supported on iOS devices with A9 processor or later"; "Move Camera to find a planar\n(Shown as Red Rectangle)" = "Move camera to find a plane\n(Shown as red rectangle)"; "Tap to place remote video canvas" = "Tap screen to place video canvas"; @@ -125,7 +123,6 @@ "Pick Role" = "Select Role"; "Create Data Stream" = "Create Data Stream"; "Raw Audio Data" = "Raw Audio Data"; -"Group Video Chat" = "Group Video Call"; "Screen Share + Camera" = "Screen Share + Camera"; "please input channel name!" = "Please enter channel name!"; "Video Process" = "Video Enhancement"; @@ -149,19 +146,10 @@ "Mutli Camera(iOS13.0+)" = "Multi Camera (iOS13.0+)"; "Open Rear Camera" = "Open Rear Camera"; "Close Rear Camera" = "Close Rear Camera"; -"Third Beautify" = "Third-party Beautification"; -"Sense Beautify" = "SenseTime Beautification"; -"FU Beautify" = "FaceUnity Beautification"; -"Byted Effect" = "ByteDance Beautification"; "license authorization failed, please check whether the license file is correct" = "License authorization failed, please check if license file is correct"; "Quick input APPID and Token methods" = "Quick methods to input APPID and Token"; -"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n II: use https://cl1p.net/ online clipboard:\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n2.Paste in anything you want.\n\n3.On another computer enter the same URL and get your stuff." = "I: If phone and Mac are logged into same Apple ID, Mac copies will auto-sync to other devices with same account. Phone can directly paste by tapping input box.\n\n II: Use https://cl1p.net/ online clipboard:\n\n1. Enter URL starting with cl1p.net. Example: cl1p.net/uqztgjnqcalmd\n\n2. Paste anything you want.\n\n3. On another device, enter same URL to get your content."; - +"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n\nII: use https://cl1p.net/ online clipboard:\n\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n\n2.Paste in anything you want.\n\n\n3.On another computer enter the same URL and get your stuff." = "I: If phone and Mac are logged into same Apple ID, Mac copies will auto-sync to other devices with same account. Phone can directly paste by tapping input box.\n\nII: Use https://cl1p.net/ online clipboard:\n\n1. Enter URL starting with cl1p.net. Example: cl1p.net/uqztgjnqcalmd\n\n2. Paste anything you want.\n\n3. On another device, enter same URL to get your content."; "After this function is enabled, it cannot be disabled and takes effect only when both the primary and secondary ends are enabled" = "Once enabled, this cannot be disabled and only takes effect when enabled on both primary and secondary ends"; -"ARKit is not available on this device." = "ARKit is not available on this device"; -"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "AR features are only supported on iOS devices with A9 processor or later"; -"Move Camera to find a planar\n(Shown as Red Rectangle)" = "Move camera to find a plane\n(Shown as red rectangle)"; -"Tap to place remote video canvas" = "Tap screen to place video canvas"; "Recording" = "Start Recording"; "Stop Recording" = "Stop Recording"; "PIP Support iOS 15+" = "Picture In Picture requires iOS 15+"; @@ -175,9 +163,7 @@ "Audio Router(Third Party Player)" = "Audio Router (Third Party Player)"; "Player Type(ijkplayer/avplayer)" = "Player Selection (ijkplayer/native)"; "Audio Waveform" = "Audio Spectrum"; - "Set AINS Mode" = "Set AI Noise Suppression mode"; - "AGGRESSIVE" = "Aggressive Mode"; "BALANCED" = "(Default) Balanced Mode"; "ULTRALOWLATENCY" = "Ultra Low Latency Mode"; @@ -213,7 +199,6 @@ "Relay" = "Relay"; "Pause" = "Pause"; "Resume" = "Resume"; -"Stop" = "Stop"; "Play" = "Play"; "software" = "Software"; "hardware" = "Hardware"; @@ -232,7 +217,6 @@ "Low light Enhancement" = "Low Light Enhancement"; "Video Dencise" = "Video Denoising"; "Color Enhancement" = "Color Enhancement"; -"Strength" = "Strength"; "Skin Protect" = "Skin Protection"; "Virtual Background" = "Virtual Background"; "Open" = "Open"; @@ -277,3 +261,23 @@ "Set Layers Config(Maximum 3)" = "Set Layers Config(Maximum 3)"; "Maxmum 3 layers can be selected" = "Maxmum 3 layers can be selected"; "LocalCompositeGraph" = "LocalCompositeGraph"; +"3D Voice" = "3D Voice"; +"Neutral" = "Neutral"; +"Sweet" = "Sweet"; +"Solid" = "Solid"; +"Bass" = "Bass"; +"ARKit" = "ARKit"; +"Simulcast" = "Simulcast"; +"Multipath" = "Multipath"; +"The camera has changed" = "The camera has changed"; +"Role" = "Role"; +"Mode" = "Mode"; +"Customize Voice Effects" = "Customize Voice Effects"; +"Pitch" = "Pitch"; +"BandFreq" = "Band Frequency"; +"BandGain" = "Band Gain"; +"ReverbKey" = "Reverb Key"; +"ReverbValue" = "Reverb Value"; +"AINSModel" = "AINS Mode"; +"Please the input APPID" = "Please enter App ID"; +"Please the input Token" = "Please enter Token"; diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 000000000..e49ca37ad --- /dev/null +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,283 @@ +/* + Localizable.strings + APIExample-SwiftUI + + English keys follow the current SwiftUI source. Simplified Chinese values + are maintained in the parallel zh-Hans localization. +*/ + +"Join" = "加入频道"; +"anti shake" = "防抖"; +"Enter channel name" = "输入频道名"; +"Default background Color" = "默认背景色"; +"Join a channel (Token)" = "实时视频通话/直播(Token验证)"; +"Join a channel (Video)" = "实时视频通话/直播"; +"Join a channel (Audio)" = "实时语音通话/直播"; +"Local or remote recording" = "本地/远端录制"; +"Live Streaming" = "RTC实时直播/主播/观众"; +"RTMP Streaming" = "RTMP旁路推流"; +"Transcoding" = "转码"; +"Video Metadata" = "SEI消息"; +"Voice Changer" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source" = "视频自采集"; +"Custom Video Source(Push)" = "视频自采集(Push)"; +"Custom Video Source(Multi)" = "多路视频自采集"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Picture In Picture" = "画中画(iOS 15支持)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Precall Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Media Channel Relay" = "跨频道流转发"; +"Set Resolution" = "设置视频分辨率"; +"Set Fps" = "设置视频帧率"; +"B Fps" = "B帧"; +"Set Orientation" = "设置视频朝向"; +"Set Chat Beautifier" = "设置语聊美声"; +"Set Timbre Transformation" = "设置音色变换"; +"Set Voice Changer" = "设置变声音效"; +"Set Style Transformation" = "设置曲风音效"; +"Set Room Acoustics" = "设置空间音效"; +"Set Band Frequency" = "设置波段频率"; +"Set Reverb Key" = "设置混响属性"; +"Set Encryption Mode" = "设置加密模式"; +"fixed portrait" = "固定纵向"; +"fixed landscape" = "固定横向"; +"adaptive" = "自适应"; +"Local Host" = "本地预览"; +"Remote Host" = "远端视频"; +"Set Audio Profile" = "设置音频参数配置"; +"Set Audio Scenario" = "设置音频使用场景"; +"Default" = "默认"; +"Music Standard" = "标准音乐"; +"Music Standard Stereo" = "标准双声道音乐"; +"Music High Quality" = "高音质音乐"; +"Music High Quality Stereo" = "高音质双声道音乐"; +"Speech Standard" = "标准人声"; +"Chat Room Gaming" = "娱乐语聊房"; +"Education" = "教育"; +"Game Streaming" = "高音质语聊房"; +"High Defination" = "高音质音乐"; +"Chat Room Entertainment" = "游戏开黑"; +"Show Room" = "秀场"; +"Cancel" = "取消"; +"Sure" = "确定"; +"Stop" = "停止"; +"Off" = "原声"; +"FemaleFresh" = "语聊美声: 清新(女)"; +"FemaleVitality" = "语聊美声: 活力(女)"; +"MaleMagnetic" = "语聊美声: 磁性(男)"; +"Vigorous" = "浑厚"; +"Deep" = "低沉"; +"Mellow" = "圆润"; +"Falsetto" = "假音"; +"Full" = "饱满"; +"Clear" = "清澈"; +"Resounding" = "高亢"; +"Ringing" = "嘹亮"; +"Spacial" = "空旷"; +"Ethereal" = "空灵"; +"Old Man" = "老男孩"; +"Baby Boy" = "小男孩"; +"Baby Girl" = "小女孩"; +"ZhuBaJie" = "猪八戒"; +"Hulk" = "绿巨人"; +"FxUncle" = "大叔"; +"FxSister" = "小姐姐"; +"Pop" = "流行"; +"Pop(Old Version)" = "流行(旧版)"; +"R&B" = "R&B"; +"R&B(Old Version)" = "R&B(旧版)"; +"Rock" = "摇滚"; +"HipHop" = "嘻哈"; +"Vocal Concert" = "演唱会"; +"Vocal Concert(Old Version)" = "演唱会(旧版)"; +"KTV" = "KTV"; +"KTV(Old Version)" = "KTV(旧版)"; +"Studio" = "录音棚"; +"Studio(Old Version)" = "录音棚(旧版)"; +"Phonograph" = "留声机"; +"Virtual Stereo" = "虚拟立体声"; +"Dry Level" = "原始声音强度"; +"Wet Level" = "早期反射信号强度"; +"Room Size" = "房间尺寸"; +"Wet Delay" = "早期反射信号延迟"; +"Strength" = "强度"; +"ARKit is not available on this device." = "当前设备不支持ARKit"; +"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "AR功能仅在内置A9处理器后的iOS机型支持"; +"Move Camera to find a planar\n(Shown as Red Rectangle)" = "移动相机以找到一个平面\n(以红色方块显示)"; +"Tap to place remote video canvas" = "点击屏幕以放置视频画布"; +"Resolution" = "分辨率"; +"Frame Rate" = "帧率"; +"Orientation" = "视频朝向"; +"Broadcaster" = "主播"; +"Audience" = "观众"; +"Pick Role" = "选择角色"; +"Create Data Stream" = "创建数据流"; +"Raw Audio Data" = "音频裸数据"; +"Screen Share + Camera" = "屏幕共享 + 摄像头"; +"please input channel name!" = "请输入频道名!"; +"Video Process" = "视频增强"; +"Agora Beauty" = "Agora 美颜"; +"Rhythm Player" = "虚拟节拍器"; +"Spatial Audio" = "空间音效"; +"Please move the red icon to experience the 3D audio effect" = "请移动红色图标体验3D音频效果"; +"Content Inspect" = "视频鉴黄"; +"Video Neutral" = "视频正常"; +"Video Sexy" = "视频性感"; +"Video Porn" = "视频色情"; +"SwitchCamera" = "切换摄像头"; +"screenshot" = "截图"; +"remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; +"Screenshot successful" = "截图成功"; +"Picture In Picture(require iOS15)" = "画中画(iOS15支持)"; +"please input Token!" = "请输入Token"; +"please input AppId!" = "请输入APPID"; +"stopMicrophoneRecording" = "停止麦克风采集"; +"Ktv copyright music" = "KTV版权音乐"; +"Mutli Camera(iOS13.0+)" = "多路摄像头(iOS13.0+)"; +"Open Rear Camera" = "打开后摄像头"; +"Close Rear Camera" = "关闭后摄像头"; +"license authorization failed, please check whether the license file is correct" = "license授权失败, 请检查license文件是否正确"; +"Quick input APPID and Token methods" = "快捷输入APPID和Token方法"; +"I: the mobile phone and Mac log in to the same Apple account. After copying the Mac, it will automatically synchronize other terminals with the same account. The mobile phone can directly click the input box to paste.\n\n\nII: use https://cl1p.net/ online clipboard:\n\n\n1.Enter in a URL that starts with cl1p.net. Example cl1p.net/uqztgjnqcalmd\n\n\n2.Paste in anything you want.\n\n\n3.On another computer enter the same URL and get your stuff." = "I: 如果手机和 Mac 登录同一个 Apple 账号,Mac 复制的内容会自动同步到使用该账号的其他设备,手机可直接在输入框中粘贴。\n\nII: 使用 https://cl1p.net/ 在线剪贴板:\n\n1. 输入一个以 cl1p.net 开头的网址,例如 cl1p.net/uqztgjnqcalmd。\n\n2. 粘贴需要传输的内容。\n\n3. 在另一台设备上打开同一网址即可获取内容。"; +"After this function is enabled, it cannot be disabled and takes effect only when both the primary and secondary ends are enabled" = "开启后无法关闭,并且需要主辅两端都开启时才生效"; +"Recording" = "开始录制"; +"Stop Recording" = "停止录制"; +"PIP Support iOS 15+" = "画中画只支持iOS15及以上版本"; +"Red" = "红色"; +"Blue" = "蓝色"; +"Pink" = "粉色"; +"Purple" = "紫色"; +"Yellow" = "黄色"; +"preload Channel" = "预加载"; +"cancel preload" = "取消预加载"; +"Audio Router(Third Party Player)" = "音频路由(第三方播放器)"; +"Player Type(ijkplayer/avplayer)" = "播放器选择(ijkplayer/原生)"; +"Audio Waveform" = "音频频谱"; +"Set AINS Mode" = "设置是否开启 AI 降噪功能并设置降噪模式。"; +"AGGRESSIVE" = "强降噪模式"; +"BALANCED" = "(默认)均衡降噪模式"; +"ULTRALOWLATENCY" = "低延时强降噪模式"; +"Face Capture" = "面捕数据"; +"Please contact Agora customer service to obtain a face capture certificate" = "请联系 Agora 客服获取面捕证书"; +"Audio Scenario" = "音频使用场景"; +"Audio Profile" = "音频参数配置"; +"RecordingVolume" = "采集音量"; +"PlaybackVolume" = "播放音量"; +"InEar Monitoring" = "耳返"; +"InEar Monitoring Volume" = "耳返音量"; +"First Frame Optimization" = "首帧出图"; +"Move Camera to find a planar \n(Shown as Red Rectangle)" = "移动相机以找到一个平面 \n(以红色方块显示)"; +"Second camera" = "第二路摄像头"; +"Please obtain the pornographic identification results through the console webhook." = "请通过控制台webhook获取鉴黄结果"; +"This device does not support Center Stage" = "此设备不支持 Center Stage"; +"Wide" = "广角"; +"Length Wide" = "超广角"; +"Telephoto" = "长焦"; +"Front camera" = "前置"; +"Rear camera" = "后置"; +"The camera has changed:" = "摄像头已改变:"; +"Move the red icon to experience the 3D audio effect" = "请移动红色图标体验3D音频效果"; +"Room" = "房间"; +"Sound barrier" = "音障"; +"Microphone" = "麦克风"; +"Mute" = "静音"; +"Send Message" = "发送消息"; +"Input message" = "输入消息"; +"Send" = "发送"; +"Send stream to another channel" = "发送流到另一个频道"; +"Enter target relay channel name" = "输入流转发目标频道名"; +"Relay" = "转发"; +"Pause" = "暂停"; +"Resume" = "恢复"; +"Play" = "播放"; +"software" = "软编"; +"hardware" = "硬编"; +"auto" = "自动"; +"Beats per Minute" = "每分钟节拍数"; +"Beats per Measure" = "每小节节拍数"; +"Image" = "图片"; +"Color" = "颜色"; +"Blur" = "毛玻璃"; +"Video" = "视频"; +"Face Beauty" = "美颜"; +"Lightening" = "美白"; +"Redness" = "红润"; +"Sharpness" = "锐利"; +"Smoothness" = "平滑"; +"Low light Enhancement" = "暗光增强"; +"Video Dencise" = "视频降噪"; +"Color Enhancement" = "色彩增强"; +"Skin Protect" = "肤色保护"; +"Virtual Background" = "虚拟背景"; +"Open" = "打开"; +"Publish" = "发流"; +"stopPublish" = "停止发流"; +"Publish Volume" = "发流音量"; +"play Track:" = "播放音轨:"; +"publish Track:" = "推送音轨:"; +"Start" = "开始"; +"Enter encryption secret" = "加密密码"; +"Encryption Mode" = "加密方式"; +"Audio Mixing Controls" = "混音控制"; +"MixingVolume" = "混音音量"; +"MixingPlaybackVolume" = "混音播放音量"; +"MixingPublishVolume" = "混音发布音量"; +"Audio Effect Controls" = "音效控制"; +"EffectVolume" = "音效音量"; +"Destory Track" = "销毁Track"; +"Create Video Track(Encode)" = "创建采集Track(编码)"; +"Create VideoTrack" = "创建采集Track"; +"Push PCM" = "播放PCM"; +"PIP SDK Render" = "画中画SDK 渲染"; +"PIP Custom Render" = "画中画自渲染"; +"PIP Button Title" = "画中画"; +"PIP Size Button Title" = "修改尺寸"; +"Camera Selected" = "相机对焦"; +"Water Mark" = "水印"; +"Gasket push flow" = "垫片推流"; +"Simulcast Stream Title" = "大小流"; +"Opened State" = "已开启"; +"Default State" = "默认:大流"; +"Code Type" = "编码方式"; +"Fast Live" = "极速直播"; +"Link Stream" = "连麦"; +"Please Enter Data" = "请输入数据"; +"Local Video Transcoding" = "本地合图"; +"Pitch Correction" = "高音修正"; +"Tonic Mode" = "主音模式"; +"Tonic Pitch" = "主音音高"; +"Voice Conversion" = "语音转换"; +"Voice Formant" = "语音共振峰"; +"Set Layers Config(Maximum 3)" = "配置多流(最多3个)"; +"Maxmum 3 layers can be selected" = "最多配置三个"; +"LocalCompositeGraph" = "本地合图屏幕共享"; +"3D Voice" = "3D 人声"; +"Neutral" = "中性"; +"Sweet" = "甜美"; +"Solid" = "浑厚"; +"Bass" = "低沉"; +"ARKit" = "ARKit"; +"Simulcast" = "同源多流"; +"Multipath" = "网络传输高级功能"; +"The camera has changed" = "摄像头已切换"; +"Role" = "角色"; +"Mode" = "模式"; +"Customize Voice Effects" = "自定义音效"; +"Pitch" = "音调"; +"BandFreq" = "频段"; +"BandGain" = "频段增益"; +"ReverbKey" = "混响参数"; +"ReverbValue" = "混响值"; +"AINSModel" = "AI 降噪模式"; +"Please the input APPID" = "请输入 App ID"; +"Please the input Token" = "请输入 Token"; diff --git a/iOS/APIExample-SwiftUI/ARCHITECTURE.md b/iOS/APIExample-SwiftUI/ARCHITECTURE.md index b864ed27b..b64b059f5 100644 --- a/iOS/APIExample-SwiftUI/ARCHITECTURE.md +++ b/iOS/APIExample-SwiftUI/ARCHITECTURE.md @@ -111,7 +111,7 @@ APIExample-SwiftUI/ │ ├── Simulcast/ # "Simulcast" │ └── Multipath/ # "Multipath" │ - ├── Resources/ # Audio/video sample files + ├── Resources/ # Audio/video sample files and en/zh-Hans localizations ├── Assets.xcassets/ └── Preview Content/ ``` diff --git a/iOS/APIExample-SwiftUI/Podfile b/iOS/APIExample-SwiftUI/Podfile index 8723d3546..d8467f96f 100644 --- a/iOS/APIExample-SwiftUI/Podfile +++ b/iOS/APIExample-SwiftUI/Podfile @@ -2,7 +2,7 @@ # platform :ios, '9.0' def common_pods - pod 'AgoraRtcEngine_iOS', '4.6.2' + pod 'AgoraRtcEngine_iOS', '4.7.0' #pod 'sdk', :path => 'sdk.podspec' end diff --git a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index 7caae3e46..76fbea3ce 100644 --- a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj +++ b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj @@ -76,19 +76,17 @@ 03DF1D9424CFC29700DF7151 /* AudioController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03DF1D8D24CFC29700DF7151 /* AudioController.m */; }; 03F8733224C8696600EDB1A3 /* EntryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F8733124C8696600EDB1A3 /* EntryViewController.swift */; }; 0AC9459113B4F0B1EFAF4EFE /* Pods_SimpleFilter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC25C1A6D9E6B8827D095985 /* Pods_SimpleFilter.framework */; }; - 5744CE0825BA99FF0099AB66 /* VideoChat.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5744CE0325BA99FF0099AB66 /* VideoChat.strings */; }; 5744CE0925BA99FF0099AB66 /* VideoChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5744CE0525BA99FF0099AB66 /* VideoChat.swift */; }; 5744CE0A25BA99FF0099AB66 /* VideoChat.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5744CE0625BA99FF0099AB66 /* VideoChat.storyboard */; }; 576CA80A25A9CC3A0091520B /* output.raw in Resources */ = {isa = PBXBuildFile; fileRef = 576CA80925A9CC3A0091520B /* output.raw */; }; 576CA80C25AA0FA90091520B /* AgoraPcmSourcePush.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576CA80B25AA0FA90091520B /* AgoraPcmSourcePush.swift */; }; 576EA54225AC3310000B3D79 /* CustomPcmAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576EA54125AC3310000B3D79 /* CustomPcmAudioSource.swift */; }; 576EA54825AC3523000B3D79 /* CustomPcmAudioSource.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 576EA54A25AC3523000B3D79 /* CustomPcmAudioSource.storyboard */; }; - 576EA59025AEDD3C000B3D79 /* (null) in Sources */ = {isa = PBXBuildFile; }; + 576EA59025AEDD3C000B3D79 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; 57FE7C4B26B2D103002D9043 /* CircularBuffer.c in Sources */ = {isa = PBXBuildFile; fileRef = 57FE7C4726B2D103002D9043 /* CircularBuffer.c */; }; 670936FD282DFE1600BC3954 /* ContentInspect.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 670936FF282DFE1600BC3954 /* ContentInspect.storyboard */; }; 6709B23B2806B0EA000BCC58 /* RawAudioData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6709B23A2806B0EA000BCC58 /* RawAudioData.swift */; }; 6709B23C2806BB4A000BCC58 /* RawAudioData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6709B23E2806BB4A000BCC58 /* RawAudioData.storyboard */; }; - 671BD6B827E1DB2D0076D5E1 /* CustomAudioRender.strings in Resources */ = {isa = PBXBuildFile; fileRef = 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */; }; 671BD6B927E1DB2D0076D5E1 /* CustomAudioRender.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */; }; 671BD6BA27E1DB2D0076D5E1 /* CustomAudioRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671BD6B727E1DB2D0076D5E1 /* CustomAudioRender.swift */; }; 67450169282D5D8B00E79F2F /* ContentInspect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67450168282D5D8B00E79F2F /* ContentInspect.swift */; }; @@ -97,7 +95,7 @@ 67B8C7B628057D1500195106 /* RawVideoData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 67B8C7B828057D1500195106 /* RawVideoData.storyboard */; }; 67CB2F0C27EB318200CB19D2 /* SpatialAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67CB2F0A27EB318100CB19D2 /* SpatialAudio.swift */; }; 67CB2F0D27EB318200CB19D2 /* SpatialAudio.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */; }; - 8407E0942472320800AC5DE8 /* (null) in Sources */ = {isa = PBXBuildFile; }; + 8407E0942472320800AC5DE8 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; 8B10BE1126AFFFA6002E1373 /* SimpleFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B10BE0F26AFFFA6002E1373 /* SimpleFilter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8B1422C226B50AB500FFF91B /* AudioProcessor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B1422BC26B50AB400FFF91B /* AudioProcessor.mm */; }; 8B1422C326B50AB500FFF91B /* ExtensionAudioFilter.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 8B1422BD26B50AB400FFF91B /* ExtensionAudioFilter.hpp */; }; @@ -118,10 +116,8 @@ 8B5E5B50274CB68E0040E97D /* RhythmPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5E5B4F274CB68E0040E97D /* RhythmPlayer.swift */; }; 8B5E5B53274CBF760040E97D /* VideoProcess.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B5E5B55274CBF760040E97D /* VideoProcess.storyboard */; }; 8B5E5B5B274CEFE20040E97D /* RhythmPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B5E5B5D274CEFE20040E97D /* RhythmPlayer.storyboard */; }; - 8BA5459426AFEC8D00ED4295 /* SimpleFilter.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8BA5458F26AFEC8D00ED4295 /* SimpleFilter.strings */; }; 8BA5459526AFEC8D00ED4295 /* SimpleFilter.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8BA5459126AFEC8D00ED4295 /* SimpleFilter.storyboard */; }; 8BA5459626AFEC8D00ED4295 /* SimpleFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA5459326AFEC8D00ED4295 /* SimpleFilter.swift */; }; - 8BC751D5273E502700552265 /* LiveStreaming.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8BC751D0273E502700552265 /* LiveStreaming.strings */; }; 8BC751D6273E502700552265 /* LiveStreaming.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8BC751D2273E502700552265 /* LiveStreaming.storyboard */; }; 8BC751D7273E502700552265 /* LiveStreaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC751D4273E502700552265 /* LiveStreaming.swift */; }; 8BC751DA273E57C900552265 /* VideoProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC751D9273E57C900552265 /* VideoProcess.swift */; }; @@ -137,17 +133,14 @@ CBCDE23FB64E60D6A79F3723 /* Pods_Agora_ScreenShare_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09E72C5D1AABD812866E41A6 /* Pods_Agora_ScreenShare_Extension.framework */; }; DD6DCEE22CBD1D68008D7B72 /* hlg-hdr.yuv in Resources */ = {isa = PBXBuildFile; fileRef = DD6DCEE12CBD1D68008D7B72 /* hlg-hdr.yuv */; }; DD6ED7F12CB69FFB007B3B5D /* LocalCompositeGraph.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */; }; - DD6ED7F22CB69FFB007B3B5D /* LocalCompositeGraph.strings in Resources */ = {isa = PBXBuildFile; fileRef = DD6ED7EE2CB69FFB007B3B5D /* LocalCompositeGraph.strings */; }; DD6ED7F32CB69FFB007B3B5D /* LocalCompositeGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6ED7EF2CB69FFB007B3B5D /* LocalCompositeGraph.swift */; }; DD71821E2CBBDF1000523123 /* PlayerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD71821D2CBBDF1000523123 /* PlayerObserver.swift */; }; DD8A1F7C2CA5048D001CEC51 /* AgoraPCMPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A1F7B2CA5048D001CEC51 /* AgoraPCMPlayer.swift */; }; DDCED0342CBAA093004B1C9D /* RtePlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDCED02F2CBAA093004B1C9D /* RtePlayer.storyboard */; }; - DDCED0352CBAA093004B1C9D /* RtePlayer.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCED0312CBAA093004B1C9D /* RtePlayer.strings */; }; DDCED0362CBAA093004B1C9D /* RtePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCED0322CBAA093004B1C9D /* RtePlayer.swift */; }; DDFD50262C205BF20050B344 /* TransparentRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFD50252C205BF20050B344 /* TransparentRender.swift */; }; DDFD50282C205CF40050B344 /* TransparentRender.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDFD50272C205CF40050B344 /* TransparentRender.storyboard */; }; DDFD502A2C2065D00050B344 /* yuv_limit_range_alpha_1280_540_right.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = DDFD50292C2065D00050B344 /* yuv_limit_range_alpha_1280_540_right.mp4 */; }; - E7163F812964149800EBBD55 /* ARKit.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7163F7C2964149800EBBD55 /* ARKit.strings */; }; E7163F822964149800EBBD55 /* ARKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7163F7E2964149800EBBD55 /* ARKit.swift */; }; E7163F832964149800EBBD55 /* ARKit.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7163F7F2964149800EBBD55 /* ARKit.storyboard */; }; E7163F88296414F700EBBD55 /* ARVideoRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7163F86296414F700EBBD55 /* ARVideoRenderer.swift */; }; @@ -155,10 +148,8 @@ E72055EA28F943520030E6D1 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72055E928F943520030E6D1 /* Util.swift */; }; E72055F32900F8780030E6D1 /* KtvCopyrightMusic.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72055F12900F8780030E6D1 /* KtvCopyrightMusic.swift */; }; E721600F28D3314B006431BD /* AlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E721600E28D3314B006431BD /* AlertManager.swift */; }; - E726BFFA2A949F70006870E2 /* AudioRouterPlayer.strings in Resources */ = {isa = PBXBuildFile; fileRef = E726BFF52A949F70006870E2 /* AudioRouterPlayer.strings */; }; E726BFFB2A949F70006870E2 /* AudioRouterPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E726BFF72A949F70006870E2 /* AudioRouterPlayer.swift */; }; E726BFFC2A949F70006870E2 /* AudioRouterPlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E726BFF82A949F70006870E2 /* AudioRouterPlayer.storyboard */; }; - E726C0032A96FD3A006870E2 /* AudioWaveform.strings in Resources */ = {isa = PBXBuildFile; fileRef = E726BFFE2A96FD3A006870E2 /* AudioWaveform.strings */; }; E726C0042A96FD3A006870E2 /* AudioWaveform.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E726C0002A96FD3A006870E2 /* AudioWaveform.storyboard */; }; E726C0052A96FD3A006870E2 /* AudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = E726C0022A96FD3A006870E2 /* AudioWaveform.swift */; }; E726C0082A96FF15006870E2 /* ZSNBoxingView.m in Sources */ = {isa = PBXBuildFile; fileRef = E726C0062A96FF15006870E2 /* ZSNBoxingView.m */; }; @@ -166,11 +157,9 @@ E728B84C28B6015800674A4A /* AgoraPictureInPictureController.m in Sources */ = {isa = PBXBuildFile; fileRef = E728B84B28B6015800674A4A /* AgoraPictureInPictureController.m */; }; E728B84F28B601A300674A4A /* AgoraSampleBufferRender.m in Sources */ = {isa = PBXBuildFile; fileRef = E728B84E28B601A300674A4A /* AgoraSampleBufferRender.m */; }; E728B85128B60D5B00674A4A /* VideoViewSampleBufferDisplayView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E728B85028B60D5B00674A4A /* VideoViewSampleBufferDisplayView.xib */; }; - E728B85828B86B0700674A4A /* CustomVideoSourcePushMulti.strings in Resources */ = {isa = PBXBuildFile; fileRef = E728B85328B86B0700674A4A /* CustomVideoSourcePushMulti.strings */; }; E728B85928B86B0700674A4A /* CustomVideoSourcePushMulti.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E728B85528B86B0700674A4A /* CustomVideoSourcePushMulti.storyboard */; }; E728B85A28B86B0700674A4A /* CustomVideoSourcePushMulti.swift in Sources */ = {isa = PBXBuildFile; fileRef = E728B85728B86B0700674A4A /* CustomVideoSourcePushMulti.swift */; }; E728B85C28B8971200674A4A /* sample.yuv in Resources */ = {isa = PBXBuildFile; fileRef = E728B85B28B8971200674A4A /* sample.yuv */; }; - E74788AC29C7FB6900CD7415 /* JoinChannelVideoRecorder.strings in Resources */ = {isa = PBXBuildFile; fileRef = E74788A729C7FB6800CD7415 /* JoinChannelVideoRecorder.strings */; }; E74788AD29C7FB6900CD7415 /* JoinChannelVideoRecorder.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E74788A929C7FB6800CD7415 /* JoinChannelVideoRecorder.storyboard */; }; E74788AE29C7FB6900CD7415 /* JoinChannelVideoRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E74788AB29C7FB6800CD7415 /* JoinChannelVideoRecorder.swift */; }; E74877B328A23B2F00CA2F58 /* SimpleFilter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B10BE0D26AFFFA6002E1373 /* SimpleFilter.framework */; }; @@ -181,22 +170,17 @@ E76347D02AAEF4AF005D130F /* FUBeautyRender.m in Sources */ = {isa = PBXBuildFile; fileRef = E76347BE2AAEF4AF005D130F /* FUBeautyRender.m */; }; E76347D32AAEF4AF005D130F /* BeautyAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = E76347C72AAEF4AF005D130F /* BeautyAPI.m */; }; E77902672A484A8A008791AD /* KFMP4Demuxer.m in Sources */ = {isa = PBXBuildFile; fileRef = E77902652A484A8A008791AD /* KFMP4Demuxer.m */; }; - E77D54C728F55E9100D51C1E /* JoinChannelVideoToken.strings in Resources */ = {isa = PBXBuildFile; fileRef = E77D54C228F55E9100D51C1E /* JoinChannelVideoToken.strings */; }; E77D54C828F55E9100D51C1E /* JoinChannelVideoToken.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E77D54C428F55E9100D51C1E /* JoinChannelVideoToken.storyboard */; }; E77D54C928F55E9100D51C1E /* JoinChannelVideoToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77D54C628F55E9100D51C1E /* JoinChannelVideoToken.swift */; }; - E7883AE92B074746003CCF44 /* FaceCapture.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7883AE42B074746003CCF44 /* FaceCapture.strings */; }; E7883AEA2B074746003CCF44 /* FaceCapture.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7883AE62B074746003CCF44 /* FaceCapture.storyboard */; }; E7883AEB2B074746003CCF44 /* FaceCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7883AE82B074746003CCF44 /* FaceCapture.swift */; }; - E7899BDC2861673600851463 /* CreateDataStream.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7899BD72861673600851463 /* CreateDataStream.strings */; }; E7899BDD2861673600851463 /* CreateDataStream.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7899BD92861673600851463 /* CreateDataStream.storyboard */; }; E7899BDE2861673600851463 /* CreateDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7899BDB2861673600851463 /* CreateDataStream.swift */; }; E7A49CB12900FC6400F06DD4 /* KtvCopyrightMusic.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */; }; - E7A49CB829011E7500F06DD4 /* MutliCamera.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CB329011E7500F06DD4 /* MutliCamera.strings */; }; E7A49CB929011E7500F06DD4 /* MutliCamera.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CB529011E7500F06DD4 /* MutliCamera.storyboard */; }; E7A49CBA29011E7500F06DD4 /* MutliCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A49CB729011E7500F06DD4 /* MutliCamera.swift */; }; E7A49CFA29029E0000F06DD4 /* ThirdBeautify.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CEC29029E0000F06DD4 /* ThirdBeautify.storyboard */; }; E7A49CFB29029E0000F06DD4 /* ThirdBeautify.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A49CEE29029E0000F06DD4 /* ThirdBeautify.swift */; }; - E7A49CFC29029E0000F06DD4 /* FUBeautify.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CF029029E0000F06DD4 /* FUBeautify.strings */; }; E7A49CFD29029E0000F06DD4 /* FUBeautify.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CF429029E0000F06DD4 /* FUBeautify.storyboard */; }; E7A49CFE29029E0000F06DD4 /* FUBeautifyVC.m in Sources */ = {isa = PBXBuildFile; fileRef = E7A49CF729029E0000F06DD4 /* FUBeautifyVC.m */; }; E7A49CFF29029E0000F06DD4 /* FUManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E7A49CF829029E0000F06DD4 /* FUManager.m */; }; @@ -209,10 +193,8 @@ F728B9DC2CA295D7007813BB /* PixelBufferPIPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D32CA295D7007813BB /* PixelBufferPIPService.swift */; }; F728B9DD2CA295D7007813BB /* PixelBufferPIPViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D42CA295D7007813BB /* PixelBufferPIPViewController.swift */; }; F728B9DE2CA295D7007813BB /* PixelBufferRenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D52CA295D7007813BB /* PixelBufferRenderView.swift */; }; - F728B9E02CA29625007813BB /* PictureInPicture.strings in Resources */ = {isa = PBXBuildFile; fileRef = F728B9DF2CA29625007813BB /* PictureInPicture.strings */; }; F734791C2DF5C9CD0030C0CE /* beauty_material.bundle in Resources */ = {isa = PBXBuildFile; fileRef = F734791B2DF5C9CD0030C0CE /* beauty_material.bundle */; }; F734791E2DF5CA0D0030C0CE /* AgoraBeauty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F734791D2DF5CA0D0030C0CE /* AgoraBeauty.swift */; }; - F73479212DF5CA140030C0CE /* AgoraBeauty.strings in Resources */ = {isa = PBXBuildFile; fileRef = F734791F2DF5CA140030C0CE /* AgoraBeauty.strings */; }; F73479242DF5CA820030C0CE /* AgoraBeauty.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73479222DF5CA820030C0CE /* AgoraBeauty.storyboard */; }; F73479262DF5CAEB0030C0CE /* AgoraBeautyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73479252DF5CAEB0030C0CE /* AgoraBeautyManager.swift */; }; F73B01D52CCB84590077B7D2 /* APIReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = F73B01D32CCB84590077B7D2 /* APIReporter.m */; }; @@ -359,7 +341,6 @@ 23B93D5F4D8304FBAAE2A23E /* Pods-SimpleFilter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleFilter.debug.xcconfig"; path = "Target Support Files/Pods-SimpleFilter/Pods-SimpleFilter.debug.xcconfig"; sourceTree = ""; }; 3EA7D4B4D7C9540659392B7F /* Pods-APIExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample.debug.xcconfig"; path = "Target Support Files/Pods-APIExample/Pods-APIExample.debug.xcconfig"; sourceTree = ""; }; 4B422A0CD53DE0FBBEBCB2CA /* Pods-SimpleFilter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleFilter.release.xcconfig"; path = "Target Support Files/Pods-SimpleFilter/Pods-SimpleFilter.release.xcconfig"; sourceTree = ""; }; - 5744CE0425BA99FF0099AB66 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/VideoChat.strings"; sourceTree = ""; }; 5744CE0525BA99FF0099AB66 /* VideoChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoChat.swift; sourceTree = ""; }; 5744CE0725BA99FF0099AB66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/VideoChat.storyboard; sourceTree = ""; }; 5744CE0C25BA9A1B0099AB66 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/VideoChat.strings"; sourceTree = ""; }; @@ -388,7 +369,8 @@ 67B8C7B728057D1500195106 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/RawVideoData.storyboard; sourceTree = ""; }; 67B8C7BA28057D1800195106 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RawVideoData.strings"; sourceTree = ""; }; 67CB2F0A27EB318100CB19D2 /* SpatialAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpatialAudio.swift; sourceTree = ""; }; - 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SpatialAudio.storyboard; sourceTree = ""; }; + F7CB2F0B27EB318200CB19D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SpatialAudio.storyboard; sourceTree = ""; }; + F7CB2F0C27EB318200CB19D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SpatialAudio.strings"; sourceTree = ""; }; 6F2725A37161C72C3445F0D7 /* Pods-Agora-ScreenShare-Extension(Socket).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScreenShare-Extension(Socket).debug.xcconfig"; path = "Target Support Files/Pods-Agora-ScreenShare-Extension(Socket)/Pods-Agora-ScreenShare-Extension(Socket).debug.xcconfig"; sourceTree = ""; }; 846AE4340F81DCC00B6F9543 /* Pods-APIExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample.release.xcconfig"; path = "Target Support Files/Pods-APIExample/Pods-APIExample.release.xcconfig"; sourceTree = ""; }; 8551A4BC255CE49B383BC575 /* Pods_APIExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -417,7 +399,6 @@ 8B5E5B5A274CDF5B0040E97D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/VideoProcess.strings"; sourceTree = ""; }; 8B5E5B5C274CEFE20040E97D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/RhythmPlayer.storyboard; sourceTree = ""; }; 8B5E5B5F274CEFE60040E97D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RhythmPlayer.strings"; sourceTree = ""; }; - 8BA5459026AFEC8D00ED4295 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SimpleFilter.strings"; sourceTree = ""; }; 8BA5459226AFEC8D00ED4295 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SimpleFilter.storyboard; sourceTree = ""; }; 8BA5459326AFEC8D00ED4295 /* SimpleFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleFilter.swift; sourceTree = ""; }; 8BAEA7B426C253FC0006B0F1 /* APIExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = APIExample.entitlements; sourceTree = ""; }; @@ -439,6 +420,8 @@ ABB399EC2DF9605B00AF5033 /* Simulcast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Simulcast.swift; sourceTree = ""; }; ABB399ED2DF9605B00AF5033 /* Simulcast.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Simulcast.storyboard; path = Base.lproj/Simulcast.storyboard; sourceTree = ""; }; ABB399F62DF9608700AF5033 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Multipath.strings; sourceTree = ""; }; + ABB90ACE2DF72E2F007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Simulcast.strings"; sourceTree = ""; }; + ABB90AD02DF72E8D007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Multipath.strings"; sourceTree = ""; }; BC25C1A6D9E6B8827D095985 /* Pods_SimpleFilter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SimpleFilter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CC6D08A23527C200339E4FD6 /* Pods-SimpleAudioFilter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleAudioFilter.release.xcconfig"; path = "Target Support Files/Pods-SimpleAudioFilter/Pods-SimpleAudioFilter.release.xcconfig"; sourceTree = ""; }; DD6DCEE12CBD1D68008D7B72 /* hlg-hdr.yuv */ = {isa = PBXFileReference; lastKnownFileType = file; path = "hlg-hdr.yuv"; sourceTree = ""; }; @@ -451,7 +434,8 @@ DDCED0302CBAA093004B1C9D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RtePlayer.strings"; sourceTree = ""; }; DDCED0322CBAA093004B1C9D /* RtePlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RtePlayer.swift; sourceTree = ""; }; DDFD50252C205BF20050B344 /* TransparentRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentRender.swift; sourceTree = ""; }; - DDFD50272C205CF40050B344 /* TransparentRender.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TransparentRender.storyboard; sourceTree = ""; }; + F7FD50272C205CF40050B344 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TransparentRender.storyboard; sourceTree = ""; }; + F7FD50282C205CF40050B344 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/TransparentRender.strings"; sourceTree = ""; }; DDFD50292C2065D00050B344 /* yuv_limit_range_alpha_1280_540_right.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = yuv_limit_range_alpha_1280_540_right.mp4; sourceTree = ""; }; E7163F7D2964149800EBBD55 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ARKit.strings"; sourceTree = ""; }; E7163F7E2964149800EBBD55 /* ARKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARKit.swift; sourceTree = ""; }; @@ -475,7 +459,6 @@ E728B84D28B601A300674A4A /* AgoraSampleBufferRender.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AgoraSampleBufferRender.h; sourceTree = ""; }; E728B84E28B601A300674A4A /* AgoraSampleBufferRender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AgoraSampleBufferRender.m; sourceTree = ""; }; E728B85028B60D5B00674A4A /* VideoViewSampleBufferDisplayView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VideoViewSampleBufferDisplayView.xib; sourceTree = ""; }; - E728B85428B86B0700674A4A /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CustomVideoSourcePushMulti.strings"; sourceTree = ""; }; E728B85628B86B0700674A4A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomVideoSourcePushMulti.storyboard; sourceTree = ""; }; E728B85728B86B0700674A4A /* CustomVideoSourcePushMulti.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomVideoSourcePushMulti.swift; sourceTree = ""; }; E728B85B28B8971200674A4A /* sample.yuv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sample.yuv; sourceTree = ""; }; @@ -501,7 +484,8 @@ E7899BD82861673600851463 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CreateDataStream.strings"; sourceTree = ""; }; E7899BDA2861673600851463 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CreateDataStream.storyboard; sourceTree = ""; }; E7899BDB2861673600851463 /* CreateDataStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDataStream.swift; sourceTree = ""; }; - E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = KtvCopyrightMusic.storyboard; sourceTree = ""; }; + F7A49CB02900FC6400F06DD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/KtvCopyrightMusic.storyboard; sourceTree = ""; }; + F7A49CB12900FC6400F06DD4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KtvCopyrightMusic.strings"; sourceTree = ""; }; E7A49CB429011E7500F06DD4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MutliCamera.strings"; sourceTree = ""; }; E7A49CB629011E7500F06DD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MutliCamera.storyboard; sourceTree = ""; }; E7A49CB729011E7500F06DD4 /* MutliCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutliCamera.swift; sourceTree = ""; }; @@ -527,7 +511,6 @@ F728B9D32CA295D7007813BB /* PixelBufferPIPService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PixelBufferPIPService.swift; sourceTree = ""; }; F728B9D42CA295D7007813BB /* PixelBufferPIPViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PixelBufferPIPViewController.swift; sourceTree = ""; }; F728B9D52CA295D7007813BB /* PixelBufferRenderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PixelBufferRenderView.swift; sourceTree = ""; }; - F728B9DF2CA29625007813BB /* PictureInPicture.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = PictureInPicture.strings; sourceTree = ""; }; F734791B2DF5C9CD0030C0CE /* beauty_material.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = beauty_material.bundle; sourceTree = ""; }; F734791D2DF5CA0D0030C0CE /* AgoraBeauty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraBeauty.swift; sourceTree = ""; }; F73479202DF5CA140030C0CE /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/AgoraBeauty.strings"; sourceTree = ""; }; @@ -904,7 +887,6 @@ 5744CE0225BA99FF0099AB66 /* VideoChat */ = { isa = PBXGroup; children = ( - 5744CE0325BA99FF0099AB66 /* VideoChat.strings */, 5744CE0525BA99FF0099AB66 /* VideoChat.swift */, 5744CE0625BA99FF0099AB66 /* VideoChat.storyboard */, ); @@ -932,7 +914,6 @@ 671BD6B227E1DB2D0076D5E1 /* CustomAudioRender */ = { isa = PBXGroup; children = ( - 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */, 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */, 671BD6B727E1DB2D0076D5E1 /* CustomAudioRender.swift */, ); @@ -1015,7 +996,6 @@ 8BA5458E26AFEC8D00ED4295 /* SimpleFilter */ = { isa = PBXGroup; children = ( - 8BA5458F26AFEC8D00ED4295 /* SimpleFilter.strings */, 8BA5459126AFEC8D00ED4295 /* SimpleFilter.storyboard */, 8BA5459326AFEC8D00ED4295 /* SimpleFilter.swift */, ); @@ -1025,7 +1005,6 @@ 8BC751CF273E502700552265 /* LiveStreaming */ = { isa = PBXGroup; children = ( - 8BC751D0273E502700552265 /* LiveStreaming.strings */, 8BC751D2273E502700552265 /* LiveStreaming.storyboard */, 8BC751D4273E502700552265 /* LiveStreaming.swift */, ); @@ -1135,7 +1114,6 @@ isa = PBXGroup; children = ( DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */, - DD6ED7EE2CB69FFB007B3B5D /* LocalCompositeGraph.strings */, DD6ED7EF2CB69FFB007B3B5D /* LocalCompositeGraph.swift */, ); path = LocalCompositeGraph; @@ -1145,7 +1123,6 @@ isa = PBXGroup; children = ( DDCED02F2CBAA093004B1C9D /* RtePlayer.storyboard */, - DDCED0312CBAA093004B1C9D /* RtePlayer.strings */, DDCED0322CBAA093004B1C9D /* RtePlayer.swift */, DD71821D2CBBDF1000523123 /* PlayerObserver.swift */, ); @@ -1164,7 +1141,6 @@ E7163F7B2964149800EBBD55 /* ARKit */ = { isa = PBXGroup; children = ( - E7163F7C2964149800EBBD55 /* ARKit.strings */, E7163F7E2964149800EBBD55 /* ARKit.swift */, E7163F7F2964149800EBBD55 /* ARKit.storyboard */, ); @@ -1191,7 +1167,6 @@ E726BFF42A949F70006870E2 /* AudioRouterPlayer */ = { isa = PBXGroup; children = ( - E726BFF52A949F70006870E2 /* AudioRouterPlayer.strings */, E726BFF72A949F70006870E2 /* AudioRouterPlayer.swift */, E726BFF82A949F70006870E2 /* AudioRouterPlayer.storyboard */, ); @@ -1201,7 +1176,6 @@ E726BFFD2A96FD3A006870E2 /* AudioWaveform */ = { isa = PBXGroup; children = ( - E726BFFE2A96FD3A006870E2 /* AudioWaveform.strings */, E726C0002A96FD3A006870E2 /* AudioWaveform.storyboard */, E726C0022A96FD3A006870E2 /* AudioWaveform.swift */, ); @@ -1216,7 +1190,6 @@ F728B9D22CA295D7007813BB /* PIPBaseViewController.swift */, F728B9D62CA295D7007813BB /* PixelBufferPIPViewController */, E728B84428B5FFCB00674A4A /* PictureInPicture.swift */, - F728B9DF2CA29625007813BB /* PictureInPicture.strings */, ); path = PictureInPicture; sourceTree = ""; @@ -1224,7 +1197,6 @@ E728B85228B86B0700674A4A /* CustomVideoSourcePushMulti */ = { isa = PBXGroup; children = ( - E728B85328B86B0700674A4A /* CustomVideoSourcePushMulti.strings */, E728B85528B86B0700674A4A /* CustomVideoSourcePushMulti.storyboard */, E728B85728B86B0700674A4A /* CustomVideoSourcePushMulti.swift */, ); @@ -1235,7 +1207,6 @@ E74788A629C7FB6800CD7415 /* JoinChannelVideo(Recorder) */ = { isa = PBXGroup; children = ( - E74788A729C7FB6800CD7415 /* JoinChannelVideoRecorder.strings */, E74788A929C7FB6800CD7415 /* JoinChannelVideoRecorder.storyboard */, E74788AB29C7FB6800CD7415 /* JoinChannelVideoRecorder.swift */, ); @@ -1283,7 +1254,6 @@ E77D54C128F55E9100D51C1E /* JoinChannelVideo(Token) */ = { isa = PBXGroup; children = ( - E77D54C228F55E9100D51C1E /* JoinChannelVideoToken.strings */, E77D54C428F55E9100D51C1E /* JoinChannelVideoToken.storyboard */, E77D54C628F55E9100D51C1E /* JoinChannelVideoToken.swift */, ); @@ -1293,7 +1263,6 @@ E7883AE32B074746003CCF44 /* FaceCapture */ = { isa = PBXGroup; children = ( - E7883AE42B074746003CCF44 /* FaceCapture.strings */, E7883AE62B074746003CCF44 /* FaceCapture.storyboard */, E7883AE82B074746003CCF44 /* FaceCapture.swift */, ); @@ -1303,7 +1272,6 @@ E7899BD62861673600851463 /* CreateDataStream */ = { isa = PBXGroup; children = ( - E7899BD72861673600851463 /* CreateDataStream.strings */, E7899BD92861673600851463 /* CreateDataStream.storyboard */, E7899BDB2861673600851463 /* CreateDataStream.swift */, ); @@ -1313,7 +1281,6 @@ E7A49CB229011E7500F06DD4 /* MutliCamera */ = { isa = PBXGroup; children = ( - E7A49CB329011E7500F06DD4 /* MutliCamera.strings */, E7A49CB529011E7500F06DD4 /* MutliCamera.storyboard */, E7A49CB729011E7500F06DD4 /* MutliCamera.swift */, ); @@ -1335,7 +1302,6 @@ isa = PBXGroup; children = ( E7A49D0029029E1900F06DD4 /* Manager */, - E7A49CF029029E0000F06DD4 /* FUBeautify.strings */, E7A49CF429029E0000F06DD4 /* FUBeautify.storyboard */, E7A49CF229029E0000F06DD4 /* FUBeautifyVC.h */, E7A49CF729029E0000F06DD4 /* FUBeautifyVC.m */, @@ -1368,7 +1334,6 @@ F73479222DF5CA820030C0CE /* AgoraBeauty.storyboard */, F734791D2DF5CA0D0030C0CE /* AgoraBeauty.swift */, F73479252DF5CAEB0030C0CE /* AgoraBeautyManager.swift */, - F734791F2DF5CA140030C0CE /* AgoraBeauty.strings */, ); path = AgoraBeauty; sourceTree = ""; @@ -1538,7 +1503,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5744CE0825BA99FF0099AB66 /* VideoChat.strings in Resources */, 033A9F8E252D8FF300BC26E1 /* JoinMultiChannel.storyboard in Resources */, 03BEED0B251C4446005E78F4 /* audiomixing.mp3 in Resources */, DDCED0342CBAA093004B1C9D /* RtePlayer.storyboard in Resources */, @@ -1552,37 +1516,26 @@ 5744CE0A25BA99FF0099AB66 /* VideoChat.storyboard in Resources */, 033A9F3F252D89BC00BC26E1 /* RTMPStreaming.storyboard in Resources */, 033A9F2A252D737900BC26E1 /* Localizable.strings in Resources */, - E726C0032A96FD3A006870E2 /* AudioWaveform.strings in Resources */, - F728B9E02CA29625007813BB /* PictureInPicture.strings in Resources */, E726BFFC2A949F70006870E2 /* AudioRouterPlayer.storyboard in Resources */, 576EA54825AC3523000B3D79 /* CustomPcmAudioSource.storyboard in Resources */, - E728B85828B86B0700674A4A /* CustomVideoSourcePushMulti.strings in Resources */, ABB399F32DF9605B00AF5033 /* Multipath.storyboard in Resources */, ABB399F42DF9605B00AF5033 /* Simulcast.storyboard in Resources */, 6709B23C2806BB4A000BCC58 /* RawAudioData.storyboard in Resources */, 033A9F7F252D8B5900BC26E1 /* AudioMixing.storyboard in Resources */, - F73479212DF5CA140030C0CE /* AgoraBeauty.strings in Resources */, - E7A49CB829011E7500F06DD4 /* MutliCamera.strings in Resources */, E7A49CFA29029E0000F06DD4 /* ThirdBeautify.storyboard in Resources */, - E7883AE92B074746003CCF44 /* FaceCapture.strings in Resources */, 033A9F7A252D8B5000BC26E1 /* MediaPlayer.storyboard in Resources */, 0339D6D224E91B80008739CD /* QuickSwitchChannelVCItem.xib in Resources */, E7163F8A29651D8900EBBD55 /* AR.scnassets in Resources */, E7AD0DE129C85FFB00C9A4B0 /* sample.mov in Resources */, 03BEED0D251CAB9C005E78F4 /* audioeffect.mp3 in Resources */, A7CA48C424553CF700507435 /* Popover.storyboard in Resources */, - E74788AC29C7FB6900CD7415 /* JoinChannelVideoRecorder.strings in Resources */, - E7A49CFC29029E0000F06DD4 /* FUBeautify.strings in Resources */, 03D13BDC2448758B00B599B3 /* LaunchScreen.storyboard in Resources */, E728B85C28B8971200674A4A /* sample.yuv in Resources */, - 8BC751D5273E502700552265 /* LiveStreaming.strings in Resources */, 8B349FE32681E2CE007247F2 /* agora-logo.png in Resources */, E728B85928B86B0700674A4A /* CustomVideoSourcePushMulti.storyboard in Resources */, F73479242DF5CA820030C0CE /* AgoraBeauty.storyboard in Resources */, 033A9F23252D70E400BC26E1 /* JoinChannelVideo.storyboard in Resources */, 03414B5525546DEC00AB114D /* frames0.yuv in Resources */, - E77D54C728F55E9100D51C1E /* JoinChannelVideoToken.strings in Resources */, - DDCED0352CBAA093004B1C9D /* RtePlayer.strings in Resources */, 8B5E5B53274CBF760040E97D /* VideoProcess.storyboard in Resources */, E7883AEA2B074746003CCF44 /* FaceCapture.storyboard in Resources */, DD6DCEE22CBD1D68008D7B72 /* hlg-hdr.yuv in Resources */, @@ -1594,28 +1547,22 @@ 03B12DAA251125B700E55818 /* VideoView.xib in Resources */, E7163F832964149800EBBD55 /* ARKit.storyboard in Resources */, 8BA5459526AFEC8D00ED4295 /* SimpleFilter.storyboard in Resources */, - E7163F812964149800EBBD55 /* ARKit.strings in Resources */, DD6ED7F12CB69FFB007B3B5D /* LocalCompositeGraph.storyboard in Resources */, 67CB2F0D27EB318200CB19D2 /* SpatialAudio.storyboard in Resources */, - 8BA5459426AFEC8D00ED4295 /* SimpleFilter.strings in Resources */, 033A9F4D252D89DB00BC26E1 /* CustomAudioSource.storyboard in Resources */, F734791C2DF5C9CD0030C0CE /* beauty_material.bundle in Resources */, E7A49CB12900FC6400F06DD4 /* KtvCopyrightMusic.storyboard in Resources */, - E7899BDC2861673600851463 /* CreateDataStream.strings in Resources */, E7A49CFD29029E0000F06DD4 /* FUBeautify.storyboard in Resources */, 8B5E5B5B274CEFE20040E97D /* RhythmPlayer.storyboard in Resources */, 033A9F84252D8B6400BC26E1 /* StreamEncryption.storyboard in Resources */, 67B8C7B628057D1500195106 /* RawVideoData.storyboard in Resources */, E726C0042A96FD3A006870E2 /* AudioWaveform.storyboard in Resources */, - DD6ED7F22CB69FFB007B3B5D /* LocalCompositeGraph.strings in Resources */, 033A9F30252D860100BC26E1 /* JoinChannelAudio.storyboard in Resources */, 033A9F75252D8B4800BC26E1 /* ScreenShare.storyboard in Resources */, - 671BD6B827E1DB2D0076D5E1 /* CustomAudioRender.strings in Resources */, DDFD50282C205CF40050B344 /* TransparentRender.storyboard in Resources */, 033A9F5C252D89FD00BC26E1 /* CustomVideoSourcePush.storyboard in Resources */, 03D13BD92448758B00B599B3 /* Assets.xcassets in Resources */, DDFD502A2C2065D00050B344 /* yuv_limit_range_alpha_1280_540_right.mp4 in Resources */, - E726BFFA2A949F70006870E2 /* AudioRouterPlayer.strings in Resources */, 033A9F6B252D8B3500BC26E1 /* MediaChannelRelay.storyboard in Resources */, 671BD6B927E1DB2D0076D5E1 /* CustomAudioRender.storyboard in Resources */, E728B85128B60D5B00674A4A /* VideoViewSampleBufferDisplayView.xib in Resources */, @@ -1642,14 +1589,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-frameworks.sh\"\n"; @@ -1778,7 +1721,7 @@ E728B84F28B601A300674A4A /* AgoraSampleBufferRender.m in Sources */, 8B333DA9267B4BC3002A3785 /* SettingsCells.swift in Sources */, 033A9EFC252D61E200BC26E1 /* CustomVideoRender.swift in Sources */, - 576EA59025AEDD3C000B3D79 /* (null) in Sources */, + 576EA59025AEDD3C000B3D79 /* BuildFile in Sources */, 033A9F09252D61FC00BC26E1 /* RTMPStreaming.swift in Sources */, F734791E2DF5CA0D0030C0CE /* AgoraBeauty.swift in Sources */, 6709B23B2806B0EA000BCC58 /* RawAudioData.swift in Sources */, @@ -1787,7 +1730,7 @@ E728B85A28B86B0700674A4A /* CustomVideoSourcePushMulti.swift in Sources */, E728B84C28B6015800674A4A /* AgoraPictureInPictureController.m in Sources */, F728B9D82CA295D7007813BB /* ChannelViewController.swift in Sources */, - 8407E0942472320800AC5DE8 /* (null) in Sources */, + 8407E0942472320800AC5DE8 /* BuildFile in Sources */, 8B5E5B50274CB68E0040E97D /* RhythmPlayer.swift in Sources */, E7A49D342907E74A00F06DD4 /* BundleUtil.m in Sources */, 036C42B524D2A3C600A59000 /* AgoraMetalRender.swift in Sources */, @@ -1889,6 +1832,33 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7CB2F0B27EB318200CB19D2 /* Base */, + F7CB2F0C27EB318200CB19D2 /* zh-Hans */, + ); + name = SpatialAudio.storyboard; + sourceTree = ""; + }; + DDFD50272C205CF40050B344 /* TransparentRender.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7FD50272C205CF40050B344 /* Base */, + F7FD50282C205CF40050B344 /* zh-Hans */, + ); + name = TransparentRender.storyboard; + sourceTree = ""; + }; + E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7A49CB02900FC6400F06DD4 /* Base */, + F7A49CB12900FC6400F06DD4 /* zh-Hans */, + ); + name = KtvCopyrightMusic.storyboard; + sourceTree = ""; + }; 033A9F25252D70E400BC26E1 /* JoinChannelVideo.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2049,14 +2019,6 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; - 5744CE0325BA99FF0099AB66 /* VideoChat.strings */ = { - isa = PBXVariantGroup; - children = ( - 5744CE0425BA99FF0099AB66 /* zh-Hans */, - ); - name = VideoChat.strings; - sourceTree = ""; - }; 5744CE0625BA99FF0099AB66 /* VideoChat.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2093,18 +2055,11 @@ name = RawAudioData.storyboard; sourceTree = ""; }; - 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */ = { - isa = PBXVariantGroup; - children = ( - 671BD6B427E1DB2D0076D5E1 /* zh-Hans */, - ); - name = CustomAudioRender.strings; - sourceTree = ""; - }; 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */ = { isa = PBXVariantGroup; children = ( 671BD6B627E1DB2D0076D5E1 /* Base */, + 671BD6B427E1DB2D0076D5E1 /* zh-Hans */, ); name = CustomAudioRender.storyboard; sourceTree = ""; @@ -2136,14 +2091,6 @@ name = RhythmPlayer.storyboard; sourceTree = ""; }; - 8BA5458F26AFEC8D00ED4295 /* SimpleFilter.strings */ = { - isa = PBXVariantGroup; - children = ( - 8BA5459026AFEC8D00ED4295 /* zh-Hans */, - ); - name = SimpleFilter.strings; - sourceTree = ""; - }; 8BA5459126AFEC8D00ED4295 /* SimpleFilter.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2154,18 +2101,11 @@ name = SimpleFilter.storyboard; sourceTree = ""; }; - 8BC751D0273E502700552265 /* LiveStreaming.strings */ = { - isa = PBXVariantGroup; - children = ( - 8BC751D1273E502700552265 /* zh-Hans */, - ); - name = LiveStreaming.strings; - sourceTree = ""; - }; 8BC751D2273E502700552265 /* LiveStreaming.storyboard */ = { isa = PBXVariantGroup; children = ( 8BC751D3273E502700552265 /* Base */, + 8BC751D1273E502700552265 /* zh-Hans */, ); name = LiveStreaming.storyboard; sourceTree = ""; @@ -2183,6 +2123,7 @@ children = ( ABB399E82DF9605B00AF5033 /* Multipath.storyboard */, ABB399F62DF9608700AF5033 /* en */, + ABB90AD02DF72E8D007FDDEB /* zh-Hans */, ); name = Multipath.storyboard; sourceTree = ""; @@ -2192,6 +2133,7 @@ children = ( ABB399ED2DF9605B00AF5033 /* Simulcast.storyboard */, AB8473EC2DF960D800B4DB73 /* en */, + ABB90ACE2DF72E2F007FDDEB /* zh-Hans */, ); name = Simulcast.storyboard; sourceTree = ""; @@ -2200,90 +2142,47 @@ isa = PBXVariantGroup; children = ( DD6ED7EB2CB69FFB007B3B5D /* Base */, - ); - name = LocalCompositeGraph.storyboard; - sourceTree = ""; - }; - DD6ED7EE2CB69FFB007B3B5D /* LocalCompositeGraph.strings */ = { - isa = PBXVariantGroup; - children = ( DD6ED7ED2CB69FFB007B3B5D /* zh-Hans */, ); - name = LocalCompositeGraph.strings; + name = LocalCompositeGraph.storyboard; sourceTree = ""; }; DDCED02F2CBAA093004B1C9D /* RtePlayer.storyboard */ = { isa = PBXVariantGroup; children = ( DDCED02E2CBAA093004B1C9D /* Base */, - ); - name = RtePlayer.storyboard; - sourceTree = ""; - }; - DDCED0312CBAA093004B1C9D /* RtePlayer.strings */ = { - isa = PBXVariantGroup; - children = ( DDCED0302CBAA093004B1C9D /* zh-Hans */, ); - name = RtePlayer.strings; - sourceTree = ""; - }; - E7163F7C2964149800EBBD55 /* ARKit.strings */ = { - isa = PBXVariantGroup; - children = ( - E7163F7D2964149800EBBD55 /* zh-Hans */, - ); - name = ARKit.strings; + name = RtePlayer.storyboard; sourceTree = ""; }; E7163F7F2964149800EBBD55 /* ARKit.storyboard */ = { isa = PBXVariantGroup; children = ( E7163F802964149800EBBD55 /* Base */, + E7163F7D2964149800EBBD55 /* zh-Hans */, ); name = ARKit.storyboard; sourceTree = ""; }; - E726BFF52A949F70006870E2 /* AudioRouterPlayer.strings */ = { - isa = PBXVariantGroup; - children = ( - E726BFF62A949F70006870E2 /* zh-Hans */, - ); - name = AudioRouterPlayer.strings; - sourceTree = ""; - }; E726BFF82A949F70006870E2 /* AudioRouterPlayer.storyboard */ = { isa = PBXVariantGroup; children = ( E726BFF92A949F70006870E2 /* Base */, + E726BFF62A949F70006870E2 /* zh-Hans */, ); name = AudioRouterPlayer.storyboard; sourceTree = ""; }; - E726BFFE2A96FD3A006870E2 /* AudioWaveform.strings */ = { - isa = PBXVariantGroup; - children = ( - E726BFFF2A96FD3A006870E2 /* zh-Hans */, - ); - name = AudioWaveform.strings; - sourceTree = ""; - }; E726C0002A96FD3A006870E2 /* AudioWaveform.storyboard */ = { isa = PBXVariantGroup; children = ( E726C0012A96FD3A006870E2 /* Base */, + E726BFFF2A96FD3A006870E2 /* zh-Hans */, ); name = AudioWaveform.storyboard; sourceTree = ""; }; - E728B85328B86B0700674A4A /* CustomVideoSourcePushMulti.strings */ = { - isa = PBXVariantGroup; - children = ( - E728B85428B86B0700674A4A /* zh-Hans */, - ); - name = CustomVideoSourcePushMulti.strings; - sourceTree = ""; - }; E728B85528B86B0700674A4A /* CustomVideoSourcePushMulti.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2293,82 +2192,47 @@ name = CustomVideoSourcePushMulti.storyboard; sourceTree = ""; }; - E74788A729C7FB6800CD7415 /* JoinChannelVideoRecorder.strings */ = { - isa = PBXVariantGroup; - children = ( - E74788A829C7FB6800CD7415 /* zh-Hans */, - ); - name = JoinChannelVideoRecorder.strings; - sourceTree = ""; - }; E74788A929C7FB6800CD7415 /* JoinChannelVideoRecorder.storyboard */ = { isa = PBXVariantGroup; children = ( E74788AA29C7FB6800CD7415 /* Base */, + E74788A829C7FB6800CD7415 /* zh-Hans */, ); name = JoinChannelVideoRecorder.storyboard; sourceTree = ""; }; - E77D54C228F55E9100D51C1E /* JoinChannelVideoToken.strings */ = { - isa = PBXVariantGroup; - children = ( - E77D54C328F55E9100D51C1E /* zh-Hans */, - ); - name = JoinChannelVideoToken.strings; - sourceTree = ""; - }; E77D54C428F55E9100D51C1E /* JoinChannelVideoToken.storyboard */ = { isa = PBXVariantGroup; children = ( E77D54C528F55E9100D51C1E /* Base */, + E77D54C328F55E9100D51C1E /* zh-Hans */, ); name = JoinChannelVideoToken.storyboard; sourceTree = ""; }; - E7883AE42B074746003CCF44 /* FaceCapture.strings */ = { - isa = PBXVariantGroup; - children = ( - E7883AE52B074746003CCF44 /* zh-Hans */, - ); - name = FaceCapture.strings; - sourceTree = ""; - }; E7883AE62B074746003CCF44 /* FaceCapture.storyboard */ = { isa = PBXVariantGroup; children = ( E7883AE72B074746003CCF44 /* Base */, + E7883AE52B074746003CCF44 /* zh-Hans */, ); name = FaceCapture.storyboard; sourceTree = ""; }; - E7899BD72861673600851463 /* CreateDataStream.strings */ = { - isa = PBXVariantGroup; - children = ( - E7899BD82861673600851463 /* zh-Hans */, - ); - name = CreateDataStream.strings; - sourceTree = ""; - }; E7899BD92861673600851463 /* CreateDataStream.storyboard */ = { isa = PBXVariantGroup; children = ( E7899BDA2861673600851463 /* Base */, + E7899BD82861673600851463 /* zh-Hans */, ); name = CreateDataStream.storyboard; sourceTree = ""; }; - E7A49CB329011E7500F06DD4 /* MutliCamera.strings */ = { - isa = PBXVariantGroup; - children = ( - E7A49CB429011E7500F06DD4 /* zh-Hans */, - ); - name = MutliCamera.strings; - sourceTree = ""; - }; E7A49CB529011E7500F06DD4 /* MutliCamera.storyboard */ = { isa = PBXVariantGroup; children = ( E7A49CB629011E7500F06DD4 /* Base */, + E7A49CB429011E7500F06DD4 /* zh-Hans */, ); name = MutliCamera.storyboard; sourceTree = ""; @@ -2382,34 +2246,20 @@ name = ThirdBeautify.storyboard; sourceTree = ""; }; - E7A49CF029029E0000F06DD4 /* FUBeautify.strings */ = { - isa = PBXVariantGroup; - children = ( - E7A49CF129029E0000F06DD4 /* zh-Hans */, - ); - name = FUBeautify.strings; - sourceTree = ""; - }; E7A49CF429029E0000F06DD4 /* FUBeautify.storyboard */ = { isa = PBXVariantGroup; children = ( E7A49CF529029E0000F06DD4 /* Base */, + E7A49CF129029E0000F06DD4 /* zh-Hans */, ); name = FUBeautify.storyboard; sourceTree = ""; }; - F734791F2DF5CA140030C0CE /* AgoraBeauty.strings */ = { - isa = PBXVariantGroup; - children = ( - F73479202DF5CA140030C0CE /* zh-Hans */, - ); - name = AgoraBeauty.strings; - sourceTree = ""; - }; F73479222DF5CA820030C0CE /* AgoraBeauty.storyboard */ = { isa = PBXVariantGroup; children = ( F73479232DF5CA820030C0CE /* Base */, + F73479202DF5CA140030C0CE /* zh-Hans */, ); name = AgoraBeauty.storyboard; sourceTree = ""; @@ -2439,7 +2289,7 @@ "$(inherited)", "$(PROJECT_DIR)/Agora-ScreenShare-Extension", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples.Agora-ScreenShare-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2473,7 +2323,7 @@ "$(inherited)", "$(PROJECT_DIR)/Agora-ScreenShare-Extension", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = "io.agora.api.examples.Agora-ScreenShare-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2624,7 +2474,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2695,7 +2545,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings b/iOS/APIExample/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings index 443a2872d..228418216 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings @@ -1,11 +1,14 @@ +/* Element = "label"; text = "First Frame Optimization"; ObjectID = "2xt-aD-ydA"; */ +"2xt-aD-ydA.text" = "首帧出图"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UILabel"; text = "Move Camera to find a planar (Shown as Red Rectangle)"; ObjectID = "bEC-x6-7dT"; */ -"bEC-x6-7dT.text" = "Move camera to find a planar (Shown as red rectangle)"; +/* Element = "label"; text = "Label"; ObjectID = "VUf-PA-t4Z"; */ +"VUf-PA-t4Z.text" = "标签"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "label"; text = "Move Camera to find a planar 
(Shown as Red Rectangle)"; ObjectID = "bEC-x6-7dT"; */ +"bEC-x6-7dT.text" = "移动相机以找到一个平面 
(以红色方块显示)"; -"2xt-aD-ydA.text" = "First frame rendered"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/AgoraBeauty.strings b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/AgoraBeauty.strings index 8a23ea288..75381875d 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/AgoraBeauty.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/AgoraBeauty.strings @@ -1,77 +1,59 @@ +/* Element = "label"; text = "Face Beautify"; ObjectID = "0Op-5A-oxl"; */ +"0Op-5A-oxl.text" = "美颜"; -/* Class = "UILabel"; text = "Face Beautify"; ObjectID = "0Op-5A-oxl"; */ -"0Op-5A-oxl.text" = "Face Beautify"; +/* Element = "button"; configuration.title = "Face Shape"; ObjectID = "8Rv-nY-rrP"; */ +"8Rv-nY-rrP.configuration.title" = "脸型"; -/* Class = "UILabel"; text = "Color Enhancement"; ObjectID = "5ZW-F5-wP6"; */ -"5ZW-F5-wP6.text" = "Color Enhancement"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "8Rv-nY-rrP"; */ +"8Rv-nY-rrP.normalTitle" = "截图"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "Bla-ht-C3j"; */ -"Bla-ht-C3j.title" = "Face Beautify"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "Er0-F3-9xn"; */ +"Er0-F3-9xn.title" = "美颜"; -/* Class = "UILabel"; text = "Video Denoise"; ObjectID = "Cdl-89-b0G"; */ -"Cdl-89-b0G.text" = "Video Denoise"; +/* Element = "button"; configuration.title = "Make Up"; ObjectID = "FVk-N0-Zza"; */ +"FVk-N0-Zza.configuration.title" = "美妆"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "Er0-F3-9xn"; */ -"Er0-F3-9xn.title" = "Face Beautify"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "FVk-N0-Zza"; */ +"FVk-N0-Zza.normalTitle" = "截图"; -/* Class = "UILabel"; text = "Skin Protect"; ObjectID = "J82-5l-UTm"; */ -"J82-5l-UTm.text" = "Skin Protect"; +/* Element = "label"; text = "Virtual Background"; ObjectID = "OLG-wB-DHO"; */ +"OLG-wB-DHO.text" = "虚拟背景"; -/* Class = "UILabel"; text = "Virtual Background"; ObjectID = "OLG-wB-DHO"; */ -"OLG-wB-DHO.text" = "Virtual Background"; +/* Element = "label"; text = "Sharpness"; ObjectID = "PkA-h3-Mk4"; */ +"PkA-h3-Mk4.text" = "锐利"; -/* Class = "UILabel"; text = "Sharpness"; ObjectID = "PkA-h3-Mk4"; */ -"PkA-h3-Mk4.text" = "Sharpness"; +/* Element = "segmentedControl"; segmentTitles[n] = "Image"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[0]" = "图片"; -/* Class = "UISegmentedControl"; Qhf-Ob-NYA.segmentTitles[0] = "Image"; ObjectID = "Qhf-Ob-NYA"; */ -"Qhf-Ob-NYA.segmentTitles[0]" = "Image"; +/* Element = "segmentedControl"; segmentTitles[n] = "Color"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[1]" = "颜色"; -/* Class = "UISegmentedControl"; Qhf-Ob-NYA.segmentTitles[1] = "Color"; ObjectID = "Qhf-Ob-NYA"; */ -"Qhf-Ob-NYA.segmentTitles[1]" = "Color"; +/* Element = "segmentedControl"; segmentTitles[n] = "Blur"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[2]" = "毛玻璃"; -/* Class = "UISegmentedControl"; Qhf-Ob-NYA.segmentTitles[2] = "Blur"; ObjectID = "Qhf-Ob-NYA"; */ -"Qhf-Ob-NYA.segmentTitles[2]" = "Blur"; +/* Element = "segmentedControl"; segmentTitles[n] = "Video"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[3]" = "视频"; -"Qhf-Ob-NYA.segmentTitles[3]" = "Video"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "W0Y-Vm-X3Y"; */ +"W0Y-Vm-X3Y.title" = "美颜"; -/* Class = "UILabel"; text = "Low light Enhancement"; ObjectID = "RiO-Eg-x0D"; */ -"RiO-Eg-x0D.text" = "Low Light Enhancement"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "ZSH-Zd-njr"; */ +"ZSH-Zd-njr.placeholder" = "请输入频道名"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "W0Y-Vm-X3Y"; */ -"W0Y-Vm-X3Y.title" = "Face Beautify"; +/* Element = "label"; text = "Smoothness"; ObjectID = "bOk-r8-Pn2"; */ +"bOk-r8-Pn2.text" = "平滑"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "ZSH-Zd-njr"; */ -"ZSH-Zd-njr.placeholder" = "Enter channel name"; +/* Element = "label"; text = "Redness"; ObjectID = "f1C-7c-nGG"; */ +"f1C-7c-nGG.text" = "红润"; -/* Class = "UILabel"; text = "Smoothness"; ObjectID = "bOk-r8-Pn2"; */ -"bOk-r8-Pn2.text" = "Smoothness"; +/* Element = "viewController"; title = "Simple Filter"; ObjectID = "jKA-9Y-IUF"; */ +"jKA-9Y-IUF.title" = "虚拟节拍器"; -/* Class = "UILabel"; text = "Redness"; ObjectID = "f1C-7c-nGG"; */ -"f1C-7c-nGG.text" = "Redness"; +/* Element = "label"; text = "Lightening"; ObjectID = "k4I-na-mXN"; */ +"k4I-na-mXN.text" = "美白"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "haN-2j-4b9"; */ -"haN-2j-4b9.title" = "Face Beautify"; +/* Element = "label"; text = "Whitening"; ObjectID = "nVP-eV-sen"; */ +"nVP-eV-sen.text" = "提亮"; -/* Class = "UIViewController"; title = "Simple Filter"; ObjectID = "jKA-9Y-IUF"; */ -"jKA-9Y-IUF.title" = "Simple Filter"; - -/* Class = "UILabel"; text = "Lightening"; ObjectID = "k4I-na-mXN"; */ -"k4I-na-mXN.text" = "Lightening"; - -/* Class = "UILabel"; text = "Strength"; ObjectID = "mLC-kh-u4J"; */ -"mLC-kh-u4J.text" = "Strength"; - -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "qcX-s3-yAy"; */ -"qcX-s3-yAy.title" = "Face Beautify"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "xcq-px-Ijl"; */ -"xcq-px-Ijl.normalTitle" = "Join"; - -/* Class = "UIButton"; normalTitle = "Face Shape"; ObjectID = "8Rv-nY-rrP"; */ -"8Rv-nY-rrP.normalTitle" = "Face Shape"; - -/* Class = "UIButton"; normalTitle = "Make Up"; ObjectID = "FVk-N0-Zza"; */ -"FVk-N0-Zza.normalTitle" = "Make Up"; - -/* Class = "UILabel"; text = "Whitening"; ObjectID = "nVP-eV-sen"; */ -"nVP-eV-sen.text" = "Whitening"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "xcq-px-Ijl"; */ +"xcq-px-Ijl.normalTitle" = "加入"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 7a6635018..6ab9e2105 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -1,66 +1,65 @@ +/* Element = "label"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "混音播放音量"; -/* Class = "UILabel"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ -"07c-He-s8j.text" = "Mixing Playback Volume"; +/* Element = "button"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ +"1zo-J9-vQy.normalTitle" = "暂停"; -/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ -"1zo-J9-vQy.normalTitle" = "Pause"; +/* Element = "label"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ +"4Y1-AZ-KwW.text" = "混音控制"; -/* Class = "UILabel"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ -"4Y1-AZ-KwW.text" = "Audio Mixing Controls"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ +"54l-lw-iap.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ -"54l-lw-iap.normalTitle" = "Stop"; +/* Element = "label"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ +"5o8-Cv-WLg.text" = "音效控制"; -/* Class = "UILabel"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ -"5o8-Cv-WLg.text" = "Audio Effect Controls"; +/* Element = "button"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ +"CRH-0X-9T4.normalTitle" = "恢复播放"; -/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ -"CRH-0X-9T4.normalTitle" = "Resume"; +/* Element = "label"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "混音音量"; -/* Class = "UILabel"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ -"DJt-Y7-fkM.text" = "Mixing Volume"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "button"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ +"J8R-TU-x8W.normalTitle" = "开始"; -/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ -"J8R-TU-x8W.normalTitle" = "Start"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; -/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ -"Q0E-5B-IED.text" = "Audio Scenario"; +/* Element = "label"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "混音发布音量"; -/* Class = "UILabel"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ -"VMe-lv-SUb.text" = "Mixing Publish Volume"; - -/* Class = "UILabel"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ +/* Element = "label"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ "cJ6-0Q-fAp.text" = "00 : 00"; -/* Class = "UILabel"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ -"e6E-so-zA5.text" = "Effect Volume"; +/* Element = "label"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ +"e6E-so-zA5.text" = "音效音量"; -/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ -"iUn-XK-AS2.text" = "Audio Profile"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频属性配置"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ -"jRA-VE-1PM.normalTitle" = "Resume"; +/* Element = "button"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ +"jRA-VE-1PM.normalTitle" = "恢复播放"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ -"jxp-ZN-2yG.title" = "Join Channel Audio"; +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ -"m2n-wi-5Xx.normalTitle" = "Play"; +/* Element = "button"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ +"m2n-wi-5Xx.normalTitle" = "播放"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ -"nzY-OP-Heo.normalTitle" = "Stop"; +/* Element = "button"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ +"nzY-OP-Heo.normalTitle" = "停止"; -/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ -"u26-Qh-itu.normalTitle" = "Pause"; +/* Element = "button"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ +"u26-Qh-itu.normalTitle" = "暂停"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/zh-Hans.lproj/AudioRouterPlayer.strings b/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/zh-Hans.lproj/AudioRouterPlayer.strings index bed444285..6c54d3a9b 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/zh-Hans.lproj/AudioRouterPlayer.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/zh-Hans.lproj/AudioRouterPlayer.strings @@ -1,25 +1,17 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "label"; text = "Loudspeaker"; ObjectID = "M22-MV-Wnj"; */ +"M22-MV-Wnj.text" = "扬声器"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "Iy0-Dq-h5x"; */ -"Iy0-Dq-h5x.title" = "Join Channel"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "VpM-9W-auG"; */ -"VpM-9W-auG.normalTitle" = "Button"; +/* Element = "button"; configuration.title = "Chose Player Type(ThirdParty/avplayer)"; ObjectID = "nd3-pG-lkL"; */ +"nd3-pG-lkL.configuration.title" = "选择播放器类型(第三方/AVPlayer)"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "nd3-pG-lkL"; */ +"nd3-pG-lkL.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "kf0-3f-UI5"; */ -"kf0-3f-UI5.normalTitle" = "Button"; - -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "Join Channel Video"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "wHl-zh-dFe"; */ -"wHl-zh-dFe.normalTitle" = "Button"; - -"M22-MV-Wnj.text" = "Speaker"; - -"nd3-pG-lkL.title" = "Player Type Options (ijkplayer/native)"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "视频实时通话"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/zh-Hans.lproj/AudioWaveform.strings b/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/zh-Hans.lproj/AudioWaveform.strings index f8e59a150..4cbb632b3 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/zh-Hans.lproj/AudioWaveform.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/zh-Hans.lproj/AudioWaveform.strings @@ -1,33 +1,20 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UILabel"; text = "PlaybackVolume"; ObjectID = "07c-He-s8j"; */ -"07c-He-s8j.text" = "Playback Volume"; +/* Element = "label"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; -/* Class = "UILabel"; text = "RecordingVolume"; ObjectID = "DJt-Y7-fkM"; */ -"DJt-Y7-fkM.text" = "Recording Volume"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频参数配置"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "截图"; -/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ -"Q0E-5B-IED.text" = "Audio Scenario"; +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; -/* Class = "UILabel"; text = "InEar Monitoring Volume"; ObjectID = "VMe-lv-SUb"; */ -"VMe-lv-SUb.text" = "In-ear Monitoring Volume"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ -"iUn-XK-AS2.text" = "Audio Profile"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; - -/* Class = "UILabel"; text = "InEar Monitoring"; ObjectID = "iru-5f-bbo"; */ -"iru-5f-bbo.text" = "In-ear Monitoring"; - -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ -"jxp-ZN-2yG.title" = "Join Channel Audio"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "截图"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings b/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings index 47f269c23..854a33f37 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/zh-Hans.lproj/ContentInspect.strings @@ -1,9 +1,8 @@ +/* Element = "label"; text = "Please obtain the pornographic identification results through the console webhook."; ObjectID = "9PU-6P-kJP"; */ +"9PU-6P-kJP.text" = "请通过控制台webhook获取鉴黄结果"; -/* Class = "UILabel"; text = "Use the camera point to picture and check content inspect effect"; ObjectID = "9PU-6P-kJP"; */ -"9PU-6P-kJP.text" = "Please get content moderation results through console webhook"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "Nsf-MZ-fEg"; */ +"Nsf-MZ-fEg.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "Nsf-MZ-fEg"; */ -"Nsf-MZ-fEg.normalTitle" = "Join"; - -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "PmB-t9-yOK"; */ -"PmB-t9-yOK.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "PmB-t9-yOK"; */ +"PmB-t9-yOK.placeholder" = "输入频道名"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings b/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings index 7e7bffad0..d2fc7f4b4 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings @@ -1,15 +1,14 @@ +/* Element = "textField"; placeholder = "Input Message"; ObjectID = "5E0-OO-sA5"; */ +"5E0-OO-sA5.placeholder" = "输入消息"; -/* Class = "UITextField"; placeholder = "Input Message"; ObjectID = "5E0-OO-sA5"; */ -"5E0-OO-sA5.placeholder" = "Input Message"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "HnX-Xj-hjt"; */ +"HnX-Xj-hjt.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "HnX-Xj-hjt"; */ -"HnX-Xj-hjt.placeholder" = "Enter channel name"; +/* Element = "button"; normalTitle = "Send"; ObjectID = "T9i-H1-PtG"; */ +"T9i-H1-PtG.normalTitle" = "发送"; -/* Class = "UIButton"; normalTitle = "Send"; ObjectID = "T9i-H1-PtG"; */ -"T9i-H1-PtG.normalTitle" = "Send"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "UF2-SD-j5U"; */ +"UF2-SD-j5U.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "UF2-SD-j5U"; */ -"UF2-SD-j5U.normalTitle" = "Join"; - -/* Class = "UILabel"; text = "Send Message"; ObjectID = "ey2-dt-kXq"; */ -"ey2-dt-kXq.text" = "Send Message"; +/* Element = "label"; text = "Send Message"; ObjectID = "ey2-dt-kXq"; */ +"ey2-dt-kXq.text" = "发送消息"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings index e1332f8a1..c4c41c43f 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings @@ -1,9 +1,8 @@ +/* Element = "viewController"; title = "Custom Audio Render"; ObjectID = "EbX-sK-6UJ"; */ +"EbX-sK-6UJ.title" = "音频自渲染"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "EbX-sK-6UJ"; */ -"EbX-sK-6UJ.title" = "Custom Audio Render"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings index d1f524d0d..868bfaafb 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings @@ -1,9 +1,8 @@ +/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "FCW-Np-auB"; */ +"FCW-Np-auB.title" = "音频自采集"; -/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "FCW-Np-auB"; */ -"FCW-Np-auB.title" = "Custom Audio Source"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings index b51ebfa81..457384b5c 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings @@ -1,18 +1,11 @@ +/* Element = "label"; text = "Push Microphone"; ObjectID = "3Sp-t2-4ZJ"; */ +"3Sp-t2-4ZJ.text" = "发布麦克风"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "6VE-rp-0Wl"; */ -"6VE-rp-0Wl.normalTitle" = "Join Channel"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "6VE-rp-0Wl"; */ +"6VE-rp-0Wl.normalTitle" = "加入频道"; -/* Class = "UILabel"; text = "Microphone"; ObjectID = "Cwr-m3-kGh"; */ -"Cwr-m3-kGh.text" = "Microphone"; +/* Element = "label"; text = "Push PCM"; ObjectID = "Dbo-LJ-5lv"; */ +"Dbo-LJ-5lv.text" = "发布本地音频"; -/* Class = "UILabel"; text = "Push PCM"; ObjectID = "Dbo-LJ-5lv"; */ -"Dbo-LJ-5lv.text" = "Publish Local Audio"; - -/* Class = "UILabel"; text = "Push Microphone"; ObjectID = "3Sp-t2-4ZJ"; */ -"3Sp-t2-4ZJ.text" = "Publish Microphone"; - -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "SdL-FU-6ht"; */ -"SdL-FU-6ht.placeholder" = "Enter channel name"; - -/* Class = "UILabel"; text = "Play Audio"; ObjectID = "mZd-9B-Gf5"; */ -"mZd-9B-Gf5.text" = "Play Local Audio"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "SdL-FU-6ht"; */ +"SdL-FU-6ht.placeholder" = "输入频道名"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings index d9691fd28..2ad5dde5d 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings @@ -1,12 +1,11 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "navigationItem"; title = "Join Channel"; ObjectID = "ZgN-iF-qYr"; */ +"ZgN-iF-qYr.title" = "加入频道"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "ZgN-iF-qYr"; */ -"ZgN-iF-qYr.title" = "Join Channel"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "aGp-ad-ObV"; */ +"aGp-ad-ObV.title" = "视频自渲染(Metal)"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "aGp-ad-ObV"; */ -"aGp-ad-ObV.title" = "Custom Video Render (Metal)"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings index 81b5ac1e2..940bf8343 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings @@ -1,12 +1,14 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "navigationItem"; title = "Join Channel"; ObjectID = "a4k-1t-KLv"; */ +"a4k-1t-KLv.title" = "加入频道"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "a4k-1t-KLv"; */ -"a4k-1t-KLv.title" = "Join Channel"; +/* Element = "label"; text = "HDR"; ObjectID = "gC7-TR-7LA"; */ +"gC7-TR-7LA.text" = "HDR"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "pjq-Wh-4Ys"; */ -"pjq-Wh-4Ys.title" = "Custom Video Source (Push)"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "pjq-Wh-4Ys"; */ +"pjq-Wh-4Ys.title" = "视频自采集(Push)"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings index 282d5d185..5c60657dc 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings @@ -1,16 +1,29 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "button"; configuration.title = "Create VideoTrack"; ObjectID = "Her-yf-kmE"; */ +"Her-yf-kmE.configuration.title" = "创建采集Track"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "a4k-1t-KLv"; */ -"a4k-1t-KLv.title" = "Join Channel"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "Her-yf-kmE"; */ +"Her-yf-kmE.normalTitle" = "截图"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "navigationItem"; title = "Join Channel"; ObjectID = "a4k-1t-KLv"; */ +"a4k-1t-KLv.title" = "加入频道"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "pjq-Wh-4Ys"; */ -"pjq-Wh-4Ys.title" = "Join Channel Video"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -"xi9-eb-Hiz.normalTitle" = "Destroy Track"; -"zBJ-mw-TwL.normalTitle" = "Create Capture Track (Encoded)"; -"Her-yf-kmE.normalTitle" = "Create Capture Track"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "pjq-Wh-4Ys"; */ +"pjq-Wh-4Ys.title" = "视频实时通话"; + +/* Element = "button"; configuration.title = "Destory Track"; ObjectID = "xi9-eb-Hiz"; */ +"xi9-eb-Hiz.configuration.title" = "销毁Track"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "xi9-eb-Hiz"; */ +"xi9-eb-Hiz.normalTitle" = "截图"; + +/* Element = "button"; configuration.title = "Create Video Track(Encode)"; ObjectID = "zBJ-mw-TwL"; */ +"zBJ-mw-TwL.configuration.title" = "创建采集Track(编码)"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "zBJ-mw-TwL"; */ +"zBJ-mw-TwL.normalTitle" = "截图"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings b/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings index 435447453..6a9d55c5c 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings @@ -1,21 +1,8 @@ +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "Iy0-Dq-h5x"; */ -"Iy0-Dq-h5x.title" = "Join Channel"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "VpM-9W-auG"; */ -"VpM-9W-auG.normalTitle" = "Button"; - -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "kf0-3f-UI5"; */ -"kf0-3f-UI5.normalTitle" = "Button"; - -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "Video Call"; - -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "wHl-zh-dFe"; */ -"wHl-zh-dFe.normalTitle" = "Button"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "视频实时通话"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings b/iOS/APIExample/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings index fcf4bf26d..f95424a48 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings @@ -1,12 +1,20 @@ +/* Element = "viewController"; title = "Join Multiple Channel"; ObjectID = "4JZ-MT-fZb"; */ +"4JZ-MT-fZb.title" = "加入多频道"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "4JZ-MT-fZb"; */ -"4JZ-MT-fZb.title" = "Join Multiple Channels"; +/* Element = "button"; configuration.title = "take Snapshot Ex"; ObjectID = "75n-CG-1tV"; */ +"75n-CG-1tV.configuration.title" = "截图"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "BpR-ES-aVX"; */ -"BpR-ES-aVX.title" = "Join Channel"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "75n-CG-1tV"; */ +"75n-CG-1tV.normalTitle" = "截图"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join"; +/* Element = "button"; configuration.title = "Leave ChannelEx"; ObjectID = "Rad-Lg-o3J"; */ +"Rad-Lg-o3J.configuration.title" = "离开Ex频道"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "Rad-Lg-o3J"; */ +"Rad-Lg-o3J.normalTitle" = "截图"; + +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.storyboard b/iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/Base.lproj/KtvCopyrightMusic.storyboard similarity index 96% rename from iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.storyboard rename to iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/Base.lproj/KtvCopyrightMusic.storyboard index 80725f939..0b0cf66a4 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.storyboard +++ b/iOS/APIExample/APIExample/Examples/Advanced/KtvCopyrightMusic/Base.lproj/KtvCopyrightMusic.storyboard @@ -19,8 +19,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macOS/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift b/macOS/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift deleted file mode 100644 index 64d401ab2..000000000 --- a/macOS/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift +++ /dev/null @@ -1,485 +0,0 @@ -// -// JoinChannelVC.swift -// APIExample -// -// Created by 张乾泽 on 2020/4/17. -// Copyright © 2020 Agora Corp. All rights reserved. -// -import Cocoa -import AgoraRtcKit -import AGEVideoLayout - -class FaceCaptureMain: BaseViewController { - - var agoraKit: AgoraRtcEngineKit! - - var videos: [VideoView] = [] - @IBOutlet weak var Container: AGEVideoContainer! - - /** - --- Cameras Picker --- - */ - @IBOutlet weak var selectCameraPicker: Picker! - var cameras: [AgoraRtcDeviceInfo] = [] { - didSet { - DispatchQueue.main.async { - self.selectCameraPicker.picker.addItems(withTitles: self.cameras.map {$0.deviceName ?? "unknown"}) - } - } - } - var selectedCamera: AgoraRtcDeviceInfo? { - let index = selectCameraPicker.indexOfSelectedItem - if index >= 0 && index < cameras.count { - return cameras[index] - } else { - return nil - } - } - func initSelectCameraPicker() { - selectCameraPicker.label.stringValue = "Camera".localized - // find device in a separate thread to avoid blocking main thread - let queue = DispatchQueue(label: "device.enumerateDevices") - queue.async {[unowned self] in - self.cameras = self.agoraKit.enumerateDevices(.videoCapture) ?? [] - } - - selectCameraPicker.onSelectChanged { - if !self.isJoined { - return - } - // use selected devices - guard let cameraId = self.selectedCamera?.deviceId else { - return - } - self.agoraKit.setDevice(.videoCapture, deviceId: cameraId) - } - } - - /** - --- Resolutions Picker --- - */ - @IBOutlet weak var selectResolutionPicker: Picker! - var selectedResolution: Resolution? { - let index = self.selectResolutionPicker.indexOfSelectedItem - if index >= 0 && index < Configs.Resolutions.count { - return Configs.Resolutions[index] - } else { - return nil - } - } - func initSelectResolutionPicker() { - selectResolutionPicker.label.stringValue = "Resolution".localized - selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) - selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) - - selectResolutionPicker.onSelectChanged { - if !self.isJoined { - return - } - - guard let resolution = self.selectedResolution, - let fps = self.selectedFps else { - return - } - self.agoraKit.setVideoEncoderConfiguration( - AgoraVideoEncoderConfiguration( - size: resolution.size(), - frameRate: 15, - bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative, - mirrorMode: AgoraVideoMirrorMode.auto - ) - ) - } - } - - /** - --- Fps Picker --- - */ - @IBOutlet weak var selectFpsPicker: Picker! - var selectedFps: Int? { - let index = self.selectFpsPicker.indexOfSelectedItem - if index >= 0 && index < Configs.Fps.count { - return Configs.Fps[index] - } else { - return nil - } - } - func initSelectFpsPicker() { - selectFpsPicker.label.stringValue = "Frame Rate".localized - selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) - selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) - - selectFpsPicker.onSelectChanged { - if !self.isJoined { - return - } - - guard let resolution = self.selectedResolution, - let fps = self.selectedFps else { - return - } - self.agoraKit.setVideoEncoderConfiguration( - AgoraVideoEncoderConfiguration( - size: resolution.size(), - frameRate: 15, - bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative, - mirrorMode: .auto - ) - ) - } - } - - /** - --- Microphones Picker --- - */ - @IBOutlet weak var selectMicsPicker: Picker! - var mics: [AgoraRtcDeviceInfo] = [] { - didSet { - DispatchQueue.main.async { - self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) - } - } - } - var selectedMicrophone: AgoraRtcDeviceInfo? { - let index = self.selectMicsPicker.indexOfSelectedItem - if index >= 0 && index < mics.count { - return mics[index] - } else { - return nil - } - } - func initSelectMicsPicker() { - selectMicsPicker.label.stringValue = "Microphone".localized - // find device in a separate thread to avoid blocking main thread - let queue = DispatchQueue(label: "device.enumerateDevices") - queue.async {[unowned self] in - self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] - } - - selectMicsPicker.onSelectChanged { - if !self.isJoined { - return - } - // use selected devices - guard let micId = self.selectedMicrophone?.deviceId else { - return - } - self.agoraKit.setDevice(.audioRecording, deviceId: micId) - } - } - - /** - --- Channel TextField --- - */ - @IBOutlet weak var channelField: Input! - func initChannelField() { - channelField.label.stringValue = "Channel".localized - channelField.field.placeholderString = "Channel Name".localized - } - - /** - --- Button --- - */ - @IBOutlet weak var joinChannelButton: NSButton! - func initJoinChannelButton() { - joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized - } - - // indicate if current instance has joined channel - var isJoined: Bool = false { - didSet { - channelField.isEnabled = !isJoined - initJoinChannelButton() - agoraKit.setVideoFrameDelegate(isJoined ? self : nil) - agoraKit.setFaceInfoDelegate(isJoined ? self : nil) - } - } - - // indicate for doing something - var isProcessing: Bool = false { - didSet { - joinChannelButton.isEnabled = !isProcessing - } - } - - override func viewDidLoad() { - super.viewDidLoad() - // Do view setup here. - let config = AgoraRtcEngineConfig() - config.appId = KeyCenter.AppId - config.areaCode = GlobalSettings.shared.area - - agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) - // Configuring Privatization Parameters - Util.configPrivatization(agoraKit: agoraKit) - agoraKit.enableVideo() - if (KeyCenter.FaceCaptureLicense ?? "").isEmpty { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.showAlert(message: "Please contact Agora customer service to obtain a face capture certificate".localized) - } - } else { - // enable face capture - agoraKit.enableExtension(withVendor: "agora_video_filters_face_capture", - extension: "face_capture", - enabled: true, - sourceType: .primaryCamera) - - agoraKit.setExtensionPropertyWithVendor("agora_video_filters_face_capture", - extension: "face_capture", - key: "authentication_information", - value: "{\"company_id\":\"agoraTest\"," + - "\"license\":\"" + (KeyCenter.FaceCaptureLicense ?? "") + "\"}", - sourceType: .primaryCamera) - - agoraKit.enableExtension(withVendor: "agora_filters_lip_sync", - extension: "lip_sync", - enabled: true, - sourceType: .speechDriven) - agoraKit.setExtensionPropertyWithVendor("agora_filters_lip_sync", - extension: "lip_sync", - key: "parameters", - value: "{\"company_id\":\"agoraTest\", \"license\":\"abc\", \"open_agc\":true}", - sourceType: .speechDriven) - } - - initSelectCameraPicker() - initSelectResolutionPicker() - initSelectFpsPicker() - initSelectMicsPicker() - initChannelField() - initJoinChannelButton() - layoutVideos(1) - } - - func layoutVideos(_ count: Int) { - videos = [] - for i in 0...count - 1 { - let view = VideoView.createFromNib()! - if(i == 0) { - view.placeholder.stringValue = "Local" - view.type = .local - view.statsInfo = StatisticsInfo(type: .local(StatisticsInfo.LocalInfo())) - } else { - view.placeholder.stringValue = "Remote \(i)" - view.type = .remote - view.statsInfo = StatisticsInfo(type: .remote(StatisticsInfo.RemoteInfo())) - } - videos.append(view) - } - // layout render view - Container.layoutStream(views: videos) - } - - @IBAction func onVideoCallButtonPressed(_ sender: NSButton) { - if !isJoined { - // check configuration - let channel = channelField.stringValue - if channel.isEmpty { - return - } - guard let cameraId = selectedCamera?.deviceId, - let resolution = selectedResolution, - let micId = selectedMicrophone?.deviceId, - let fps = selectedFps else { - return - } - - // set proxy configuration -// let proxySetting = GlobalSettings.shared.proxySetting.selectedOption().value -// agoraKit.setCloudProxy(AgoraCloudProxyType.init(rawValue: UInt(proxySetting)) ?? .noneProxy) - - agoraKit.setDevice(.videoCapture, deviceId: cameraId) - agoraKit.setDevice(.audioRecording, deviceId: micId) - // set myself as broadcaster to stream video/audio - agoraKit.setClientRole(.broadcaster) - agoraKit.setVideoEncoderConfiguration( - AgoraVideoEncoderConfiguration( - size: resolution.size(), - frameRate: 15, - bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative, - mirrorMode: .auto - ) - ) - - // set up local video to render your local camera preview - let localVideo = videos[0] - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = 0 - // the view to be binded - videoCanvas.view = localVideo.videocanvas - videoCanvas.renderMode = .hidden - agoraKit.setupLocalVideo(videoCanvas) - // you have to call startPreview to see local video - agoraKit.startPreview() - - // start joining channel - // 1. Users can only see each other after they join the - // same channel successfully using the same app id. - // 2. If app certificate is turned on at dashboard, token is needed - // when joining channel. The channel name and uid used to calculate - // the token has to match the ones used for channel join - isProcessing = true - let option = AgoraRtcChannelMediaOptions() - option.publishCameraTrack = false - option.publishMicrophoneTrack = false - option.clientRoleType = .broadcaster - NetworkManager.shared.generateToken(channelName: channel, success: { token in - let result = self.agoraKit.joinChannel(byToken: token, channelId: channel, uid: 0, mediaOptions: option) - if result != 0 { - self.isProcessing = false - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code - self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") - } - }) - } else { - isProcessing = true - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = 0 - // the view to be binded - videoCanvas.view = nil - videoCanvas.renderMode = .hidden - agoraKit.setupLocalVideo(videoCanvas) - agoraKit.leaveChannel { (stats:AgoraChannelStats) in - LogUtils.log(message: "Left channel", level: .info) - self.isProcessing = false - self.videos[0].uid = nil - self.isJoined = false - self.videos.forEach { - $0.uid = nil - $0.statsLabel.stringValue = "" - } - } - } - } - - override func viewWillBeRemovedFromSplitView() { - if isJoined { - agoraKit.disableVideo() - agoraKit.leaveChannel { (stats:AgoraChannelStats) in - LogUtils.log(message: "Left channel", level: .info) - } - } - AgoraRtcEngineKit.destroy() - } -} - -extension FaceCaptureMain: AgoraFaceInfoDelegate { - func onFaceInfo(_ outFaceInfo: String) -> Bool { - videos[0].statsInfo?.updateMetaInfo(data: outFaceInfo) - return true - } -} - -extension FaceCaptureMain: AgoraVideoFrameDelegate { - func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool { - let info = videoFrame.metaInfo["KEY_FACE_CAPTURE"] as? String - videos[0].statsInfo?.updateMetaInfo(data: info) - LogUtils.log(message: info ?? "", level: .info) - return true - } - func getVideoFrameProcessMode() -> AgoraVideoFrameProcessMode { - .readWrite - } - func getObservedFramePosition() -> AgoraVideoFramePosition { - .postCapture - } -} - -/// agora rtc engine delegate events -extension FaceCaptureMain: AgoraRtcEngineDelegate { - /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out - /// what is happening - /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// @param warningCode warning code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { - LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) - } - - /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand - /// to let user know something wrong is happening - /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code - /// @param errorCode error code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "error: \(errorCode)", level: .error) - if self.isProcessing { - self.isProcessing = false - } - self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") - } - - /// callback when the local user joins a specified channel. - /// @param channel - /// @param uid uid of local user - /// @param elapsed time elapse since current sdk instance join the channel in ms - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - isProcessing = false - isJoined = true - let localVideo = videos[0] - localVideo.uid = uid - LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) - } - - /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param elapsed time elapse since current sdk instance join the channel in ms - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) - - } - - /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param reason reason why this user left, note this event may be triggered when the remote user - /// become an audience in live broadcasting profile - func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) - - // to unlink your view from sdk, so that your view reference will be released - // note the video will stay at its last frame, to completely remove it - // you will need to remove the EAGL sublayer from your binded view - } - - /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { - videos[0].statsInfo?.updateChannelStats(stats) - } - - /// Reports the statistics of the uploading local video streams once every two seconds. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, localVideoStats stats: AgoraRtcLocalVideoStats, sourceType:AgoraVideoSourceType) { - videos[0].statsInfo?.updateLocalVideoStats(stats) - } - - /// Reports the statistics of the uploading local audio streams once every two seconds. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { - videos[0].statsInfo?.updateLocalAudioStats(stats) - } - - /// Reports the statistics of the video stream from each remote user/host. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) { - videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateVideoStats(stats) - } - - /// Reports the statistics of the audio stream from each remote user/host. - /// @param stats stats struct for current call statistics - func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { - videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateAudioStats(stats) - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, localVideoStateChangedOf state: AgoraVideoLocalState, reason: AgoraLocalVideoStreamReason, sourceType: AgoraVideoSourceType) { - LogUtils.log(message: "AgoraRtcEngineKit state: \(state), reason \(reason.rawValue)", level: .info) - } -} diff --git a/macOS/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings b/macOS/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings deleted file mode 100644 index 29b0bdebb..000000000 --- a/macOS/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ -"BP9-4w-AfJ.title" = "Box"; - -/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ -"Lhu-U1-6qh.title" = "加入频道"; - -/* Element = "viewController"; title = "Face Capture"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "面捕数据"; diff --git a/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift b/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift index e695db3d0..d892920bc 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift @@ -514,14 +514,18 @@ class LiveStreamingMain: BaseViewController { private func onWaterMark(_ isOn: Bool) { if isOn { if let filepath = Bundle.main.path(forResource: "agora-logo", ofType: "png") { - if let url = URL(string: filepath) { - let waterMark = WatermarkOptions() - waterMark.visibleInPreview = true - let localVideo = self.videos[0] - waterMark.positionInPortraitMode = localVideo.frame.offsetBy(dx: 20, dy: 20) - waterMark.positionInLandscapeMode = localVideo.frame.offsetBy(dx: 20, dy: 20) - agoraKit.addVideoWatermark(url, options: waterMark) - } + let options = WatermarkOptions() + options.visibleInPreview = true + let localVideo = self.videos[0] + options.positionInPortraitMode = localVideo.frame.offsetBy(dx: 20, dy: 20) + options.positionInLandscapeMode = localVideo.frame.offsetBy(dx: 20, dy: 20) + + let config = WatermarkConfig() + config.id = UUID().uuidString.replacingOccurrences(of: "-", with: "") + config.type = .image + config.imageUrl = URL(fileURLWithPath: filepath) + config.options = options + agoraKit.addVideoWatermark(config) } } else { agoraKit.clearVideoWatermarks() diff --git a/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift b/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift index 447b92828..d07b05d3f 100644 --- a/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift +++ b/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift @@ -542,8 +542,8 @@ extension SimpleFilterMain: AgoraRtcEngineDelegate { } -extension SimpleFilterMain: AgoraMediaFilterEventDelegate{ - func onEvent(_ provider: String?, extension: String?, key: String?, value: String?) { +extension SimpleFilterMain: AgoraMediaFilterEventDelegate { + func onEventWithContext(_ context: AgoraExtensionContext, key: String?, value: String?) { LogUtils.log(message: "onEvent: \(String(describing: key)) \(String(describing: value))", level: .info) } } diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift index 3961ea597..a2fdae360 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift @@ -53,7 +53,7 @@ class JoinChannelAudioMain: BaseViewController { var audioScenarios = AgoraAudioScenario.allValues() var selectedAudioScenario: AgoraAudioScenario? { let index = self.selectAudioScenarioPicker.indexOfSelectedItem - if index >= 0 && index < Configs.Resolutions.count { + if index >= 0 && index < audioScenarios.count { return audioScenarios[index] } else { return nil diff --git a/macOS/APIExample/ViewController.swift b/macOS/APIExample/ViewController.swift index f9ee332c3..f79cb8272 100644 --- a/macOS/APIExample/ViewController.swift +++ b/macOS/APIExample/ViewController.swift @@ -53,7 +53,6 @@ class MenuController: NSViewController { MenuItem(name: "Spatial Audio".localized, identifier: "menuCell", controller: "SpatialAudio", storyboard: "SpatialAudio"), MenuItem(name: "Content Inspect".localized, identifier: "menuCell", controller: "ContentInspect", storyboard: "ContentInspect"), MenuItem(name: "Multi Camera Sourece".localized, identifier: "menuCell", controller: "MultiCameraSourece", storyboard: "MultiCameraSourece"), - MenuItem(name: "Face Capture".localized, identifier: "menuCell", controller: "FaceCapture", storyboard: "FaceCapture"), MenuItem(name: "URL Streaming(RTE Player)".localized, identifier: "menuCell", controller: "RtePlayer", storyboard: "RtePlayer"), MenuItem(name: "Simulcast".localized, identifier: "menuCell", controller: "Simulcast", storyboard: "Simulcast"), MenuItem(name: "Multipath".localized, identifier: "menuCell", controller: "Multipath", storyboard: "Multipath"), @@ -146,4 +145,3 @@ class ViewController: NSViewController { } } } - diff --git a/macOS/APIExample/zh-Hans.lproj/Localizable.strings b/macOS/APIExample/zh-Hans.lproj/Localizable.strings index 5ccd30b8d..07cc24609 100644 --- a/macOS/APIExample/zh-Hans.lproj/Localizable.strings +++ b/macOS/APIExample/zh-Hans.lproj/Localizable.strings @@ -59,6 +59,10 @@ "Chat Room Gaming" = "娱乐语聊房"; "Education" = "教育"; "Game Streaming" = "高音质语聊房"; +"Chat Room" = "语聊房"; +"Chorus" = "实时合唱"; +"Meeting" = "会议场景"; +"AI Client" = "对话式 AI 客户端"; "Chat Room Entertainment" = "游戏开黑"; "Show Room" = "秀场"; "Cancel" = "取消"; @@ -216,8 +220,6 @@ "AGGRESSIVE" = "强降噪模式"; "BALANCED" = "(默认)均衡降噪模式"; "ULTRALOWLATENCY" = "低延时强降噪模式"; -"Face Capture" = "面捕数据"; -"Please contact Agora customer service to obtain a face capture certificate" = "请联系 Agora 客服获取面捕证书"; "Start Video Echo Test" = "开始视频回路测试"; "Stop Video Echo Test" = "停止视频回路测试"; diff --git a/macOS/ARCHITECTURE.md b/macOS/ARCHITECTURE.md index 27f5612dd..e44673345 100644 --- a/macOS/ARCHITECTURE.md +++ b/macOS/ARCHITECTURE.md @@ -79,12 +79,12 @@ All examples share utilities from `APIExample/Common/`: | Case | Path | Key APIs | Description | |------|------|----------|-------------| -| JoinChannelAudio | `Examples/Basic/JoinChannelAudio/` | `createAgoraRtcEngine()`, `joinChannel()`, `leaveChannel()`, `destroy()` | Basic audio call — join channel and manage audio stream | +| JoinChannelAudio | `Examples/Basic/JoinChannelAudio/` | `createAgoraRtcEngine()`, `setAudioScenario()`, `joinChannel()`, `leaveChannel()`, `destroy()` | Basic audio call — select an audio scenario, join a channel, and manage the audio stream | | JoinChannelVideo | `Examples/Basic/JoinChannelVideo/` | `createAgoraRtcEngine()`, `joinChannel()`, `setupLocalVideo()`, `setupRemoteVideo()`, `leaveChannel()`, `destroy()` | Basic video call — join channel and render local/remote video | | JoinChannelVideo(Token) | `Examples/Basic/JoinChannelVideo(Token)/` | `createAgoraRtcEngine()`, `joinChannel()` with token, `setupLocalVideo()`, `setupRemoteVideo()` | Video call with token authentication | | JoinChannelVideo(Recorder) | `Examples/Basic/JoinChannelVideo(Recorder)/` | `createAgoraRtcEngine()`, `joinChannel()`, `startAudioRecording()`, `stopAudioRecording()` | Video call with local audio recording | | AgoraBeauty | `Examples/Advanced/AgoraBeauty/` | `createVideoEffectObject()`, `setVideoEffectStringParam()`, `enableVirtualBackground()` | Beauty filter and enhancement effects | -| AudioMixing | `Examples/Advanced/AudioMixing/` | `startAudioMixing()`, `stopAudioMixing()`, `pauseAudioMixing()`, `resumeAudioMixing()` | Audio file mixing and playback control | +| AudioMixing | `Examples/Advanced/AudioMixing/` | `setAudioScenario()`, `startAudioMixing()`, `stopAudioMixing()`, `pauseAudioMixing()`, `resumeAudioMixing()` | Audio scenario selection, file mixing, and playback control | | ChannelMediaRelay | `Examples/Advanced/ChannelMediaRelay/` | `startChannelMediaRelay()`, `updateChannelMediaRelay()`, `stopChannelMediaRelay()` | Relay media streams across multiple channels | | ContentInspect | `Examples/Advanced/ContentInspect/` | `enableContentInspect()`, `disableContentInspect()` | Content inspection and moderation | | CreateDataStream | `Examples/Advanced/CreateDataStream/` | `createDataStream()`, `sendStreamMessage()` | Custom data stream creation and messaging | @@ -94,9 +94,8 @@ All examples share utilities from `APIExample/Common/`: | CustomVideoSourceMediaIO | `Examples/Advanced/CustomVideoSourceMediaIO/` | `setExternalVideoSource()`, `pushVideoFrame()` with MediaIO | Custom video source with media I/O | | CustomVideoSourcePush | `Examples/Advanced/CustomVideoSourcePush/` | `setExternalVideoSource()`, `pushVideoFrame()` | Custom video source push | | CustomVideoSourcePushMulti | `Examples/Advanced/CustomVideoSourcePushMulti/` | `setExternalVideoSource()`, `pushVideoFrame()` with multiple sources | Multiple custom video sources | -| FaceCapture | `Examples/Advanced/FaceCapture/` | `enableFaceDetection()`, `getFaceDetectionResult()` | Face detection and capture | | JoinMultiChannel | `Examples/Advanced/JoinMultiChannel/` | `createRtcChannel()`, `joinChannel()` on multiple channels | Join and manage multiple channels simultaneously | -| LiveStreaming | `Examples/Advanced/LiveStreaming/` | `setClientRole()`, `startRtmpStreamWithTranscoding()`, `stopRtmpStream()` | RTMP live streaming with transcoding | +| LiveStreaming | `Examples/Advanced/LiveStreaming/` | `setClientRole()`, `addVideoWatermark()` with `WatermarkConfig`, `startRtmpStreamWithTranscoding()`, `stopRtmpStream()` | Live streaming with watermark configuration and RTMP transcoding | | LocalVideoTranscoding | `Examples/Advanced/LocalVideoTranscoding/` | `startLocalVideoTranscoding()`, `updateLocalTranscodingConfig()`, `stopLocalVideoTranscoding()` | Local video transcoding and composition | | MediaPlayer | `Examples/Advanced/MediaPlayer/` | `createMediaPlayer()`, `open()`, `play()`, `pause()`, `stop()` | Media file playback and control | | MultiCameraSourece | `Examples/Advanced/MultiCameraSourece/` | `enumerateDevices()`, `setDevice()` with multiple cameras | Multiple camera source selection | @@ -109,7 +108,7 @@ All examples share utilities from `APIExample/Common/`: | RtePlayer | `Examples/Advanced/RtePlayer/` | `createMediaPlayer()`, `open()` with RTE protocol | RTE protocol media playback | | RTMPStreaming | `Examples/Advanced/RTMPStreaming/` | `startRtmpStreamWithTranscoding()`, `updateRtmpTranscodingConfig()`, `stopRtmpStream()` | RTMP streaming with live transcoding | | ScreenShare | `Examples/Advanced/ScreenShare/` | `startScreenCapture()`, `updateScreenCaptureParameters()`, `stopScreenCapture()` | Screen sharing and capture | -| SimpleFilter | `Examples/Advanced/SimpleFilter/` | `setVideoEncoderConfiguration()`, `setBeautyEffectOptions()` | Simple video filter effects | +| SimpleFilter | `Examples/Advanced/SimpleFilter/` | `enableExtension()`, `setExtensionPropertyWithVendor()`, `onEventWithContext()` | Simple audio and video filter extensions with context-aware event callbacks | | Simulcast | `Examples/Advanced/Simulcast/` | `setSimulcastConfig()`, `enableSimulcast()` | Simulcast streaming with multiple bitrates | | SpatialAudio | `Examples/Advanced/SpatialAudio/` | `getLocalSpatialAudioEngine()`, `updateSelfPosition()`, `updateRemotePosition()` | 3D spatial audio positioning | | StreamEncryption | `Examples/Advanced/StreamEncryption/` | `enableEncryption()`, `setEncryptionConfig()` | Stream encryption and security | From 2bb3f14c1a8b38c6de9b53b4f0defd78f5b99113 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Fri, 7 Aug 2026 17:28:14 +0800 Subject: [PATCH 20/49] chore(android): hide deprecated rhythm player examples --- Android/APIExample-Audio/ARCHITECTURE.md | 4 ++-- .../io/agora/api/example/examples/advanced/RhythmPlayer.java | 4 ++-- Android/APIExample-Compose/ARCHITECTURE.md | 4 ++-- .../main/java/io/agora/api/example/compose/model/Examples.kt | 4 +--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Android/APIExample-Audio/ARCHITECTURE.md b/Android/APIExample-Audio/ARCHITECTURE.md index 1aab8f934..0d6697358 100644 --- a/Android/APIExample-Audio/ARCHITECTURE.md +++ b/Android/APIExample-Audio/ARCHITECTURE.md @@ -51,7 +51,7 @@ APIExample-Audio/ │ │ ├── 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 + │ │ ├── RhythmPlayer.java # Hidden — APIs deprecated since RTC SDK 4.6.0 │ │ └── SpatialSound.java # [17] "Spatial Audio" — 3D spatial audio │ └── audio/ # Audio-specific cases (grouped as ADVANCED) │ └── AudioWaveform.java # [18] "Audio Waveform" — audio visualization @@ -79,7 +79,7 @@ APIExample-Audio/ | 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 | +| Rhythm Player (hidden) | `advanced/RhythmPlayer.java` | `startRhythmPlayer()`, `stopRhythmPlayer()`, `enableAudioVolumeIndication()` | Source retained for reference; hidden because the APIs are deprecated since RTC SDK 4.6.0 | | 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 | 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 c87d5c383..e64171380 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 @@ -36,13 +36,13 @@ /** * This demo demonstrates how to make a VideoProcessExtension */ -@Example( +/*@Example( index = 16, group = ADVANCED, name = R.string.item_rhythmplayer, actionId = R.id.action_mainFragment_rhythm_player, tipsId = R.string.rhythmplayer -) +)*/ public class RhythmPlayer extends BaseFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener { private static final String TAG = RhythmPlayer.class.getSimpleName(); diff --git a/Android/APIExample-Compose/ARCHITECTURE.md b/Android/APIExample-Compose/ARCHITECTURE.md index bf757f933..d732ef645 100644 --- a/Android/APIExample-Compose/ARCHITECTURE.md +++ b/Android/APIExample-Compose/ARCHITECTURE.md @@ -55,7 +55,7 @@ APIExample-Compose/ │ ├── 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 + │ ├── RhythmPlayer.kt # Hidden — APIs deprecated since RTC SDK 4.6.0 │ ├── 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 @@ -117,7 +117,7 @@ APIExample-Compose/ | 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 | +| Rhythm Player (hidden) | `RhythmPlayer.kt` | `joinChannel()`, `startRhythmPlayer()`, `stopRhythmPlayer()`, `updateChannelMediaOptions()` | Source retained for reference; hidden because the APIs are deprecated since RTC SDK 4.6.0 | | 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 | diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt index 50b4f63f5..d93611bd0 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt @@ -24,7 +24,6 @@ import io.agora.api.example.compose.samples.PictureInPicture import io.agora.api.example.compose.samples.PlayAudioFiles import io.agora.api.example.compose.samples.PreCallTest import io.agora.api.example.compose.samples.RTMPStreaming -import io.agora.api.example.compose.samples.RhythmPlayer import io.agora.api.example.compose.samples.ScreenSharing import io.agora.api.example.compose.samples.SendDataStream import io.agora.api.example.compose.samples.SpatialSound @@ -63,9 +62,8 @@ val AdvanceExampleList = listOf( Example(R.string.example_mediaplayer) { MediaPlayer() }, Example(R.string.example_screensharing) { ScreenSharing() }, Example(R.string.example_videoprocessextension) { VideoProcessExtension() }, - Example(R.string.example_rhythmplayer) { RhythmPlayer() }, Example(R.string.example_localvideotranscoding) { LocalVideoTranscoding() }, Example(R.string.example_senddatastream) { SendDataStream() }, Example(R.string.example_hostacrosschannel) { HostAcrossChannel() }, Example(R.string.example_spatialsound) { SpatialSound() }, -) \ No newline at end of file +) From 15034f985e4758e6b7a646bb60309e0372570d43 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Fri, 7 Aug 2026 17:37:12 +0800 Subject: [PATCH 21/49] chore(ios): remove deprecated face capture examples --- .github/ci/build/modify_ios_keycenter.py | 6 - cicd/scripts/keycenter.py | 1 + .../APIExample-Audio/ViewController.swift | 1 - iOS/APIExample-Audio/ARCHITECTURE.md | 4 +- .../APIExample-OC/ViewController.m | 1 - iOS/APIExample-OC/ARCHITECTURE.md | 4 +- .../project.pbxproj | 16 - .../APIExample-SwiftUI/Common/KeyCenter.swift | 3 - .../APIExample-SwiftUI/ContentView.swift | 6 - .../Advanced/FaceCapture/FaceCapture.swift | 71 ---- .../Advanced/FaceCapture/FaceCaptureRTC.swift | 207 ------------ .../Resources/en.lproj/Localizable.strings | 3 - .../zh-Hans.lproj/Localizable.strings | 3 - iOS/APIExample-SwiftUI/ARCHITECTURE.md | 8 +- .../APIExample.xcodeproj/project.pbxproj | 26 -- .../APIExample/Common/KeyCenter.swift | 1 - .../Base.lproj/FaceCapture.storyboard | 100 ------ .../Advanced/FaceCapture/FaceCapture.swift | 310 ------------------ .../zh-Hans.lproj/FaceCapture.strings | 8 - .../APIExample/ViewController.swift | 1 - .../zh-Hans.lproj/Localizable.strings | 2 - iOS/APIExample/ARCHITECTURE.md | 2 - 22 files changed, 7 insertions(+), 777 deletions(-) delete mode 100644 iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCapture.swift delete mode 100644 iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCaptureRTC.swift delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift delete mode 100644 iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings diff --git a/.github/ci/build/modify_ios_keycenter.py b/.github/ci/build/modify_ios_keycenter.py index d80c8c23d..784833a4d 100644 --- a/.github/ci/build/modify_ios_keycenter.py +++ b/.github/ci/build/modify_ios_keycenter.py @@ -2,7 +2,6 @@ def modfiy(path, isReset): appId = os.environ.get('APP_ID') - faceCaptureLicense = os.environ.get('FACE_CAPTURE_LICENSE') with open(path, 'r', encoding='utf-8') as file: contents = [] for num, line in enumerate(file): @@ -17,11 +16,6 @@ def modfiy(path, isReset): line = "static let Certificate: String? = <#YOUR Certificate#>" else: line = 'static let Certificate: String? = nil' - elif "static let FaceCaptureLicense" in line: - if isReset: - line = "static let FaceCaptureLicense: String? = nil" - else: - line = f'static let FaceCaptureLicense: String? = "{faceCaptureLicense}"' elif "static NSString * const APPID" in line: if isReset: line = "static NSString * const APPID = <#YOUR APPID#>" diff --git a/cicd/scripts/keycenter.py b/cicd/scripts/keycenter.py index 3779b1a3e..e6d912403 100644 --- a/cicd/scripts/keycenter.py +++ b/cicd/scripts/keycenter.py @@ -40,6 +40,7 @@ def main(): appString = "@\"" + appId + "\"" tokenString = "@\"" + token + "\"" contentNew = re.sub(r'<#Your App Id#>', appString, content) + contentNew = re.sub(r'<#YOUR APPID#>', appString, contentNew) contentNew = re.sub(r'<#Temp Access Token#>', tokenString, contentNew) f.seek(0) f.write(contentNew) diff --git a/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift b/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift index c893af889..d8617becb 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift @@ -35,7 +35,6 @@ class ViewController: AGViewController { MenuItem(name: "Custom Audio Render".localized, storyboard: "CustomAudioRender", controller: "CustomAudioRender"), MenuItem(name: "Raw Audio Data".localized, storyboard: "RawAudioData", controller: ""), MenuItem(name: "Audio Mixing".localized, storyboard: "AudioMixing", controller: ""), - MenuItem(name: "Rhythm Player".localized, storyboard: "RhythmPlayer", controller: "RhythmPlayer"), MenuItem(name: "Precall Test".localized, storyboard: "PrecallTest", controller: ""), MenuItem(name: "Spatial Audio".localized, storyboard: "SpatialAudio", controller: "SpatialAudio"), ]), diff --git a/iOS/APIExample-Audio/ARCHITECTURE.md b/iOS/APIExample-Audio/ARCHITECTURE.md index 823be6545..632030ca0 100644 --- a/iOS/APIExample-Audio/ARCHITECTURE.md +++ b/iOS/APIExample-Audio/ARCHITECTURE.md @@ -12,7 +12,7 @@ | CustomAudioRender | `Examples/Advanced/CustomAudioRender/CustomAudioRender.swift` | `enableExternalAudioSink()`, `pullPlaybackAudioFrameRawData()` | Pull audio frames for custom rendering | | RawAudioData | `Examples/Advanced/RawAudioData/RawAudioData.swift` | `setAudioFrameDelegate()` | Capture raw audio PCM data via delegate | | AudioMixing | `Examples/Advanced/AudioMixing/AudioMixing.swift` | `startAudioMixing()`, `stopAudioMixing()`, `adjustAudioMixingVolume()`, `setEffectsVolume()` | Mix local audio file with microphone input | -| RhythmPlayer | `Examples/Advanced/RhythmPlayer/RhythmPlayer.swift` | `startRhythmPlayer()`, `stopRhythmPlayer()` | Play metronome-style rhythm audio | +| RhythmPlayer (hidden) | `Examples/Advanced/RhythmPlayer/RhythmPlayer.swift` | `startRhythmPlayer()`, `stopRhythmPlayer()` | Source retained for reference; hidden because the APIs are deprecated since RTC SDK 4.6.0 | | PrecallTest | `Examples/Advanced/PrecallTest/PrecallTest.swift` | `startEchoTest()`, `stopEchoTest()`, `startLastmileProbeTest()` | Pre-call echo test and last-mile network probe | | SpatialAudio | `Examples/Advanced/SpatialAudio/SpatialAudio.swift` | `createMediaPlayer()`, `updateChannel()`, `setEnableSpeakerphone()` | 3D spatial audio with media player integration | @@ -56,7 +56,7 @@ APIExample-Audio/ │ ├── CustomAudioRender/ # "Custom Audio Render" │ ├── RawAudioData/ # "Raw Audio Data" │ ├── AudioMixing/ # "Audio Mixing" - │ ├── RhythmPlayer/ # "Rhythm Player" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 │ ├── PrecallTest/ # "Precall Test" │ └── SpatialAudio/ # "Spatial Audio" │ diff --git a/iOS/APIExample-OC/APIExample-OC/ViewController.m b/iOS/APIExample-OC/APIExample-OC/ViewController.m index c7b3c5ff8..d94a81720 100644 --- a/iOS/APIExample-OC/APIExample-OC/ViewController.m +++ b/iOS/APIExample-OC/APIExample-OC/ViewController.m @@ -71,7 +71,6 @@ + (NSArray *)menus { [[MenuItem alloc]initWithName:@"Media Player".localized storyboard:@"MediaPlayer" controller:@""], [[MenuItem alloc]initWithName:@"Screen Share".localized storyboard:@"ScreenShare" controller:@""], [[MenuItem alloc]initWithName:@"Video Process".localized storyboard:@"VideoProcess" controller:@""], - [[MenuItem alloc]initWithName:@"Rhythm Player".localized storyboard:@"RhythmPlayer" controller:@""], [[MenuItem alloc]initWithName:@"Create Data Stream".localized storyboard:@"CreateDataStream" controller:@""], [[MenuItem alloc]initWithName:@"Media Channel Relay".localized storyboard:@"MediaChannelRelay" controller:@""], [[MenuItem alloc]initWithName:@"Spatial Audio".localized storyboard:@"SpatialAudio" controller:@""], diff --git a/iOS/APIExample-OC/ARCHITECTURE.md b/iOS/APIExample-OC/ARCHITECTURE.md index e7091a64d..b66067743 100644 --- a/iOS/APIExample-OC/ARCHITECTURE.md +++ b/iOS/APIExample-OC/ARCHITECTURE.md @@ -26,7 +26,7 @@ | ScreenShare | `Examples/Advanced/ScreenShare/ScreenShare.m` | `startScreenCapture:`, `updateScreenCapture:`, `stopScreenCapture` | Screen capture and sharing via ReplayKit extension | | LocalCompositeGraph | `Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.m` | `startLocalVideoTranscoder:`, `startCameraCapture:config:`, `enableVirtualBackground:backData:segData:` | Composite multiple video sources locally before publishing | | VideoProcess | `Examples/Advanced/VideoProcess/VideoProcess.m` | `setBeautyEffectOptions:options:`, `enableVirtualBackground:backData:segData:`, `enableExtensionWithVendor:` | Built-in beauty, virtual background, and video enhancement | -| RhythmPlayer | `Examples/Advanced/RhythmPlayer/RhythmPlayer.m` | `startRhythmPlayer:sound2:config:`, `stopRhythmPlayer` | Play metronome-style rhythm audio | +| RhythmPlayer (hidden) | `Examples/Advanced/RhythmPlayer/RhythmPlayer.m` | `startRhythmPlayer:sound2:config:`, `stopRhythmPlayer` | Source retained for reference; hidden because the APIs are deprecated since RTC SDK 4.6.0 | | CreateDataStream | `Examples/Advanced/CreateDataStream/CreateDataStream.m` | `createDataStream:config:`, `sendStreamMessage:data:` | Create and send data stream messages between users | | MediaChannelRelay | `Examples/Advanced/MediaChannelRelay/MediaChannelRelay.m` | `startOrUpdateChannelMediaRelay:`, `stopChannelMediaRelay`, `pauseAllChannelMediaRelay`, `resumeAllChannelMediaRelay` | Relay media stream to multiple destination channels | | SpatialAudio | `Examples/Advanced/SpatialAudio/SpatialAudio.m` | `createMediaPlayerWithDelegate:`, `updateChannelWithMediaOptions:` | 3D spatial audio with media player integration | @@ -88,7 +88,7 @@ APIExample-OC/ │ ├── MediaPlayer/ # "Media Player" │ ├── ScreenShare/ # "Screen Share" │ ├── VideoProcess/ # "Video Process" - │ ├── RhythmPlayer/ # "Rhythm Player" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 │ ├── CreateDataStream/ # "Create Data Stream" │ ├── MediaChannelRelay/ # "Media Channel Relay" │ ├── SpatialAudio/ # "Spatial Audio" diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj index 27955f88e..579dc01d6 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj @@ -74,8 +74,6 @@ 4C9309B02CBA91530085EFF9 /* RawVideoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9309562CBA91520085EFF9 /* RawVideoData.swift */; }; 4C9309B12CBA91530085EFF9 /* ARKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9309582CBA91520085EFF9 /* ARKit.swift */; }; 4C9309B22CBA91530085EFF9 /* ARKitRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9309592CBA91520085EFF9 /* ARKitRTC.swift */; }; - 4C9309B32CBA91530085EFF9 /* FaceCaptureRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C93095B2CBA91520085EFF9 /* FaceCaptureRTC.swift */; }; - 4C9309B42CBA91530085EFF9 /* FaceCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C93095C2CBA91520085EFF9 /* FaceCapture.swift */; }; 4C9309B52CBA91530085EFF9 /* MutliCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C93095E2CBA91520085EFF9 /* MutliCamera.swift */; }; 4C9309B62CBA91530085EFF9 /* MutliCameraRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C93095F2CBA91520085EFF9 /* MutliCameraRTC.swift */; }; 4C9309B72CBA91530085EFF9 /* StreamEncryptionRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9309612CBA91520085EFF9 /* StreamEncryptionRTC.swift */; }; @@ -255,8 +253,6 @@ 4C9309562CBA91520085EFF9 /* RawVideoData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawVideoData.swift; sourceTree = ""; }; 4C9309582CBA91520085EFF9 /* ARKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARKit.swift; sourceTree = ""; }; 4C9309592CBA91520085EFF9 /* ARKitRTC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARKitRTC.swift; sourceTree = ""; }; - 4C93095B2CBA91520085EFF9 /* FaceCaptureRTC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaceCaptureRTC.swift; sourceTree = ""; }; - 4C93095C2CBA91520085EFF9 /* FaceCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaceCapture.swift; sourceTree = ""; }; 4C93095E2CBA91520085EFF9 /* MutliCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutliCamera.swift; sourceTree = ""; }; 4C93095F2CBA91520085EFF9 /* MutliCameraRTC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutliCameraRTC.swift; sourceTree = ""; }; 4C9309612CBA91520085EFF9 /* StreamEncryptionRTC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamEncryptionRTC.swift; sourceTree = ""; }; @@ -626,7 +622,6 @@ 4C9309662CBA91520085EFF9 /* CreateDataStream */, 4C9309872CBA91530085EFF9 /* CustomAudioRender */, 4C93096E2CBA91520085EFF9 /* CustomPCMAudioSource */, - 4C93095A2CBA91520085EFF9 /* FaceCapture */, 4C9309712CBA91520085EFF9 /* JoinMultiChannel */, 4C9309692CBA91520085EFF9 /* KtvCopyrightMusic */, 4C9309632CBA91520085EFF9 /* LiveStreaming */, @@ -686,15 +681,6 @@ path = ARKit; sourceTree = ""; }; - 4C93095A2CBA91520085EFF9 /* FaceCapture */ = { - isa = PBXGroup; - children = ( - 4C93095B2CBA91520085EFF9 /* FaceCaptureRTC.swift */, - 4C93095C2CBA91520085EFF9 /* FaceCapture.swift */, - ); - path = FaceCapture; - sourceTree = ""; - }; 4C93095D2CBA91520085EFF9 /* MutliCamera */ = { isa = PBXGroup; children = ( @@ -1221,8 +1207,6 @@ 4C9309D32CBA91530085EFF9 /* LocalVideoTranscoding.swift in Sources */, 4C93076C2CB961150085EFF9 /* VideoView.swift in Sources */, 4C9307722CB961150085EFF9 /* AgoraExtension.swift in Sources */, - 4C9309B32CBA91530085EFF9 /* FaceCaptureRTC.swift in Sources */, - 4C9309B42CBA91530085EFF9 /* FaceCapture.swift in Sources */, 4C9309DF2CBA91530085EFF9 /* VideoProcessRTC.swift in Sources */, 4C93076B2CB961150085EFF9 /* AgoraYUVImageSourcePush.m in Sources */, 4C9307C62CB961160085EFF9 /* ContentView.swift in Sources */, diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/KeyCenter.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/KeyCenter.swift index 5e641061e..7c9f003a4 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/KeyCenter.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/KeyCenter.swift @@ -32,7 +32,4 @@ class KeyCenter: NSObject { PS: If the project does not have certificates enabled, leave this field blank. */ static let Certificate: String? = nil - - - static let FaceCaptureLicense: String? = nil } diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift index daa2f15ab..82eea31a4 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift @@ -72,10 +72,6 @@ struct ContentView: View { view: AnyView(ScreenShareEntry())), MenuItem(name: "Video Process".localized, view: AnyView(VideoProcessEntry())), - MenuItem(name: "Agora Beauty".localized, - view: AnyView(AgoraBeautyEntry())), - MenuItem(name: "Rhythm Player".localized, - view: AnyView(RhythmPlayerEntry())), MenuItem(name: "Create Data Stream".localized, view: AnyView(CreateDataStreamEntry())), MenuItem(name: "Media Channel Relay".localized, @@ -92,8 +88,6 @@ struct ContentView: View { view: AnyView(ARKitEntry())), MenuItem(name: "Audio Waveform".localized, view: AnyView(AudioWaveformEntry())), - MenuItem(name: "Face Capture".localized, - view: AnyView(FaceCaptureEntry())), MenuItem(name: "Simulcast".localized, view: AnyView(SimulcastEntry())), MenuItem(name: "Multipath".localized, diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCapture.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCapture.swift deleted file mode 100644 index ecc26d1d9..000000000 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCapture.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// JoinChannelVideoView.swift -// APIExample-SwiftUI -// -// Created by zhaoyongqiang on 2024/3/19. -// - -import SwiftUI - -struct FaceCaptureEntry: View { - @State private var channelName: String = "" - @State private var isActive = false - @State private var configs: [String: Any] = [:] - - var body: some View { - VStack { - Spacer() - TextField("Enter channel name".localized, text: $channelName).textFieldStyle(.roundedBorder).padding() - Button { - configs = ["channelName": channelName] - self.isActive = true - } label: { - Text("Join".localized) - }.disabled(channelName.isEmpty) - Spacer() - NavigationLink(destination: FaceCapture(configs: configs).navigationTitle(channelName).navigationBarTitleDisplayMode(.inline), isActive: $isActive) { - EmptyView() - } - Spacer() - } - .navigationBarTitleDisplayMode(.inline) - } -} - -struct FaceCaptureEntryPreviews: PreviewProvider { - static var previews: some View { - FaceCaptureEntry() - } -} - -struct FaceCapture: View { - @State var configs: [String: Any] = [:] - - @State private var statsInfo: String = "" - @ObservedObject private var agoraKit = FaceCaptureRTC() - - var localView = VideoView(type: .local, - audioOnly: false) - - var body: some View { - VStack { - HStack{ - localView - } - Spacer() - }.onAppear(perform: { - agoraKit.setupRTC(configs: configs, - localView: localView.videoView) - }).onDisappear(perform: { - agoraKit.onDestory() - }).alert(isPresented: $agoraKit.isFaceLicenseEmpty, content: { - Alert(title: Text("Please contact Agora customer service to obtain a face capture certificate".localized)) - }) - } -} - -struct FaceCapturePreviews: PreviewProvider { - static var previews: some View { - FaceCapture(configs: [:]) - } -} diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCaptureRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCaptureRTC.swift deleted file mode 100644 index ac4e30dd7..000000000 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/FaceCapture/FaceCaptureRTC.swift +++ /dev/null @@ -1,207 +0,0 @@ -// -// JoinChannelVideoRTC.swift -// APIExample-SwiftUI -// -// Created by zhaoyongqiang on 2024/3/19. -// - -import AgoraRtcKit -import SwiftUI - -class FaceCaptureRTC: NSObject, ObservableObject { - private var agoraKit: AgoraRtcEngineKit! - private var isJoined: Bool = false - private var localView: VideoUIView? - - @Published var isFaceLicenseEmpty: Bool = false - - func setupRTC(configs: [String: Any], - localView: VideoUIView) { - self.localView = localView - // set up agora instance when view loaded - let config = AgoraRtcEngineConfig() - config.appId = KeyCenter.AppId - config.areaCode = GlobalSettings.shared.area - config.channelProfile = .liveBroadcasting - agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) - // Configuring Privatization Parameters - Util.configPrivatization(agoraKit: agoraKit) - - agoraKit.setLogFile(LogUtils.sdkLogPath()) - // get channel name from configs - guard let channelName = configs["channelName"] as? String else {return} - let fps = GlobalSettings.shared.getFps() - let resolution = GlobalSettings.shared.getResolution() - let orientation = GlobalSettings.shared.getOrientation() - - // make myself a broadcaster - agoraKit.setClientRole(GlobalSettings.shared.getUserRole()) - // enable video module and set up video encoding configs - agoraKit.enableVideo() - agoraKit.enableAudio() - agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, - frameRate: fps.rawValue, - bitrate: AgoraVideoBitrateStandard, - orientationMode: orientation, mirrorMode: .auto)) - - isFaceLicenseEmpty = (KeyCenter.FaceCaptureLicense ?? "").isEmpty - guard !isFaceLicenseEmpty else { return } - // enable face capture - agoraKit.enableExtension(withVendor: "agora_video_filters_face_capture", - extension: "face_capture", - enabled: true, - sourceType: .primaryCamera) - - agoraKit.setExtensionPropertyWithVendor("agora_video_filters_face_capture", - extension: "face_capture", - key: "authentication_information", - value: "{\"company_id\":\"agoraTest\"," + - "\"license\":\"" + (KeyCenter.FaceCaptureLicense ?? "") + "\"}", - sourceType: .primaryCamera) - - agoraKit.enableExtension(withVendor: "agora_filters_lip_sync", - extension: "lip_sync", - enabled: true, - sourceType: .speechDriven) - agoraKit.setExtensionPropertyWithVendor("agora_filters_lip_sync", - extension: "lip_sync", - key: "parameters", - value: "{\"company_id\":\"agoraTest\", \"license\":\"abc\", \"open_agc\":true}", - sourceType: .speechDriven) - - agoraKit.setVideoFrameDelegate(self) - agoraKit.setFaceInfoDelegate(self) - - setupCanvasView(view: localView.videoView) - - // Set audio route to speaker - agoraKit.setDefaultAudioRouteToSpeakerphone(true) - - // start joining channel - // 1. Users can only see each other after they join the - // same channel successfully using the same app id. - // 2. If app certificate is turned on at dashboard, token is needed - // when joining channel. The channel name and uid used to calculate - // the token has to match the ones used for channel join - let option = AgoraRtcChannelMediaOptions() - option.publishCameraTrack = GlobalSettings.shared.getUserRole() == .broadcaster - option.publishMicrophoneTrack = GlobalSettings.shared.getUserRole() == .broadcaster - option.clientRoleType = GlobalSettings.shared.getUserRole() - NetworkManager.shared.generateToken(channelName: channelName, success: { token in - let result = self.agoraKit.joinChannel(byToken: token, channelId: channelName, uid: 0, mediaOptions: option) - if result != 0 { - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code - LogUtils.log(message: "joinChannel call failed: \(result), please check your params", level: .error) - } - }) - } - - private func setupCanvasView(view: UIView?) { - // set up local video to render your local camera preview - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = 0 - // the view to be binded - videoCanvas.view = view - videoCanvas.renderMode = .hidden - agoraKit.setupLocalVideo(videoCanvas) - // you have to call startPreview to see local video - agoraKit.startPreview() - } - - func onDestory() { - agoraKit.disableAudio() - agoraKit.disableVideo() - if isJoined { - agoraKit.stopPreview() - agoraKit.leaveChannel { (stats) -> Void in - LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) - } - } - AgoraRtcEngineKit.destroy() - } -} - -extension FaceCaptureRTC: AgoraFaceInfoDelegate { - func onFaceInfo(_ outFaceInfo: String) -> Bool { - localView?.statsInfo?.updateMetaInfo(data: outFaceInfo) - return true - } -} - -extension FaceCaptureRTC: AgoraVideoFrameDelegate { - func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool { - let info = videoFrame.metaInfo["KEY_FACE_CAPTURE"] as? String - LogUtils.log(message: info ?? "", level: .info) - return true - } - func getVideoFrameProcessMode() -> AgoraVideoFrameProcessMode { - .readWrite - } - func getObservedFramePosition() -> AgoraVideoFramePosition { - .postCapture - } -} - -// agora rtc engine delegate events -extension FaceCaptureRTC: AgoraRtcEngineDelegate { - /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out - /// what is happening - /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// @param warningCode warning code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { - LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) - } - - /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand - /// to let user know something wrong is happening - /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code - /// @param errorCode error code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "error: \(errorCode)", level: .error) -// self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - self.isJoined = true - localView?.uid = uid - LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) - } - - /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param elapsed time elapse since current sdk instance join the channel in ms - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) - } - - /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param reason reason why this user left, note this event may be triggered when the remote user - /// become an audience in live broadcasting profile - func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, connectionChangedTo state: AgoraConnectionState, reason: AgoraConnectionChangedReason) { - LogUtils.log(message: "Connection state changed: \(state) \(reason)", level: .info) - } - - /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { - localView?.statsInfo?.updateChannelStats(stats) - } - - /// Reports the statistics of the uploading local audio streams once every two seconds. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { - localView?.statsInfo?.updateLocalAudioStats(stats) - } -} diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings index 4312635ce..4762e0563 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings @@ -126,7 +126,6 @@ "Screen Share + Camera" = "Screen Share + Camera"; "please input channel name!" = "Please enter channel name!"; "Video Process" = "Video Enhancement"; -"Agora Beauty" = "Agora Beauty"; "Rhythm Player" = "Virtual Metronome"; "Spatial Audio" = "Spatial Audio"; "Please move the red icon to experience the 3D audio effect" = "Please move the red icon to experience 3D audio effect"; @@ -167,8 +166,6 @@ "AGGRESSIVE" = "Aggressive Mode"; "BALANCED" = "(Default) Balanced Mode"; "ULTRALOWLATENCY" = "Ultra Low Latency Mode"; -"Face Capture" = "Face Capture"; -"Please contact Agora customer service to obtain a face capture certificate" = "Please contact Agora support for face capture certificate"; "Audio Scenario" = "Audio Scenario"; "Audio Profile" = "Audio Profile"; "RecordingVolume" = "Recording Volume"; diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings index 49aa90b14..536e809b3 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings @@ -126,7 +126,6 @@ "Screen Share + Camera" = "屏幕共享 + 摄像头"; "please input channel name!" = "请输入频道名!"; "Video Process" = "视频增强"; -"Agora Beauty" = "Agora 美颜"; "Rhythm Player" = "虚拟节拍器"; "Spatial Audio" = "空间音效"; "Please move the red icon to experience the 3D audio effect" = "请移动红色图标体验3D音频效果"; @@ -167,8 +166,6 @@ "AGGRESSIVE" = "强降噪模式"; "BALANCED" = "(默认)均衡降噪模式"; "ULTRALOWLATENCY" = "低延时强降噪模式"; -"Face Capture" = "面捕数据"; -"Please contact Agora customer service to obtain a face capture certificate" = "请联系 Agora 客服获取面捕证书"; "Audio Scenario" = "音频使用场景"; "Audio Profile" = "音频参数配置"; "RecordingVolume" = "采集音量"; diff --git a/iOS/APIExample-SwiftUI/ARCHITECTURE.md b/iOS/APIExample-SwiftUI/ARCHITECTURE.md index b64b059f5..8d727305b 100644 --- a/iOS/APIExample-SwiftUI/ARCHITECTURE.md +++ b/iOS/APIExample-SwiftUI/ARCHITECTURE.md @@ -27,8 +27,7 @@ | LocalVideoTranscoding | `Examples/Advanced/LocalVideoTranscoding/` | `startLocalVideoTranscoder()`, `startCameraCapture()`, `createMediaPlayer()` | Transcode multiple video sources locally before publishing | | LocalVideoComposition | `Examples/Advanced/LocalVideoComposition/` | `startLocalVideoTranscoder()`, `startCameraCapture()`, `startScreenCapture()` | Composite camera and screen capture into one stream | | VideoProcess | `Examples/Advanced/VideoProcess/` | `setBeautyEffectOptions()`, `enableVirtualBackground()`, `enableExtension()` | Built-in beauty, virtual background, and video enhancement | -| AgoraBeauty | `Examples/Advanced/AgoraBeauty/` | `enableExtension()`, `createVideoEffectObject()`, `setFilterEffectOptions()` | Agora beauty extension with makeup and virtual background | -| RhythmPlayer | `Examples/Advanced/RhythmPlayer/` | `startRhythmPlayer()`, `stopRhythmPlayer()` | Play metronome-style rhythm audio | +| RhythmPlayer (hidden) | `Examples/Advanced/RhythmPlayer/` | `startRhythmPlayer()`, `stopRhythmPlayer()` | Source retained for reference; hidden because the APIs are deprecated since RTC SDK 4.6.0 | | CreateDataStream | `Examples/Advanced/CreateDataStream/` | `createDataStream()`, `sendStreamMessage()` | Create and send data stream messages between users | | MediaChannelRelay | `Examples/Advanced/MediaChannelRelay/` | `startOrUpdateChannelMediaRelay()`, `stopChannelMediaRelay()`, `pauseAllChannelMediaRelay()` | Relay media stream to multiple destination channels | | SpatialAudio | `Examples/Advanced/SpatialAudio/` | `createMediaPlayer()`, `updateChannel()` | 3D spatial audio with media player integration | @@ -37,7 +36,6 @@ | KtvCopyrightMusic | `Examples/Advanced/KtvCopyrightMusic/` | — | Links to KTV copyright music documentation | | ARKit | `Examples/Advanced/ARKit/` | `setVideoFrameDelegate()`, `enableInstantMediaRendering()`, `startMediaRenderingTracing()` | Push ARKit face tracking frames as custom video source | | AudioWaveform | `Examples/Advanced/AudioWaveform/` | `setAudioProfile()`, `enableAudioVolumeIndication()` | Visualize audio waveform from volume callbacks | -| FaceCapture | `Examples/Advanced/FaceCapture/` | `enableExtension()`, `setExtensionPropertyWithVendor()`, `setFaceInfoDelegate()` | Face capture and lip sync via Agora extension | | Simulcast | `Examples/Advanced/Simulcast/` | `setSimulcastConfig()`, `setRemoteVideoStream()` | Publish multiple video quality layers simultaneously | | Multipath | `Examples/Advanced/Multipath/` | `updateChannel()` | Multi-path network transmission configuration | @@ -97,8 +95,7 @@ APIExample-SwiftUI/ │ ├── LocalVideoTranscoding/ # "Local Video Transcoding" │ ├── LocalVideoComposition/ # "Local Composite Graph" │ ├── VideoProcess/ # "Video Process" - │ ├── AgoraBeauty/ # "Agora Beauty" - │ ├── RhythmPlayer/ # "Rhythm Player" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 │ ├── CreateDataStream/ # "Create Data Stream" │ ├── MediaChannelRelay/ # "Media Channel Relay" │ ├── SpatialAudio/ # "Spatial Audio" @@ -107,7 +104,6 @@ APIExample-SwiftUI/ │ ├── KtvCopyrightMusic/ # "KTV Copyright Music" │ ├── ARKit/ # "ARKit" │ ├── AudioWaveform/ # "Audio Waveform" - │ ├── FaceCapture/ # "Face Capture" │ ├── Simulcast/ # "Simulcast" │ └── Multipath/ # "Multipath" │ diff --git a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index bc7618fb0..2ee49eaf0 100644 --- a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj +++ b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj @@ -172,8 +172,6 @@ E77902672A484A8A008791AD /* KFMP4Demuxer.m in Sources */ = {isa = PBXBuildFile; fileRef = E77902652A484A8A008791AD /* KFMP4Demuxer.m */; }; E77D54C828F55E9100D51C1E /* JoinChannelVideoToken.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E77D54C428F55E9100D51C1E /* JoinChannelVideoToken.storyboard */; }; E77D54C928F55E9100D51C1E /* JoinChannelVideoToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77D54C628F55E9100D51C1E /* JoinChannelVideoToken.swift */; }; - E7883AEA2B074746003CCF44 /* FaceCapture.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7883AE62B074746003CCF44 /* FaceCapture.storyboard */; }; - E7883AEB2B074746003CCF44 /* FaceCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7883AE82B074746003CCF44 /* FaceCapture.swift */; }; E7899BDD2861673600851463 /* CreateDataStream.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7899BD92861673600851463 /* CreateDataStream.storyboard */; }; E7899BDE2861673600851463 /* CreateDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7899BDB2861673600851463 /* CreateDataStream.swift */; }; E7A49CB12900FC6400F06DD4 /* KtvCopyrightMusic.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */; }; @@ -478,9 +476,6 @@ E77D54C328F55E9100D51C1E /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/JoinChannelVideoToken.strings"; sourceTree = ""; }; E77D54C528F55E9100D51C1E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/JoinChannelVideoToken.storyboard; sourceTree = ""; }; E77D54C628F55E9100D51C1E /* JoinChannelVideoToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinChannelVideoToken.swift; sourceTree = ""; }; - E7883AE52B074746003CCF44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/FaceCapture.strings"; sourceTree = ""; }; - E7883AE72B074746003CCF44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/FaceCapture.storyboard; sourceTree = ""; }; - E7883AE82B074746003CCF44 /* FaceCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaceCapture.swift; sourceTree = ""; }; E7899BD82861673600851463 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CreateDataStream.strings"; sourceTree = ""; }; E7899BDA2861673600851463 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CreateDataStream.storyboard; sourceTree = ""; }; E7899BDB2861673600851463 /* CreateDataStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDataStream.swift; sourceTree = ""; }; @@ -1038,7 +1033,6 @@ ABB399EB2DF9605B00AF5033 /* Multipath */, ABB399F02DF9605B00AF5033 /* Simulcast */, DDFD50242C205BDF0050B344 /* TransparentRender */, - E7883AE32B074746003CCF44 /* FaceCapture */, E726BFFD2A96FD3A006870E2 /* AudioWaveform */, E726BFF42A949F70006870E2 /* AudioRouterPlayer */, E7163F7B2964149800EBBD55 /* ARKit */, @@ -1260,15 +1254,6 @@ path = "JoinChannelVideo(Token)"; sourceTree = ""; }; - E7883AE32B074746003CCF44 /* FaceCapture */ = { - isa = PBXGroup; - children = ( - E7883AE62B074746003CCF44 /* FaceCapture.storyboard */, - E7883AE82B074746003CCF44 /* FaceCapture.swift */, - ); - path = FaceCapture; - sourceTree = ""; - }; E7899BD62861673600851463 /* CreateDataStream */ = { isa = PBXGroup; children = ( @@ -1537,7 +1522,6 @@ 033A9F23252D70E400BC26E1 /* JoinChannelVideo.storyboard in Resources */, 03414B5525546DEC00AB114D /* frames0.yuv in Resources */, 8B5E5B53274CBF760040E97D /* VideoProcess.storyboard in Resources */, - E7883AEA2B074746003CCF44 /* FaceCapture.storyboard in Resources */, DD6DCEE22CBD1D68008D7B72 /* hlg-hdr.yuv in Resources */, 033A9F66252D8B2A00BC26E1 /* VoiceChanger.storyboard in Resources */, E7A49CB929011E7500F06DD4 /* MutliCamera.storyboard in Resources */, @@ -1769,7 +1753,6 @@ 67450169282D5D8B00E79F2F /* ContentInspect.swift in Sources */, 67B8C7B52805757200195106 /* MediaUtils.m in Sources */, 03D13C0124488F1F00B599B3 /* KeyCenter.swift in Sources */, - E7883AEB2B074746003CCF44 /* FaceCapture.swift in Sources */, 03BEED08251C35E7005E78F4 /* AudioMixing.swift in Sources */, E74877B728A23B8B00CA2F58 /* NetworkManager.swift in Sources */, 03DF1D9424CFC29700DF7151 /* AudioController.m in Sources */, @@ -2210,15 +2193,6 @@ name = JoinChannelVideoToken.storyboard; sourceTree = ""; }; - E7883AE62B074746003CCF44 /* FaceCapture.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E7883AE72B074746003CCF44 /* Base */, - E7883AE52B074746003CCF44 /* zh-Hans */, - ); - name = FaceCapture.storyboard; - sourceTree = ""; - }; E7899BD92861673600851463 /* CreateDataStream.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/iOS/APIExample/APIExample/Common/KeyCenter.swift b/iOS/APIExample/APIExample/Common/KeyCenter.swift index 16c590ead..f54799026 100644 --- a/iOS/APIExample/APIExample/Common/KeyCenter.swift +++ b/iOS/APIExample/APIExample/Common/KeyCenter.swift @@ -33,5 +33,4 @@ class KeyCenter: NSObject { */ static let Certificate: String? = nil - static let FaceCaptureLicense: String? = nil } diff --git a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard b/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard deleted file mode 100644 index e46a4510d..000000000 --- a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift b/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift deleted file mode 100644 index 2da2621ce..000000000 --- a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/FaceCapture.swift +++ /dev/null @@ -1,310 +0,0 @@ -// -// JoinChannelVideo.swift -// APIExample -// -// Created by 张乾泽 on 2020/4/17. -// Copyright © 2020 Agora Corp. All rights reserved. -// -import UIKit -import AGEVideoLayout -import AgoraRtcKit - -class FaceCaptureEntry: UIViewController { - @IBOutlet weak var joinButton: UIButton! - @IBOutlet weak var channelTextField: UITextField! - let identifier = "FaceCapture" - @IBOutlet var resolutionBtn: UIButton! - @IBOutlet var fpsBtn: UIButton! - @IBOutlet var orientationBtn: UIButton! - var width: Int = 960, height: Int = 540, orientation: AgoraVideoOutputOrientationMode = .adaptative, fps = 15 - - override func viewDidLoad() { - super.viewDidLoad() - } - - func getResolutionAction(width: Int, height: Int) -> UIAlertAction { - return UIAlertAction(title: "\(width)x\(height)", style: .default, handler: {[unowned self] _ in - self.width = width - self.height = height - self.resolutionBtn.setTitle("\(width)x\(height)", for: .normal) - }) - } - - func getFpsAction(_ fps: Int) -> UIAlertAction { - return UIAlertAction(title: "\(fps)fps", style: .default, handler: { [unowned self] _ in - self.fps = fps - self.fpsBtn.setTitle("\(fps)fps", for: .normal) - }) - } - - func getOrientationAction(_ orientation: AgoraVideoOutputOrientationMode) -> UIAlertAction { - return UIAlertAction(title: "\(orientation.description())", style: .default, handler: { [unowned self] _ in - self.orientation = orientation - self.orientationBtn.setTitle("\(orientation.description())", for: .normal) - }) - } - - @IBAction func setResolution() { - let style: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet - let alert = UIAlertController(title: "Set Resolution".localized, - message: nil, - preferredStyle: style) - alert.addAction(getResolutionAction(width: 90, height: 90)) - alert.addAction(getResolutionAction(width: 160, height: 120)) - alert.addAction(getResolutionAction(width: 320, height: 240)) - alert.addAction(getResolutionAction(width: 960, height: 540)) - alert.addAction(getResolutionAction(width: 1280, height: 720)) - alert.addCancelAction() - present(alert, animated: true, completion: nil) - } - - @IBAction func setFps() { - let style: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet - let alert = UIAlertController(title: "Set Fps".localized, - message: nil, - preferredStyle: style) - alert.addAction(getFpsAction(10)) - alert.addAction(getFpsAction(15)) - alert.addAction(getFpsAction(24)) - alert.addAction(getFpsAction(30)) - alert.addAction(getFpsAction(60)) - alert.addCancelAction() - present(alert, animated: true, completion: nil) - } - - @IBAction func setOrientation() { - let style: UIAlertController.Style = UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet - let alert = UIAlertController(title: "Set Orientation".localized, - message: nil, - preferredStyle: style) - alert.addAction(getOrientationAction(.adaptative)) - alert.addAction(getOrientationAction(.fixedLandscape)) - alert.addAction(getOrientationAction(.fixedPortrait)) - alert.addCancelAction() - present(alert, animated: true, completion: nil) - } - - @IBAction func doJoinPressed(sender: UIButton) { - guard let channelName = channelTextField.text else { return } - // resign channel text field - channelTextField.resignFirstResponder() - - let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) - // create new view controller every time to ensure we get a clean vc - guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} - newViewController.title = channelName - newViewController.configs = ["channelName": channelName, - "resolution": CGSize(width: width, height: height), - "fps": fps, - "orientation": orientation] - navigationController?.pushViewController(newViewController, animated: true) - } -} - -class FaceCaptureMain: BaseViewController { - var localVideo = Bundle.loadVideoView(type: .local, audioOnly: false) - - @IBOutlet weak var container: AGEVideoContainer! - var agoraKit: AgoraRtcEngineKit! - - // indicate if current instance has joined channel - var isJoined: Bool = false - - override func viewDidLoad() { - super.viewDidLoad() - // layout render view - localVideo.setPlaceholder(text: "Local Host".localized) - container.layoutStream(views: [localVideo]) - - // set up agora instance when view loaded - let config = AgoraRtcEngineConfig() - config.appId = KeyCenter.AppId - config.areaCode = GlobalSettings.shared.area - config.channelProfile = .liveBroadcasting - agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) - // Configuring Privatization Parameters - Util.configPrivatization(agoraKit: agoraKit) - - agoraKit.setLogFile(LogUtils.sdkLogPath()) - - // get channel name from configs - guard let channelName = configs["channelName"] as? String, - let resolution = configs["resolution"] as? CGSize, - let fps = configs["fps"] as? Int, - let orientation = configs["orientation"] as? AgoraVideoOutputOrientationMode else {return} - - // make myself a broadcaster - agoraKit.setClientRole(GlobalSettings.shared.getUserRole()) - // enable video module and set up video encoding configs - agoraKit.enableVideo() - agoraKit.enableAudio() - agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, - frameRate: (AgoraVideoFrameRate(rawValue: fps) ?? .fps15).rawValue, - bitrate: AgoraVideoBitrateStandard, - orientationMode: orientation, mirrorMode: .auto)) - - if (KeyCenter.FaceCaptureLicense ?? "").isEmpty { - showAlert(message: "Please contact Agora customer service to obtain a face capture certificate".localized) - } else { - // enable face capture - agoraKit.enableExtension(withVendor: "agora_video_filters_face_capture", - extension: "face_capture", - enabled: true, - sourceType: .primaryCamera) - - agoraKit.setExtensionPropertyWithVendor("agora_video_filters_face_capture", - extension: "face_capture", - key: "authentication_information", - value: "{\"company_id\":\"agoraTest\"," + - "\"license\":\"" + (KeyCenter.FaceCaptureLicense ?? "") + "\"}", - sourceType: .primaryCamera) - - agoraKit.enableExtension(withVendor: "agora_filters_lip_sync", - extension: "lip_sync", - enabled: true, - sourceType: .speechDriven) - agoraKit.setExtensionPropertyWithVendor("agora_filters_lip_sync", - extension: "lip_sync", - key: "parameters", - value: "{\"company_id\":\"agoraTest\", \"license\":\"abc\", \"open_agc\":true}", - sourceType: .speechDriven) - - agoraKit.setVideoFrameDelegate(self) - agoraKit.setFaceInfoDelegate(self) - } - - // set up local video to render your local camera preview - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = 0 - // the view to be binded - videoCanvas.view = localVideo.videoView - videoCanvas.renderMode = .hidden - agoraKit.setupLocalVideo(videoCanvas) - // you have to call startPreview to see local video - agoraKit.startPreview() - - // Set audio route to speaker - agoraKit.setDefaultAudioRouteToSpeakerphone(true) - - // start joining channel - // 1. Users can only see each other after they join the - // same channel successfully using the same app id. - // 2. If app certificate is turned on at dashboard, token is needed - // when joining channel. The channel name and uid used to calculate - // the token has to match the ones used for channel join - let option = AgoraRtcChannelMediaOptions() - option.publishCameraTrack = GlobalSettings.shared.getUserRole() == .broadcaster - option.publishMicrophoneTrack = GlobalSettings.shared.getUserRole() == .broadcaster - option.clientRoleType = GlobalSettings.shared.getUserRole() - NetworkManager.shared.generateToken(channelName: channelName, success: { token in - let result = self.agoraKit.joinChannel(byToken: token, channelId: channelName, uid: 0, mediaOptions: option) - if result != 0 { - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code - self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") - } - }) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - agoraKit.disableAudio() - agoraKit.disableVideo() - if isJoined { - agoraKit.stopPreview() - agoraKit.leaveChannel { (stats) -> Void in - LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) - } - } - } -} - -extension FaceCaptureMain: AgoraFaceInfoDelegate { - func onFaceInfo(_ outFaceInfo: String) -> Bool { - localVideo.statsInfo?.updateMetaInfo(data: outFaceInfo) - return true - } -} - -extension FaceCaptureMain: AgoraVideoFrameDelegate { - func onCapture(_ videoFrame: AgoraOutputVideoFrame, sourceType: AgoraVideoSourceType) -> Bool { - let info = videoFrame.metaInfo["KEY_FACE_CAPTURE"] as? String - localVideo.statsInfo?.updateMetaInfo(data: info) - LogUtils.log(message: info ?? "", level: .info) - return true - } - func getVideoFrameProcessMode() -> AgoraVideoFrameProcessMode { - .readWrite - } - func getObservedFramePosition() -> AgoraVideoFramePosition { - .postCapture - } -} - -/// agora rtc engine delegate events -extension FaceCaptureMain: AgoraRtcEngineDelegate { - /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out - /// what is happening - /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// @param warningCode warning code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { - LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) - } - - /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand - /// to let user know something wrong is happening - /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code - /// @param errorCode error code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "error: \(errorCode)", level: .error) - self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - self.isJoined = true - LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) - } - - /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param elapsed time elapse since current sdk instance join the channel in ms - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) - } - - /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param reason reason why this user left, note this event may be triggered when the remote user - /// become an audience in live broadcasting profile - func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) - - // to unlink your view from sdk, so that your view reference will be released - // note the video will stay at its last frame, to completely remove it - // you will need to remove the EAGL sublayer from your binded view - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = uid - // the view to be binded - videoCanvas.view = nil - videoCanvas.renderMode = .hidden - agoraKit.setupRemoteVideo(videoCanvas) - } - - /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { - localVideo.statsInfo?.updateChannelStats(stats) - } - - /// Reports the statistics of the uploading local audio streams once every two seconds. - /// @param stats stats struct - func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { - localVideo.statsInfo?.updateLocalAudioStats(stats) - } -} diff --git a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings b/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings deleted file mode 100644 index 6a9d55c5c..000000000 --- a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "输入频道名"; - -/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "加入频道"; - -/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "视频实时通话"; diff --git a/iOS/APIExample/APIExample/ViewController.swift b/iOS/APIExample/APIExample/ViewController.swift index 2ca072147..e416322a6 100644 --- a/iOS/APIExample/APIExample/ViewController.swift +++ b/iOS/APIExample/APIExample/ViewController.swift @@ -74,7 +74,6 @@ class ViewController: AGViewController { MenuItem(name: "ARKit".localized, storyboard: "ARKit", controller: ""), MenuItem(name: "Audio Router(Third Party Player)".localized, storyboard: "AudioRouterPlayer", controller: ""), MenuItem(name: "Audio Waveform".localized, storyboard: "AudioWaveform", controller: ""), - MenuItem(name: "Face Capture".localized, storyboard: "FaceCapture", controller: ""), MenuItem(name: "Transparent Render".localized, storyboard: "TransparentRender", controller: ""), MenuItem(name: "URL Streaming(RTE Player)".localized, storyboard: "RtePlayer", controller: ""), MenuItem(name: "Simulcast".localized, storyboard: "Simulcast", controller: "SimulcasthEntry"), diff --git a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings index 3b5e99392..375d1fdc7 100644 --- a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings @@ -169,8 +169,6 @@ "AGGRESSIVE" = "强降噪模式"; "BALANCED" = "(默认)均衡降噪模式"; "ULTRALOWLATENCY" = "低延时强降噪模式"; -"Face Capture" = "面捕数据"; -"Please contact Agora customer service to obtain a face capture certificate" = "请联系 Agora 客服获取面捕证书"; "Second camera" = "第二路摄像头"; "This device does not support Center Stage" = "此设备不支持 Center Stage"; "Wide" = "广角"; diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index 7c8e112aa..185bb113a 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -44,7 +44,6 @@ | ARKit | `Examples/Advanced/ARKit/ARKit.swift` | `setVideoFrameDelegate()`, `enableInstantMediaRendering()`, `startMediaRenderingTracing()` | Push ARKit face tracking frames as custom video source | | AudioRouterPlayer | `Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift` | `setEnableSpeakerphone()` | Control audio output routing with third-party player | | AudioWaveform | `Examples/Advanced/AudioWaveform/AudioWaveform.swift` | `setAudioProfile()`, `enableAudioVolumeIndication()` | Visualize audio waveform from volume callbacks | -| FaceCapture | `Examples/Advanced/FaceCapture/FaceCapture.swift` | `enableExtension()`, `setExtensionPropertyWithVendor()`, `setFaceInfoDelegate()` | Face capture and lip sync via Agora extension | | TransparentRender | `Examples/Advanced/TransparentRender/TransparentRender.swift` | `createMediaPlayer()`, `setExternalVideoSource()`, `pushExternalVideoFrame()` | Render video with transparent background | | RtePlayer | `Examples/Advanced/RtePlayer/RtePlayer.swift` | `AgoraRte`, `AgoraRtePlayer`, `AgoraRteCanvas` | URL-based stream playback via RTE Player API | | Simulcast | `Examples/Advanced/Simulcast/Simulcast.swift` | `setSimulcastConfig()`, `setRemoteVideoStream()` | Publish multiple video quality layers simultaneously | @@ -131,7 +130,6 @@ APIExample/ │ ├── ARKit/ # "ARKit" │ ├── AudioRouterPlayer/ # "Audio Router (Third Party Player)" │ ├── AudioWaveform/ # "Audio Waveform" - │ ├── FaceCapture/ # "Face Capture" │ ├── TransparentRender/ # "Transparent Render" │ ├── RtePlayer/ # "URL Streaming (RTE Player)" │ ├── Simulcast/ # "Simulcast" From d5bd7ae301c18e4b2022283a430acf2c0d00dc9d Mon Sep 17 00:00:00 2001 From: zhangwei Date: Fri, 7 Aug 2026 18:19:05 +0800 Subject: [PATCH 22/49] feat: update Windows examples for RTC SDK 4.7.0 --- windows/APIExample/APIExample/APIExample.rc | 25 - .../APIExample/APIExample/APIExample.vcxproj | 4 +- .../APIExample/APIExample.vcxproj.filters | 11 +- .../APIExample/APIExample/APIExampleDlg.cpp | 13 - windows/APIExample/APIExample/APIExampleDlg.h | 3 - .../AudioMixing/CAgoraAudioMixingDlg.cpp | 2 +- .../AudioProfile/CAgoraAudioProfile.cpp | 46 +- .../CAgoraCaptureAudioDlg.cpp | 86 ++- .../CAgoraCaptureAudioDlg.h | 3 +- .../FaceCapture/CAgoraFaceCaptureDlg.cpp | 632 ------------------ .../FaceCapture/CAgoraFaceCaptureDlg.h | 223 ------ .../CLocalVideoTranscodingDlg.cpp | 45 +- .../CAgoraMutilVideoSourceDlg.cpp | 58 +- .../Advanced/RtePlayer/RtePlayerDlg.cpp | 44 +- .../Advanced/RtePlayer/RtePlayerDlg.h | 12 + .../ScreenShare/AgoraScreenCapture.cpp | 129 ++-- .../Advanced/ScreenShare/AgoraScreenCapture.h | 6 + .../DirectShow/AGDShowAudioCapture.cpp | 8 +- .../DirectShow/AGDShowAudioCapture.h | 1 + windows/APIExample/APIExample/Language.h | 2 - windows/APIExample/APIExample/en.ini | 3 +- windows/APIExample/APIExample/resource.h | 2 - windows/APIExample/APIExample/stdafx.cpp | 2 - windows/APIExample/APIExample/stdafx.h | 2 - windows/APIExample/APIExample/zh-cn.ini | Bin 18336 -> 18280 bytes windows/ARCHITECTURE.md | 3 +- 26 files changed, 334 insertions(+), 1031 deletions(-) delete mode 100644 windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.cpp delete mode 100644 windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.h diff --git a/windows/APIExample/APIExample/APIExample.rc b/windows/APIExample/APIExample/APIExample.rc index 7c2095e59..7b509e3bf 100755 --- a/windows/APIExample/APIExample/APIExample.rc +++ b/windows/APIExample/APIExample/APIExample.rc @@ -732,19 +732,6 @@ BEGIN PUSHBUTTON "Create track(Encode)",IDC_BUTTON_CREATE_TRACK_ENCODED,231,340,78,14 END -IDD_DIALOG_FACE_CAPTURE DIALOGEX 0, 0, 632, 400 -STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU -FONT 8, "MS Shell Dlg", 400, 0, 0x1 -BEGIN - LTEXT "",IDC_STATIC_VIDEO,1,0,484,311,NOT WS_VISIBLE - LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP - LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 - EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL - PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 - LTEXT "",IDC_STATIC_DETAIL,441,328,181,58 - EDITTEXT IDC_EDIT_METADATA_INFO,297,12,187,252,ES_MULTILINE | ES_READONLY | NOT WS_BORDER | WS_VSCROLL -END - IDD_DIALOG_JOINCHANNELVIDEOBYTOKEN DIALOGEX 0, 0, 632, 407 STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU FONT 8, "MS Shell Dlg", 400, 0, 0x1 @@ -1170,13 +1157,6 @@ BEGIN HORZGUIDE, 383 END - IDD_DIALOG_FACE_CAPTURE, DIALOG - BEGIN - RIGHTMARGIN, 630 - BOTTOMMARGIN, 397 - HORZGUIDE, 357 - END - IDD_DIALOG_JOINCHANNELVIDEOBYTOKEN, DIALOG BEGIN VERTGUIDE, 7 @@ -1361,11 +1341,6 @@ BEGIN 0 END -IDD_DIALOG_FACE_CAPTURE AFX_DIALOG_LAYOUT -BEGIN - 0 -END - IDD_DIALOG_JOINCHANNELVIDEOBYTOKEN AFX_DIALOG_LAYOUT BEGIN 0 diff --git a/windows/APIExample/APIExample/APIExample.vcxproj b/windows/APIExample/APIExample/APIExample.vcxproj index a241f1454..a76c495fd 100644 --- a/windows/APIExample/APIExample/APIExample.vcxproj +++ b/windows/APIExample/APIExample/APIExample.vcxproj @@ -295,7 +295,6 @@ if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIEx - @@ -354,7 +353,6 @@ if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIEx - @@ -460,4 +458,4 @@ if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIEx - \ No newline at end of file + diff --git a/windows/APIExample/APIExample/APIExample.vcxproj.filters b/windows/APIExample/APIExample/APIExample.vcxproj.filters index bc67fbe4b..d12ed8737 100644 --- a/windows/APIExample/APIExample/APIExample.vcxproj.filters +++ b/windows/APIExample/APIExample/APIExample.vcxproj.filters @@ -103,9 +103,6 @@ {a7d62827-b116-4a28-b8e7-2e5fddbce712} - - {53c31cfd-3175-427f-afd2-102e745ae081} - {f40fbcc5-c4fb-4581-a7b2-05cbcc5f737c} @@ -270,9 +267,6 @@ Header Files - - Advanced\FaceCapture - Basic\JoinChannelVideoByToken @@ -434,9 +428,6 @@ Source Files - - Advanced\FaceCapture - Basic\JoinChannelVideoByToken @@ -551,4 +542,4 @@ Resource Files - \ No newline at end of file + diff --git a/windows/APIExample/APIExample/APIExampleDlg.cpp b/windows/APIExample/APIExample/APIExampleDlg.cpp index a18d54388..528cf177c 100755 --- a/windows/APIExample/APIExample/APIExampleDlg.cpp +++ b/windows/APIExample/APIExample/APIExampleDlg.cpp @@ -237,7 +237,6 @@ void CAPIExampleDlg::InitSceneDialog() m_vecAdvanced.push_back(advancedOriginalVideo); m_vecAdvanced.push_back(advancedCustomAudioCapture); m_vecAdvanced.push_back(advancedOriginalAudio); - m_vecAdvanced.push_back(advancedFaceCapture); m_vecAdvanced.push_back(advancedCustomEncrypt); m_vecAdvanced.push_back(advancedMultiChannel); m_vecAdvanced.push_back(advancedMultiVideoSource); @@ -312,10 +311,6 @@ void CAPIExampleDlg::InitSceneDialog() m_pOriginalAudioDlg->Create(CAgoraOriginalAudioDlg::IDD); m_pOriginalAudioDlg->MoveWindow(&rcWnd); - m_pAgoraFaceCaptureDlg = new CAgoraFaceCaptureDlg(&m_staMainArea); - m_pAgoraFaceCaptureDlg->Create(CAgoraFaceCaptureDlg::IDD); - m_pAgoraFaceCaptureDlg->MoveWindow(&rcWnd); - //custom encrypt m_pCustomEncryptDlg = new CAgoraCustomEncryptDlg(&m_staMainArea); m_pCustomEncryptDlg->Create(CAgoraCustomEncryptDlg::IDD); @@ -596,10 +591,6 @@ void CAPIExampleDlg::CreateScene(CTreeCtrl& treeScene, CString selectedText) m_pOriginalAudioDlg->InitAgora(); m_pOriginalAudioDlg->ShowWindow(SW_SHOW); } - else if (selectedText.Compare(advancedFaceCapture) == 0) { - m_pAgoraFaceCaptureDlg->InitAgora(); - m_pAgoraFaceCaptureDlg->ShowWindow(SW_SHOW); - } else if (selectedText.Compare(advancedCustomEncrypt) == 0) { m_pCustomEncryptDlg->InitAgora(); m_pCustomEncryptDlg->ShowWindow(SW_SHOW); @@ -745,10 +736,6 @@ void CAPIExampleDlg::ReleaseScene(CTreeCtrl& treeScene, HTREEITEM& hSelectItem) m_pOriginalAudioDlg->UnInitAgora(); m_pOriginalAudioDlg->ShowWindow(SW_HIDE); } - else if (str.Compare(advancedFaceCapture) == 0) { - m_pAgoraFaceCaptureDlg->UnInitAgora(); - m_pAgoraFaceCaptureDlg->ShowWindow(SW_HIDE); - } else if (str.Compare(advancedCustomEncrypt) == 0) { m_pCustomEncryptDlg->UnInitAgora(); m_pCustomEncryptDlg->ShowWindow(SW_HIDE); diff --git a/windows/APIExample/APIExample/APIExampleDlg.h b/windows/APIExample/APIExample/APIExampleDlg.h index 1f3a9571b..585edee46 100755 --- a/windows/APIExample/APIExample/APIExampleDlg.h +++ b/windows/APIExample/APIExample/APIExampleDlg.h @@ -33,7 +33,6 @@ #include "Advanced/MediaRecorder/CAgoraMediaRecorder.h" #include "Advanced/PushExternalVideoYUV/PushExternalVideoYUV.h" #include "Advanced/MultiVideoSourceTracks/MultiVideoSourceTracks.h" -#include "Advanced/FaceCapture/CAgoraFaceCaptureDlg.h" #include "Advanced/TransparentBg/TransparentBgDialog.h" #include "Advanced/RtePlayer/RtePlayerDlg.h" #include "Advanced/CrossChannel/CAgoraCrossChannelDlg.h" @@ -112,7 +111,6 @@ class CAPIExampleDlg : public CDialogEx CAgoraSpatialAudioDlg *m_pSpatialAudioDlg = nullptr; PushExternalVideoYUV *m_pPushExternalVideoYUV = nullptr; MultiVideoSourceTracks *m_pMultiVideoSourceTracks = nullptr; - CAgoraFaceCaptureDlg *m_pAgoraFaceCaptureDlg = nullptr; CAgoraCrossChannelDlg *m_pAgoraCrossChannelDlg = nullptr; CMultipathDlg *m_pMultipathDlg = nullptr; CSimulcastDlg *m_pSimulcastDlg = nullptr; @@ -150,4 +148,3 @@ class CAPIExampleDlg : public CDialogEx virtual BOOL PreTranslateMessage(MSG* pMsg); afx_msg void OnClose(); }; - diff --git a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp index 20b6bb515..1b3136b01 100755 --- a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp @@ -517,7 +517,7 @@ void CAudioMixingEventHandler::onAudioMixingStateChanged(AUDIO_MIXING_STATE_TYPE void CAgoraAudioMixingDlg::OnBnClickedButtonMixingStart() { CString audioUrl = GetExePath() + _T("\\ID_MUSIC_01.m4a"); - int ret = m_rtcEngine->startAudioMixing(cs2utf8(audioUrl).c_str(), false, -1); + int ret = m_rtcEngine->startAudioMixing(cs2utf8(audioUrl).c_str(), false, -1, 0); CString strInfo; strInfo.Format(_T("startAudioMixing path:%s, ret:%d"), audioUrl.AllocSysString(), ret); diff --git a/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp b/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp index b3a57f402..d29e5b439 100644 --- a/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp +++ b/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp @@ -6,6 +6,26 @@ IMPLEMENT_DYNAMIC(CAgoraAudioProfile, CDialogEx) +namespace { +const AUDIO_PROFILE_TYPE kAudioProfiles[] = { + AUDIO_PROFILE_DEFAULT, + AUDIO_PROFILE_SPEECH_STANDARD, + AUDIO_PROFILE_MUSIC_STANDARD, + AUDIO_PROFILE_MUSIC_STANDARD_STEREO, + AUDIO_PROFILE_MUSIC_HIGH_QUALITY, + AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO, +}; + +const AUDIO_SCENARIO_TYPE kAudioScenarios[] = { + AUDIO_SCENARIO_DEFAULT, + AUDIO_SCENARIO_GAME_STREAMING, + AUDIO_SCENARIO_CHATROOM, + AUDIO_SCENARIO_CHORUS, + AUDIO_SCENARIO_MEETING, + AUDIO_SCENARIO_AI_CLIENT, +}; +} + CAgoraAudioProfile::CAgoraAudioProfile(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_DIALOG_AUDIO_PROFILE, pParent) { @@ -209,12 +229,11 @@ BOOL CAgoraAudioProfile::OnInitDialog() nIndex = 0; m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_DEFAULT")); - /*m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_CHATROOM_ENTERTAINMENT")); - m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_EDUCATION")); m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_GAME_STREAMING")); - m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_SHOWROOM")); - m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_HIGH_DEFINITION"));*/ - m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_CHATROOM_GAMING")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_CHATROOM")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_CHORUS")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_MEETING")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_AI_CLIENT")); ResumeStatus(); return TRUE; @@ -267,6 +286,12 @@ void CAgoraAudioProfile::OnBnClickedButtonSetAudioProfile() { int nProfileSel = m_cmbAudioProfile.GetCurSel(); int nScenSel = m_cmbAudioScenario.GetCurSel(); + if (nProfileSel < 0 || nProfileSel >= static_cast(_countof(kAudioProfiles)) || + nScenSel < 0 || nScenSel >= static_cast(_countof(kAudioScenarios))) + { + AfxMessageBox(_T("Select a valid audio profile and scenario")); + return; + } CString strInfo; CString strAudioProfile; CString strAudioScenario; @@ -274,16 +299,15 @@ void CAgoraAudioProfile::OnBnClickedButtonSetAudioProfile() m_cmbAudioScenario.GetWindowText(strAudioScenario); strInfo.Format(_T("Profile:%s,\nScenario:%s"), strAudioProfile, strAudioScenario); m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - AUDIO_SCENARIO_TYPE type = AUDIO_SCENARIO_DEFAULT; - if (nScenSel == 1) - type = AUDIO_SCENARIO_GAME_STREAMING; - // set audio profile. - m_rtcEngine->setAudioProfile((AUDIO_PROFILE_TYPE)nProfileSel, (AUDIO_SCENARIO_TYPE)nScenSel); + // ComboBox positions do not match the sparse SDK scenario values. + m_rtcEngine->setAudioProfile(kAudioProfiles[nProfileSel]); + m_rtcEngine->setAudioScenario(kAudioScenarios[nScenSel]); m_btnSetAudioProfile.SetWindowText(audioProfileCtrlUnSetAudioProfile); } else { - m_rtcEngine->setAudioProfile((AUDIO_PROFILE_TYPE)0, (AUDIO_SCENARIO_TYPE)0); + m_rtcEngine->setAudioProfile(AUDIO_PROFILE_DEFAULT); + m_rtcEngine->setAudioScenario(AUDIO_SCENARIO_DEFAULT); m_btnSetAudioProfile.SetWindowText(audioProfileCtrlSetAudioProfile); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("reset default audio profile")); } diff --git a/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp index 88024082e..994e3791f 100644 --- a/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp @@ -135,6 +135,24 @@ bool CAgoraCaptureAduioDlg::InitAgora() m_initialize = true; mediaEngine.queryInterface(m_rtcEngine, AGORA_IID_MEDIA_ENGINE); + if (!mediaEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("query media engine failed")); + m_rtcEngine->release(nullptr); + m_rtcEngine = nullptr; + m_initialize = false; + return false; + } + AudioTrackConfig audioTrackConfig; + audioTrackConfig.enableLocalPlayback = false; + m_customAudioTrackId = mediaEngine->createCustomAudioTrack(AUDIO_TRACK_MIXABLE, audioTrackConfig); + if (m_customAudioTrackId == INVALID_TRACK_ID) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("create custom audio track failed")); + mediaEngine.reset(); + m_rtcEngine->release(nullptr); + m_rtcEngine = nullptr; + m_initialize = false; + return false; + } m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); //enable video in the engine. m_rtcEngine->enableVideo(); @@ -150,6 +168,7 @@ bool CAgoraCaptureAduioDlg::InitAgora() m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); m_agAudioCaptureDevice.engine_ = m_rtcEngine; + m_agAudioCaptureDevice.audioTrackId_ = m_customAudioTrackId; return true; } @@ -171,6 +190,12 @@ void CAgoraCaptureAduioDlg::UnInitAgora() m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); m_agAudioCaptureDevice.Stop(); m_agAudioCaptureDevice.engine_ = NULL; + m_agAudioCaptureDevice.audioTrackId_ = INVALID_TRACK_ID; + if (m_customAudioTrackId != INVALID_TRACK_ID) { + mediaEngine->destroyCustomAudioTrack(m_customAudioTrackId); + m_customAudioTrackId = INVALID_TRACK_ID; + } + mediaEngine.reset(); //release engine. if (m_initialize) { m_rtcEngine->release(nullptr); @@ -254,7 +279,9 @@ void CAgoraCaptureAduioDlg::OnBnClickedButtonJoinchannel() } std::string szChannelId = cs2utf8(strChannelName); ChannelMediaOptions option; - option.publishCustomAudioTrack = true; + option.publishMicrophoneTrack = !m_extenalCaptureAudio; + option.publishCustomAudioTrack = m_extenalCaptureAudio; + option.publishCustomAudioTrackId = static_cast(m_customAudioTrackId); option.publishCameraTrack = true; option.autoSubscribeAudio = true; option.autoSubscribeVideo = true; @@ -275,11 +302,9 @@ void CAgoraCaptureAduioDlg::OnBnClickedButtonJoinchannel() // start or stop capture. // if bEnable is true start capture otherwise stop capture. -void CAgoraCaptureAduioDlg::EnableCaputre(BOOL bEnable) { - - SIZE_T nBufferSize = 0; +BOOL CAgoraCaptureAduioDlg::EnableCaputre(BOOL bEnable) { if (bEnable == (BOOL)m_extenalCaptureAudio) - return; + return TRUE; if (bEnable) { //select media capture. @@ -288,15 +313,17 @@ void CAgoraCaptureAduioDlg::EnableCaputre(BOOL bEnable) { m_agAudioCaptureDevice.InitAudioFrame(); //create audio capture filter. if (!m_agAudioCaptureDevice.CreateCaptureFilter()) - return; + return FALSE; //start audio capture. - m_agAudioCaptureDevice.Start(); + if (!m_agAudioCaptureDevice.Start()) + return FALSE; } else { //stop audio capture. m_agAudioCaptureDevice.Stop(); } - m_extenalCaptureAudio = !m_extenalCaptureAudio; + m_extenalCaptureAudio = bEnable; + return TRUE; } void CAgoraCaptureAduioDlg::PullAudioFrameThread(CAgoraCaptureAduioDlg * self) @@ -337,24 +364,27 @@ void CAgoraCaptureAduioDlg::PullAudioFrameThread(CAgoraCaptureAduioDlg * self) void CAgoraCaptureAduioDlg::OnBnClickedButtonStartCaputre() { if ( !m_extenalCaptureAudio ){ + if (!EnableCaputre(TRUE)) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start external audio capture failed")); + return; + } + if (!EnableExtendAudioCapture(TRUE)) { + EnableCaputre(FALSE); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("publish external audio track failed")); + return; + } m_btnSetAudioCtx.SetWindowText(customAudioCaptureCtrlCancelExternlCapture); - //use external audio source. - EnableExtendAudioCapture(TRUE); - //start capture - EnableCaputre(TRUE); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("use external audio source")); - // m_agAudioCaptureDevice.SetCaptureDlg(this); } else { - m_btnSetAudioCtx.SetWindowText(customAudioCaptureCtrlSetExternlCapture); - //use inner audio source. - EnableExtendAudioCapture(FALSE); - //stop capture. + if (!EnableExtendAudioCapture(FALSE)) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("publish microphone track failed")); + return; + } EnableCaputre(FALSE); + m_btnSetAudioCtx.SetWindowText(customAudioCaptureCtrlSetExternlCapture); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("use inner audio source")); - // m_agAudioCaptureDevice.SetCaptureDlg(nullptr); } - } @@ -366,14 +396,14 @@ void CAgoraCaptureAduioDlg::OnBnClickedButtonStartCaputre() */ BOOL CAgoraCaptureAduioDlg::EnableExtendAudioCapture(BOOL bEnable) { - agora::util::AutoPtr mediaEngine; - //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. - mediaEngine.queryInterface(m_rtcEngine, AGORA_IID_MEDIA_ENGINE); - int nRet = 0; - if ( bEnable ) - nRet = mediaEngine->setExternalAudioSource(true,m_agAudioCaptureDevice.m_audioFrame.samplesPerSec , m_agAudioCaptureDevice.m_audioFrame.channels); - else - nRet = mediaEngine->setExternalAudioSource(false, m_agAudioCaptureDevice.m_audioFrame.samplesPerSec, m_agAudioCaptureDevice.m_audioFrame.channels); + if (!m_joinChannel) + return TRUE; + + ChannelMediaOptions options; + options.publishMicrophoneTrack = !bEnable; + options.publishCustomAudioTrack = bEnable; + options.publishCustomAudioTrackId = static_cast(m_customAudioTrackId); + int nRet = m_rtcEngine->updateChannelMediaOptions(options); return nRet == 0 ? TRUE : FALSE; } @@ -645,5 +675,3 @@ BOOL CAgoraCaptureAduioDlg::PreTranslateMessage(MSG* pMsg) } return CDialogEx::PreTranslateMessage(pMsg); } - - diff --git a/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h index f9ce69a73..c1e11282e 100644 --- a/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h +++ b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h @@ -128,7 +128,7 @@ class CAgoraCaptureAduioDlg : public CDialogEx void ResumeStatus(); // start or stop capture. // if bEnable is true start capture otherwise stop capture. - void EnableCaputre(BOOL bEnable); + BOOL EnableCaputre(BOOL bEnable); bool m_joinChannel = false; bool m_initialize = false; @@ -137,6 +137,7 @@ class CAgoraCaptureAduioDlg : public CDialogEx bool m_extenalRenderAudio = false; IRtcEngine* m_rtcEngine = nullptr; agora::util::AutoPtr mediaEngine; + agora::rtc::track_id_t m_customAudioTrackId = agora::rtc::INVALID_TRACK_ID; CAGVideoWnd m_localVideoWnd; CAgoraCaptureAduioDlgEngineEventHandler m_eventHandler; diff --git a/windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.cpp b/windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.cpp deleted file mode 100644 index 46a4f5edd..000000000 --- a/windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.cpp +++ /dev/null @@ -1,632 +0,0 @@ -#include "stdafx.h" -#include "APIExample.h" -#include "CAgoraFaceCaptureDlg.h" - - - -IMPLEMENT_DYNAMIC(CAgoraFaceCaptureDlg, CDialogEx) - -CAgoraFaceCaptureDlg::CAgoraFaceCaptureDlg(CWnd* pParent /*=nullptr*/) - : CDialogEx(CAgoraFaceCaptureDlg::IDD, pParent) -{ - -} - -CAgoraFaceCaptureDlg::~CAgoraFaceCaptureDlg() -{ -} - -//Initialize the Ctrl Text. -void CAgoraFaceCaptureDlg::InitCtrlText() -{ - m_staChannel.SetWindowText(commonCtrlChannel); - m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); -} - - -//Initialize the Agora SDK -bool CAgoraFaceCaptureDlg::InitAgora() -{ - //create Agora RTC engine - m_rtcEngine = createAgoraRtcEngine(); - if (!m_rtcEngine) { - m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); - return false; - } - //set message notify receiver window - m_eventHandler.SetMsgReceiver(m_hWnd); - - RtcEngineContext context; - std::string strAppID = GET_APP_ID; - context.appId = strAppID.c_str(); - context.eventHandler = &m_eventHandler; - //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. - context.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING; - //initialize the Agora RTC engine context. - int ret = m_rtcEngine->initialize(context); - if (ret != 0) { - m_initialize = false; - CString strInfo; - strInfo.Format(_T("initialize failed: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - return false; - } - else - m_initialize = true; - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); - //enable video in the engine. - m_rtcEngine->enableVideo(); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); - - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); - //set client role in the engine to the CLIENT_ROLE_BROADCASTER. - m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); - - // load face capture extension - ret = m_rtcEngine->loadExtensionProvider("libagora_face_capture_extension.dll"); - CString strInfo; - strInfo.Format(_T("Load face capture extension ret: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - // enable face capture - ret = m_rtcEngine->enableExtension("agora_video_filters_face_capture", "face_capture", true, MEDIA_SOURCE_TYPE::PRIMARY_CAMERA_SOURCE); - strInfo.Format(_T("Enable face capture extension ret: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - // face capture authentication - CString strAuth; - strAuth.Format(_T("{\"company_id\":\"%s\",\"license\":\"%s\"}"), _T("agoraTest"), utf82cs(std::string(FACE_CAPTURE_LICENSE))); - ret = m_rtcEngine->setExtensionProperty("agora_video_filters_face_capture", - "face_capture", - "authentication_information", - cs2utf8(strAuth).c_str(), - MEDIA_SOURCE_TYPE::PRIMARY_CAMERA_SOURCE); - strInfo.Format(_T("Auth face capture ret: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - // load speech driven extension - ret = m_rtcEngine->loadExtensionProvider("libagora_lip_sync_extension.dll"); - strInfo.Format(_T("Load speech driven extension ret: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - // enable speech driven - ret = m_rtcEngine->enableExtension("agora_filters_lip_sync", "lip_sync", true, MEDIA_SOURCE_TYPE::SPEECH_DRIVEN_VIDEO_SOURCE); - strInfo.Format(_T("Enable speech driven extension ret: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - // speech driven authentication - strAuth.Format(_T("{\"company_id\":\"%s\",\"license\":\"%s\",\"open_agc\":true}"), _T("agoraTest"), utf82cs(std::string(FACE_CAPTURE_LICENSE))); - ret = m_rtcEngine->setExtensionProperty("agora_filters_lip_sync", - "lip_sync", - "parameters", - cs2utf8(strAuth).c_str(), - MEDIA_SOURCE_TYPE::SPEECH_DRIVEN_VIDEO_SOURCE); - strInfo.Format(_T("Auth speech driven ret: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - - m_videoFrameObserver.SetMsgReceiver(m_hWnd); - RegisterVideoFrameObserver(TRUE, &m_videoFrameObserver); - m_faceInfoObserver.SetMsgReceiver(m_hWnd); - RegisterFaceInfoObserver(TRUE, &m_faceInfoObserver); - - return true; -} - - -//UnInitialize the Agora SDK -void CAgoraFaceCaptureDlg::UnInitAgora() -{ - if (m_rtcEngine) { - if (m_joinChannel) - //leave channel - m_joinChannel = !m_rtcEngine->leaveChannel(); - //stop preview in the engine. - m_rtcEngine->stopPreview(); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); - //disable video in the engine. - m_rtcEngine->disableVideo(); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); - RegisterVideoFrameObserver(FALSE); - m_videoFrameObserver.SetMsgReceiver(nullptr); - RegisterFaceInfoObserver(FALSE); - m_faceInfoObserver.SetMsgReceiver(nullptr); - //release engine. - if (m_initialize) { - m_rtcEngine->release(nullptr); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); - } - m_rtcEngine = NULL; - } -} - -//render local video from SDK local capture. -void CAgoraFaceCaptureDlg::RenderLocalVideo() -{ - if (m_rtcEngine) { - - VideoCanvas canvas; - canvas.renderMode = media::base::RENDER_MODE_FIT; - canvas.uid = 0; - canvas.sourceType = VIDEO_SOURCE_CAMERA_PRIMARY; - canvas.view = m_localVideoWnd.GetSafeHwnd(); - //setup local video in the engine to canvas. - m_rtcEngine->setupLocalVideo(canvas); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); - } -} - - -//resume window status -void CAgoraFaceCaptureDlg::ResumeStatus() -{ - InitCtrlText(); - m_staDetail.SetWindowText(_T("")); - m_edtChannel.SetWindowText(_T("")); - m_edtMetadataInfo.SetWindowText(_T("")); - m_btnJoinChannel.EnableWindow(TRUE); - m_lstInfo.ResetContent(); - m_joinChannel = false; - m_initialize = false; -} - -void CAgoraFaceCaptureDlg::DoDataExchange(CDataExchange* pDX) -{ - CDialogEx::DoDataExchange(pDX); - DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); - DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); - DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); - DDX_Control(pDX, IDC_EDIT_METADATA_INFO, m_edtMetadataInfo); - DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); - DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); - DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); -} - - -BEGIN_MESSAGE_MAP(CAgoraFaceCaptureDlg, CDialogEx) - ON_WM_SHOWWINDOW() - ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraFaceCaptureDlg::OnEIDJoinChannelSuccess) - ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraFaceCaptureDlg::OnEIDLeaveChannel) - ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraFaceCaptureDlg::OnEIDUserJoined) - ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraFaceCaptureDlg::OnEIDUserOffline) - ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANGED), &CAgoraFaceCaptureDlg::OnEIDRemoteVideoStateChanged) - ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraFaceCaptureDlg::OnBnClickedButtonJoinchannel) - ON_MESSAGE(WM_MSGID(EID_EXTENSION_EVENT), &CAgoraFaceCaptureDlg::OnEIDExtensionEvent) - ON_MESSAGE(WM_MSGID(EID_ON_CAPTURE_VIDEOFRAME), &CAgoraFaceCaptureDlg::OnEIDonCaptureVideoFrame) - ON_MESSAGE(WM_MSGID(EID_ON_FACE_INFO), &CAgoraFaceCaptureDlg::OnEIDonFaceInfo) - ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraFaceCaptureDlg::OnSelchangeListInfoBroadcasting) -END_MESSAGE_MAP() - - - - -void CAgoraFaceCaptureDlg::OnShowWindow(BOOL bShow, UINT nStatus) -{ - CDialogEx::OnShowWindow(bShow, nStatus); - if (bShow)//bShwo is true ,show window - { - InitCtrlText(); - RenderLocalVideo(); - } - else { - ResumeStatus(); - } -} - - -BOOL CAgoraFaceCaptureDlg::OnInitDialog() -{ - CDialogEx::OnInitDialog(); - m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); - RECT rcArea; - m_staVideoArea.GetClientRect(&rcArea); - m_localVideoWnd.MoveWindow(&rcArea); - m_localVideoWnd.ShowWindow(SW_SHOW); - ResumeStatus(); - return TRUE; -} - - -/* - register or unregister agora video Frame Observer. -*/ -BOOL CAgoraFaceCaptureDlg::RegisterVideoFrameObserver(BOOL bEnable, agora::media::IVideoFrameObserver * videoFrameObserver) -{ - agora::util::AutoPtr mediaEngine; - //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. - mediaEngine.queryInterface(m_rtcEngine, AGORA_IID_MEDIA_ENGINE); - int nRet = 0; - if (mediaEngine.get() == NULL) - return FALSE; - if (bEnable) { - //register agora video frame observer. - nRet = mediaEngine->registerVideoFrameObserver(videoFrameObserver); - } - else { - //unregister agora video frame observer. - nRet = mediaEngine->registerVideoFrameObserver(nullptr); - } - return nRet == 0 ? TRUE : FALSE; -} - -BOOL CAgoraFaceCaptureDlg::RegisterFaceInfoObserver(BOOL bEnable, agora::media::IFaceInfoObserver* faceInfoObserver) -{ - agora::util::AutoPtr mediaEngine; - //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. - mediaEngine.queryInterface(m_rtcEngine, AGORA_IID_MEDIA_ENGINE); - int nRet = 0; - if (mediaEngine.get() == NULL) - return FALSE; - if (bEnable) { - //register agora video frame observer. - nRet = mediaEngine->registerFaceInfoObserver(faceInfoObserver); - } - else { - //unregister agora video frame observer. - nRet = mediaEngine->registerFaceInfoObserver(nullptr); - } - return nRet == 0 ? TRUE : FALSE; -} - -//click button handler to join channel or leave channel. -void CAgoraFaceCaptureDlg::OnBnClickedButtonJoinchannel() -{ - if (!m_rtcEngine || !m_initialize) - return; - CString strInfo; - if (!m_joinChannel) { - CString strChannelName; - m_edtChannel.GetWindowText(strChannelName); - if (strChannelName.IsEmpty()) { - AfxMessageBox(_T("Fill channel name first")); - return; - } - std::string szChannelId = cs2utf8(strChannelName); - ChannelMediaOptions options; - options.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING; - options.clientRoleType = CLIENT_ROLE_BROADCASTER; - //join channel in the engine. - if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), 0, options)) { - strInfo.Format(_T("join channel %s, use ChannelMediaOptions"), getCurrentTime()); - m_btnJoinChannel.EnableWindow(FALSE); - } - } - else { - //leave channel in the engine. - if (0 == m_rtcEngine->leaveChannel()) { - strInfo.Format(_T("leave channel %s"), getCurrentTime()); - } - } - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); -} - -void CAgoraFaceCaptureDlg::OnSelchangeListInfoBroadcasting() { - int sel = m_lstInfo.GetCurSel(); - if (sel < 0)return; - CString strDetail; - m_lstInfo.GetText(sel, strDetail); - m_staDetail.SetWindowText(strDetail); -} - -//EID_JOINCHANNEL_SUCCESS message window handler -LRESULT CAgoraFaceCaptureDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) -{ - m_joinChannel = true; - m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); - m_btnJoinChannel.EnableWindow(TRUE); - CString strInfo; - strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - m_localVideoWnd.SetUID(wParam); - //notify parent window - ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); - return 0; -} - -//EID_LEAVEHANNEL_SUCCESS message window handler -LRESULT CAgoraFaceCaptureDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) -{ - m_joinChannel = false; - m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); - CString strInfo; - strInfo.Format(_T("leave channel success %s"), getCurrentTime()); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); - return 0; -} - -//EID_USER_JOINED message window handler -LRESULT CAgoraFaceCaptureDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) -{ - CString strInfo; - strInfo.Format(_T("%u joined"), wParam); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - return 0; -} - -//EID_USER_OFFLINE message handler. -LRESULT CAgoraFaceCaptureDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) -{ - uid_t remoteUid = (uid_t)wParam; - CString strInfo; - strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - return 0; -} - -//EID_REMOTE_VIDEO_STATE_CHANGED message window handler. -LRESULT CAgoraFaceCaptureDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) -{ - PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; - if (stateChanged) { - //onRemoteVideoStateChanged - CString strSateInfo; - switch (stateChanged->state) { - case REMOTE_VIDEO_STATE_STARTING: - strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); - break; - case REMOTE_VIDEO_STATE_STOPPED: - strSateInfo = _T("strSateInfo"); - break; - case REMOTE_VIDEO_STATE_DECODING: - strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); - break; - case REMOTE_VIDEO_STATE_FAILED: - strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); - break; - case REMOTE_VIDEO_STATE_FROZEN: - strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); - break; - } - CString strInfo; - strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - } - return 0; -} - - -LRESULT CAgoraFaceCaptureDlg::OnEIDExtensionEvent(WPARAM wParam, LPARAM lParam) -{ - PExtensionEvent event = (PExtensionEvent)wParam; - - CString strInfo; - strInfo.Format(_T("OnEIDExtensionEvent: provicder=%s, extension=%s, key=%s, value=%s"), - utf82cs(event->provider), - utf82cs(event->extension), - utf82cs(event->key), - utf82cs(event->value)); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - if (strcmp(event->provider, "agora_video_filters_face_capture") == 0 - && strcmp(event->extension, "face_capture") == 0 - && strcmp(event->key, "authentication_state") == 0) { - if (strcmp(event->value, "0") == 0) { - // authentication successful. - strInfo.Format(_T("Face caputure authentication successful.")); - } - else if (strcmp(event->value, "-1") == 0) { - // authentication failure. - strInfo.Format(_T("Face caputure authentication failure.")); - } - else if (strcmp(event->value, "-2") == 0) { - // authentication unset. - strInfo.Format(_T("Face caputure authentication unset.")); - } - } - else if (strcmp(event->provider, "agora_filters_lip_sync") == 0 - && strcmp(event->extension, "lip_sync") == 0 - && strcmp(event->key, "status_code") == 0) { - if (strcmp(event->value, "0") == 0) { - // authentication successful. - strInfo.Format(_T("Speech driven authentication successful.")); - } - else { - strInfo.Format(_T("Speech driven authentication failed. code=%s"), event->value); - } - } - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); - - delete[] event->provider; - delete[] event->extension; - delete[] event->key; - delete[] event->value; - delete event; - return 0; -} - - -LRESULT CAgoraFaceCaptureDlg::OnEIDonCaptureVideoFrame(WPARAM wParam, LPARAM lParam) -{ - char* metedata = (char*)wParam; - - // m_edtMetadataInfo.SetWindowTextW(utf82cs(std::string(metedata))); - - delete[] metedata; - return 0; -} - -LRESULT CAgoraFaceCaptureDlg::OnEIDonFaceInfo(WPARAM wParam, LPARAM lParam) -{ - char* faceInfo = (char*)wParam; - - m_edtMetadataInfo.SetWindowTextW(utf82cs(std::string(faceInfo))); - delete[] faceInfo; - return 0; -} - -/* -note: - Join the channel callback.This callback method indicates that the client - successfully joined the specified channel.Channel ids are assigned based - on the channel name specified in the joinChannel. If IRtcEngine::joinChannel - is called without a user ID specified. The server will automatically assign one -parameters: - channel:channel name. - uid: user ID。If the UID is specified in the joinChannel, that ID is returned here; - Otherwise, use the ID automatically assigned by the Agora server. - elapsed: The Time from the joinChannel until this event occurred (ms). -*/ -void FaceCaptureEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) -{ - if (m_hMsgHanlder) { - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); - } -} -/* -note: - In the live broadcast scene, each anchor can receive the callback - of the new anchor joining the channel, and can obtain the uID of the anchor. - Viewers also receive a callback when a new anchor joins the channel and - get the anchor's UID.When the Web side joins the live channel, the SDK will - default to the Web side as long as there is a push stream on the - Web side and trigger the callback. -parameters: - uid: remote user/anchor ID for newly added channel. - elapsed: The joinChannel is called from the local user to the delay triggered - by the callback(ms). -*/ -void FaceCaptureEventHandler::onUserJoined(uid_t uid, int elapsed) -{ - if (m_hMsgHanlder) { - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); - } -} - -/* -note: - Remote user (communication scenario)/anchor (live scenario) is called back from - the current channel.A remote user/anchor has left the channel (or dropped the line). - There are two reasons for users to leave the channel, namely normal departure and - time-out:When leaving normally, the remote user/anchor will send a message like - "goodbye". After receiving this message, determine if the user left the channel. - The basis of timeout dropout is that within a certain period of time - (live broadcast scene has a slight delay), if the user does not receive any - packet from the other side, it will be judged as the other side dropout. - False positives are possible when the network is poor. We recommend using the - Agora Real-time messaging SDK for reliable drop detection. -parameters: - uid: The user ID of an offline user or anchor. - reason:Offline reason: USER_OFFLINE_REASON_TYPE. -*/ -void FaceCaptureEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) -{ - if (m_hMsgHanlder) { - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); - } -} -/* -note: - When the App calls the leaveChannel method, the SDK indicates that the App - has successfully left the channel. In this callback method, the App can get - the total call time, the data traffic sent and received by THE SDK and other - information. The App obtains the call duration and data statistics received - or sent by the SDK through this callback. -parameters: - stats: Call statistics. -*/ - -void FaceCaptureEventHandler::onLeaveChannel(const RtcStats& stats) -{ - if (m_hMsgHanlder) { - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); - } -} -/** - Occurs when the remote video state changes. - @note 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. See #REMOTE_VIDEO_STATE. - @param reason The reason of the remote video state change. See - #REMOTE_VIDEO_STATE_REASON. - @param elapsed Time elapsed (ms) from the local user calling the - \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the - SDK triggers this callback. -*/ -void FaceCaptureEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) -{ - if (m_hMsgHanlder) { - PVideoStateStateChanged stateChanged = new VideoStateStateChanged; - stateChanged->uid = uid; - stateChanged->reason = reason; - stateChanged->state = state; - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANGED), (WPARAM)stateChanged, 0); - } -} - -void FaceCaptureEventHandler::onExtensionEventWithContext(const ExtensionContext& context, const char* key, const char* value) -{ - if (m_hMsgHanlder) { - - const char* provider = context.providerName; - const char* extension = context.extensionName; - - PExtensionEvent evnet = new ExtensionEvent(); - int len = strlen(provider); - evnet->provider = new char[len + 1]; - evnet->provider[len] = 0; - strcpy_s(evnet->provider, len + 1, provider); - - - len = strlen(extension); - evnet->extension = new char[len + 1]; - evnet->extension[len] = 0; - strcpy_s(evnet->extension, len + 1, extension); - - len = strlen(key); - evnet->key = new char[len + 1]; - evnet->key[len] = 0; - strcpy_s(evnet->key, len + 1, key); - - len = strlen(value); - evnet->value = new char[len + 1]; - evnet->value[len] = 0; - strcpy_s(evnet->value, len + 1, value); - - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_EXTENSION_EVENT), (WPARAM)evnet, 0); - } -} - -bool FaceCaptureVideoFrameObserver::onCaptureVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE type, VideoFrame& videoFrame) { - if (m_hMsgHanlder) { - - if (videoFrame.metaInfo != NULL) { - const char* metaInfo = videoFrame.metaInfo->getMetaInfoStr(media::base::IVideoFrameMetaInfo::META_INFO_KEY::KEY_FACE_CAPTURE); - if (metaInfo != NULL) { - int len = strlen(metaInfo); - char* out = new char[len + 1]; - out[len] = 0; - strcpy_s(out, len + 1, metaInfo); - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_ON_CAPTURE_VIDEOFRAME), (WPARAM)out, 0); - } - } - - } - return true; -} - -bool FaceCaptureFaceInfoObserver::onFaceInfo(const char* outFaceInfo) { - if (m_hMsgHanlder && outFaceInfo) { - - int len = strlen(outFaceInfo); - char* out = new char[len + 1]; - out[len] = 0; - strcpy_s(out, len + 1, outFaceInfo); - ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_ON_FACE_INFO), (WPARAM)out, 0); - - } - return true; -} - - -BOOL CAgoraFaceCaptureDlg::PreTranslateMessage(MSG* pMsg) -{ - if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { - return TRUE; - } - return CDialogEx::PreTranslateMessage(pMsg); -} - - - diff --git a/windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.h b/windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.h deleted file mode 100644 index 241739aa4..000000000 --- a/windows/APIExample/APIExample/Advanced/FaceCapture/CAgoraFaceCaptureDlg.h +++ /dev/null @@ -1,223 +0,0 @@ -#pragma once -#include "AGVideoWnd.h" - - -#define FACE_CAPTURE_LICENSE "0qIR1q9+df9pr6ha8ESiBoj51vKDGOvAILh8tO5AyGaxaUteIAzkAQlIitWIGypt1y8sDzZy/wYRz/q8FxKDwkev6PvON4z7sHTpQMPXFTbJgoGQsBvbY4zRWC0o1raLlvnA+Rui15ySFTQCgsM8QnsyF/gGPNEy639N8OaUbj0=" - -// Video processing Frame Observer -class FaceCaptureVideoFrameObserver : - public agora::media::IVideoFrameObserver -{ -public: - // set the message notify window handler - void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } - - virtual ~FaceCaptureVideoFrameObserver() { } - /** - * Occurs each time the SDK receives a video frame captured by the local camera. - * - * After you successfully register the video frame observer, the SDK triggers this callback each time - * a video frame is received. In this callback, you can get the video data captured by the local - * camera. You can then pre-process the data according to your scenarios. - * - * After pre-processing, you can send the processed video data back to the SDK by setting the - * `videoFrame` parameter in this callback. - * - * @param videoFrame A pointer to the video frame: VideoFrame - * @return Determines whether to ignore the current video frame if the pre-processing fails: - * - true: Do not ignore. - * - false: Ignore, in which case this method does not sent the current video frame to the SDK. - */ - virtual bool onCaptureVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE type, VideoFrame& videoFrame) override; - - /** - * Occurs each time the SDK receives a video frame sent by the remote user. - * - * After you successfully register the video frame observer, the SDK triggers this callback each time a - * video frame is received. In this callback, you can get the video data sent by the remote user. You - * can then post-process the data according to your scenarios. - * - * After post-processing, you can send the processed data back to the SDK by setting the `videoFrame` - * parameter in this callback. - * - * @param uid ID of the remote user who sends the current video frame. - * @param connectionId ID of the connection. - * @param videoFrame A pointer to the video frame: VideoFrame - * @return Determines whether to ignore the current video frame if the post-processing fails: - * - true: Do not ignore. - * - false: Ignore, in which case this method does not sent the current video frame to the SDK. - */ - - virtual bool onRenderVideoFrame(const char* channelId, rtc::uid_t remoteUid, VideoFrame& videoFrame) { return true; } - - virtual bool onTranscodedVideoFrame(VideoFrame& videoFrame)override { return true; } - virtual bool onMediaPlayerVideoFrame(VideoFrame& videoFrame, int mediaPlayerId) override { return true; } - virtual bool onPreEncodeVideoFrame(agora::rtc::VIDEO_SOURCE_TYPE type, VideoFrame& videoFrame) { return true; } - -private: - HWND m_hMsgHanlder; -}; - -class FaceCaptureFaceInfoObserver : - public agora::media::IFaceInfoObserver -{ - -public: - - // set the message notify window handler - void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } - - /** - * Occurs when the face info is received. - * @param outFaceInfo The output face info. - * @return - * - true: The face info is valid. - * - false: The face info is invalid. - */ - virtual bool onFaceInfo(const char* outFaceInfo) override; - -private: - HWND m_hMsgHanlder; -}; - - - -class FaceCaptureEventHandler : public IRtcEngineEventHandler -{ -public: - //set the message notify window handler - void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } - - /* - note: - Join the channel callback.This callback method indicates that the client - successfully joined the specified channel.Channel ids are assigned based - on the channel name specified in the joinChannel. If IRtcEngine::joinChannel - is called without a user ID specified. The server will automatically assign one - parameters: - channel:channel name. - uid: user ID。If the UID is specified in the joinChannel, that ID is returned here; - Otherwise, use the ID automatically assigned by the Agora server. - elapsed: The Time from the joinChannel until this event occurred (ms). - */ - virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; - /* - note: - In the live broadcast scene, each anchor can receive the callback - of the new anchor joining the channel, and can obtain the uID of the anchor. - Viewers also receive a callback when a new anchor joins the channel and - get the anchor's UID.When the Web side joins the live channel, the SDK will - default to the Web side as long as there is a push stream on the - Web side and trigger the callback. - parameters: - uid: remote user/anchor ID for newly added channel. - elapsed: The joinChannel is called from the local user to the delay triggered - by the callback(ms). - */ - virtual void onUserJoined(uid_t uid, int elapsed) override; - /* - note: - Remote user (communication scenario)/anchor (live scenario) is called back from - the current channel.A remote user/anchor has left the channel (or dropped the line). - There are two reasons for users to leave the channel, namely normal departure and - time-out:When leaving normally, the remote user/anchor will send a message like - "goodbye". After receiving this message, determine if the user left the channel. - The basis of timeout dropout is that within a certain period of time - (live broadcast scene has a slight delay), if the user does not receive any - packet from the other side, it will be judged as the other side dropout. - False positives are possible when the network is poor. We recommend using the - Agora Real-time messaging SDK for reliable drop detection. - parameters: - uid: The user ID of an offline user or anchor. - reason:Offline reason: USER_OFFLINE_REASON_TYPE. - */ - virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; - /* - note: - When the App calls the leaveChannel method, the SDK indicates that the App - has successfully left the channel. In this callback method, the App can get - the total call time, the data traffic sent and received by THE SDK and other - information. The App obtains the call duration and data statistics received - or sent by the SDK through this callback. - parameters: - stats: Call statistics. - */ - virtual void onLeaveChannel(const RtcStats& stats) override; - /** - Occurs when the remote video state changes. - @note 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. See #REMOTE_VIDEO_STATE. - @param reason The reason of the remote video state change. See - #REMOTE_VIDEO_STATE_REASON. - @param elapsed Time elapsed (ms) from the local user calling the - \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the - SDK triggers this callback. - */ - virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; - - - virtual void onExtensionEventWithContext(const ExtensionContext& context, const char* key, const char* value) override; -private: - HWND m_hMsgHanlder; -}; - - - -class CAgoraFaceCaptureDlg : public CDialogEx -{ - DECLARE_DYNAMIC(CAgoraFaceCaptureDlg) - -public: - CAgoraFaceCaptureDlg(CWnd* pParent = nullptr); - virtual ~CAgoraFaceCaptureDlg(); - - enum { IDD = IDD_DIALOG_FACE_CAPTURE }; -public: - //Initialize the Ctrl Text. - void InitCtrlText(); - //Initialize the Agora SDK - bool InitAgora(); - //UnInitialize the Agora SDK - void UnInitAgora(); - //render local video from SDK local capture. - void RenderLocalVideo(); - //resume window status - void ResumeStatus(); - //register or unregister agora video Frame Observer. - BOOL RegisterVideoFrameObserver(BOOL bEnable, agora::media::IVideoFrameObserver * videoFrameObserver = NULL); - BOOL RegisterFaceInfoObserver(BOOL bEnable, agora::media::IFaceInfoObserver* faceInfoObserver = NULL); -private: - bool m_joinChannel = false; - bool m_initialize = false; - IRtcEngine* m_rtcEngine = nullptr; - CAGVideoWnd m_localVideoWnd; - FaceCaptureEventHandler m_eventHandler; - FaceCaptureVideoFrameObserver m_videoFrameObserver; - FaceCaptureFaceInfoObserver m_faceInfoObserver; -protected: - virtual void DoDataExchange(CDataExchange* pDX); - LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDExtensionEvent(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDonCaptureVideoFrame(WPARAM wParam, LPARAM lParam); - LRESULT OnEIDonFaceInfo(WPARAM wParam, LPARAM lParam); - DECLARE_MESSAGE_MAP() -public: - CStatic m_staVideoArea; - CStatic m_staChannel; - CEdit m_edtChannel; - CButton m_btnJoinChannel; - CListBox m_lstInfo; - CStatic m_staDetail; - CEdit m_edtMetadataInfo; - afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); - virtual BOOL OnInitDialog(); - virtual BOOL PreTranslateMessage(MSG* pMsg); - afx_msg void OnBnClickedButtonJoinchannel(); - afx_msg void OnSelchangeListInfoBroadcasting(); -}; diff --git a/windows/APIExample/APIExample/Advanced/LocalVideoTranscoding/CLocalVideoTranscodingDlg.cpp b/windows/APIExample/APIExample/Advanced/LocalVideoTranscoding/CLocalVideoTranscodingDlg.cpp index e6612339d..68a9b7fdf 100755 --- a/windows/APIExample/APIExample/Advanced/LocalVideoTranscoding/CLocalVideoTranscodingDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/LocalVideoTranscoding/CLocalVideoTranscodingDlg.cpp @@ -10,6 +10,30 @@ IMPLEMENT_DYNAMIC(CLocalVideoTranscodingDlg, CDialogEx) +namespace { +bool GetPrimaryDisplayId(IRtcEngine* rtcEngine, int64_t& displayId) +{ + SIZE sourceSize = { 64, 64 }; + IScreenCaptureSourceList* sources = rtcEngine->getScreenCaptureSources(sourceSize, sourceSize, true); + if (!sources) + return false; + + bool found = false; + for (unsigned int i = 0; i < sources->getCount(); ++i) { + ScreenCaptureSourceInfo source = sources->getSourceInfo(i); + if (source.type != ScreenCaptureSourceType_Screen) + continue; + + displayId = source.sourceId; + found = true; + if (source.primaryMonitor) + break; + } + sources->release(); + return found; +} +} + CLocalVideoTranscodingDlg::CLocalVideoTranscodingDlg(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_DIALOG_LOCAL_VIDEO_TRANSCODING, pParent) { @@ -78,12 +102,12 @@ void CLocalVideoTranscodingDlg::OnBnClickedButtonJoinchannel() return; } CString strInfo; - agora::rtc::Rectangle rect; auto params = agora::rtc::ScreenCaptureParameters(640, 360, 15, 800); - - RECT rc; - ::GetWindowRect(GetDesktopWindow()->GetSafeHwnd(), &rc); - rect = { rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top }; + int64_t displayId = 0; + if (!GetPrimaryDisplayId(m_rtcEngine, displayId)) { + AfxMessageBox(_T("No screen is available for capture")); + return; + } //primary camera configuration @@ -111,8 +135,15 @@ void CLocalVideoTranscodingDlg::OnBnClickedButtonJoinchannel() m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start primary camera capture")); //start Screen capture - ret = m_rtcEngine->startScreenCaptureByScreenRect(rect, rect, params); - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start Screen capture")); + agora::rtc::Rectangle regionRect; + ret = m_rtcEngine->startScreenCaptureByDisplayId(displayId, regionRect, params); + if (ret != 0) { + m_rtcEngine->stopCameraCapture(VIDEO_SOURCE_CAMERA_PRIMARY); + strInfo.Format(_T("start screen capture failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return; + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start screen capture")); int i = 0; //screen stream_infos[i].sourceType = VIDEO_SOURCE_SCREEN_PRIMARY; diff --git a/windows/APIExample/APIExample/Advanced/MultiVideoSource/CAgoraMutilVideoSourceDlg.cpp b/windows/APIExample/APIExample/Advanced/MultiVideoSource/CAgoraMutilVideoSourceDlg.cpp index cdc1faadf..1a620b43e 100755 --- a/windows/APIExample/APIExample/Advanced/MultiVideoSource/CAgoraMutilVideoSourceDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/MultiVideoSource/CAgoraMutilVideoSourceDlg.cpp @@ -11,6 +11,30 @@ IMPLEMENT_DYNAMIC(CAgoraMutilVideoSourceDlg, CDialogEx) +namespace { +bool GetPrimaryDisplayId(IRtcEngine* rtcEngine, int64_t& displayId) +{ + SIZE sourceSize = { 64, 64 }; + IScreenCaptureSourceList* sources = rtcEngine->getScreenCaptureSources(sourceSize, sourceSize, true); + if (!sources) + return false; + + bool found = false; + for (unsigned int i = 0; i < sources->getCount(); ++i) { + ScreenCaptureSourceInfo source = sources->getSourceInfo(i); + if (source.type != ScreenCaptureSourceType_Screen) + continue; + + displayId = source.sourceId; + found = true; + if (source.primaryMonitor) + break; + } + sources->release(); + return found; +} +} + CAgoraMutilVideoSourceDlg::CAgoraMutilVideoSourceDlg(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD, pParent) { @@ -1073,17 +1097,23 @@ void CAgoraMutilVideoSourceDlg::OnBnClickedButtonCaptureScreen() std::string szChannelId = cs2utf8(strChannelName); if (!m_bStartScreenSharing) { - - agora::rtc::Rectangle rc; ScreenCaptureParameters scp; scp.frameRate = 15; scp.bitrate = 0; - HWND hWnd = ::GetDesktopWindow(); - RECT destop_rc; - ::GetWindowRect(hWnd, &destop_rc); - scp.dimensions.width = destop_rc.right - destop_rc.left; - scp.dimensions.height = destop_rc.bottom - destop_rc.top; - m_rtcEngine->startScreenCaptureByScreenRect(rc, rc, scp); + int64_t displayId = 0; + if (!GetPrimaryDisplayId(m_rtcEngine, displayId)) { + AfxMessageBox(_T("No screen is available for capture")); + return; + } + + agora::rtc::Rectangle regionRect; + int ret = m_rtcEngine->startScreenCaptureByDisplayId(displayId, regionRect, scp); + if (ret != 0) { + CString str; + str.Format(_T("start screen capture failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), str); + return; + } screenConnection.localUid = generateUid(); m_screenEventHandler.SetId(screenConnection.localUid); @@ -1111,10 +1141,19 @@ void CAgoraMutilVideoSourceDlg::OnBnClickedButtonCaptureScreen() options2.clientRoleType = CLIENT_ROLE_BROADCASTER; // joinChannelEx secondary camera capture(broadcaster) screenConnection.channelId = szChannelId.data(); - int ret = m_rtcEngine->joinChannelEx(APP_TOKEN, screenConnection, options2, &m_screenEventHandler); + ret = m_rtcEngine->joinChannelEx(APP_TOKEN, screenConnection, options2, &m_screenEventHandler); CString str; str.Format(_T("joinChannelEx: %d"), ret); m_lstInfo.InsertString(m_lstInfo.GetCount(), str); + if (ret != 0) { + m_rtcEngine->stopPreview(VIDEO_SOURCE_SCREEN); + m_rtcEngine->stopScreenCapture(); + canvas.view = nullptr; + m_rtcEngine->setupLocalVideo(canvas); + m_videoWnds[4].Reset(); + m_btnScreenCapture.SetWindowText(MultiVideoSourceStartCapture); + return; + } } else { VideoCanvas canvas; @@ -1141,4 +1180,3 @@ void CAgoraMutilVideoSourceDlg::OnBnClickedButtonCaptureScreen() m_bStartScreenSharing = !m_bStartScreenSharing; } - diff --git a/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.cpp b/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.cpp index 822f3f61e..4a3ee1aa4 100644 --- a/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.cpp @@ -14,6 +14,7 @@ void PlayerObserverAdapter::onMetadata(rte::PlayerMetadataType type, const uint8_t* data, size_t length) {} void PlayerObserverAdapter::onPlayerInfoUpdated(const rte::PlayerInfo* info) {} void PlayerObserverAdapter::onAudioVolumeIndication(int32_t volume) {} +void PlayerObserverAdapter::onStreamDataReceived(const rte::PlayerStreamData* streamData) {} IMPLEMENT_DYNAMIC(CRtePlayerDlg, CDialogEx) @@ -42,6 +43,7 @@ BEGIN_MESSAGE_MAP(CRtePlayerDlg, CDialogEx) ON_MESSAGE(WM_MSGID(WM_PLAYER_PREPARED), &CRtePlayerDlg::OnPrepared) ON_MESSAGE(WM_MSGID(WM_PLAYER_STATE), &CRtePlayerDlg::OnPlayerStateChanged) ON_MESSAGE(WM_MSGID(WM_PLAYER_EVENT), &CRtePlayerDlg::OnPlayerOnEvent) + ON_MESSAGE(WM_MSGID(WM_PLAYER_STREAM_DATA), &CRtePlayerDlg::OnPlayerStreamData) ON_BN_CLICKED(IDC_BUTTON_START, &CRtePlayerDlg::OnBnClickedButtonStart) ON_BN_CLICKED(IDC_BUTTON_STOP, &CRtePlayerDlg::OnBnClickedButtonStop) ON_BN_CLICKED(IDC_BUTTON_PLAY, &CRtePlayerDlg::OnBnClickedButtonPlay) @@ -168,16 +170,17 @@ bool CRtePlayerDlg::InitAgora() // UnInitialize the Agora SDK void CRtePlayerDlg::UnInitAgora() { - if (mCanvas) - { - delete mCanvas; - mCanvas = nullptr; - } if (mRtePlayer) { + mRtePlayer->UnregisterObserver(this); delete mRtePlayer; mRtePlayer = nullptr; } + if (mCanvas) + { + delete mCanvas; + mCanvas = nullptr; + } if (mRte) { delete mRte; @@ -202,6 +205,23 @@ void CRtePlayerDlg::onEvent(rte::PlayerEvent event) ::PostMessage(m_hWnd, WM_MSGID(WM_PLAYER_EVENT), (WPARAM)event, 0); } +void CRtePlayerDlg::onStreamDataReceived(const rte::PlayerStreamData* streamData) +{ + if (!streamData) + return; + + for (const auto& volumeInfo : streamData->AudioVolumes()) + { + auto* event = new RteStreamVolumeEvent{ + volumeInfo.ChannelName(), volumeInfo.StreamId(), volumeInfo.Volume()}; + if (!::PostMessage(m_hWnd, WM_MSGID(WM_PLAYER_STREAM_DATA), + reinterpret_cast(event), 0)) + { + delete event; + } + } +} + void CRtePlayerDlg::OnBnClickedButtonStart() { CString url; @@ -353,3 +373,17 @@ LRESULT CRtePlayerDlg::OnPlayerOnEvent(WPARAM wParam, LPARAM lParam) } return 0; } + +LRESULT CRtePlayerDlg::OnPlayerStreamData(WPARAM wParam, LPARAM lParam) +{ + auto* event = reinterpret_cast(wParam); + if (!event) + return 0; + + CString strInfo; + strInfo.Format(_T("stream volume: channel=%s, stream=%s, volume=%d"), + utf82cs(event->channelName), utf82cs(event->streamId), event->volume); + mListInfo.AddString(strInfo); + delete event; + return 0; +} diff --git a/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.h b/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.h index 4fc0e465a..43e610547 100644 --- a/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.h +++ b/windows/APIExample/APIExample/Advanced/RtePlayer/RtePlayerDlg.h @@ -9,6 +9,15 @@ using namespace rte; #define WM_PLAYER_PREPARED (WM_USER + 10002) #define WM_PLAYER_STATE (WM_USER + 10003) #define WM_PLAYER_EVENT (WM_USER + 1004) +#define WM_PLAYER_STREAM_DATA (WM_USER + 10005) + +struct RteStreamVolumeEvent +{ + std::string channelName; + std::string streamId; + int32_t volume; +}; + class PlayerObserverAdapter:public PlayerObserver { public: @@ -24,6 +33,7 @@ class PlayerObserverAdapter:public PlayerObserver const uint8_t* data, size_t length) override; void onPlayerInfoUpdated(const rte::PlayerInfo* info) override; void onAudioVolumeIndication(int32_t volume) override; + void onStreamDataReceived(const rte::PlayerStreamData* streamData) override; }; @@ -39,6 +49,7 @@ class CRtePlayerDlg : public CDialogEx,public PlayerObserverAdapter void UnInitAgora(); void onStateChanged(rte::PlayerState old_state, rte::PlayerState new_state,rte::Error* err) override; void onEvent(rte::PlayerEvent event) override; + void onStreamDataReceived(const rte::PlayerStreamData* streamData) override; enum { IDD = IDD_DIALOG_RTE_PLAYER @@ -80,6 +91,7 @@ class CRtePlayerDlg : public CDialogEx,public PlayerObserverAdapter afx_msg LRESULT OnPrepared(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnPlayerStateChanged(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnPlayerOnEvent(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnPlayerStreamData(WPARAM wParam, LPARAM lParam); CListBox mListInfo; CStatic mVideoWnd; }; diff --git a/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp index 728d98052..ad76f657f 100755 --- a/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp +++ b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp @@ -107,6 +107,7 @@ bool CAgoraScreenCapture::InitAgora() m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + InitMonitorInfos(); m_btnJoinChannel.EnableWindow(TRUE); return true; } @@ -286,22 +287,41 @@ BOOL CAgoraScreenCapture::OnInitDialog() m_cmbScreenScenario.InsertString(2, _T("Video")); m_cmbScreenScenario.InsertString(3, _T("RDC")); ResumeStatus(); - InitMonitorInfos(); return TRUE; } void CAgoraScreenCapture::InitMonitorInfos() { - m_monitors.EnumMonitor(); - std::vector infos = m_monitors.GetMonitors(); - for (size_t i = 0; i < infos.size(); i++) { - char szName[MAX_PATH] = { 0 }; - sprintf_s(szName, MAX_PATH, "Display%d", i + 1); - CString strDevice = infos[i].monitorInfo.szDevice; - strDevice.Replace(L"\\\\.\\", L""); - m_cmbScreenRegion.InsertString(i, strDevice.GetBuffer(0)); + m_cmbScreenRegion.ResetContent(); + m_displaySources.clear(); + if (!m_rtcEngine) + return; + + SIZE sourceSize = { 64, 64 }; + IScreenCaptureSourceList* sources = + m_rtcEngine->getScreenCaptureSources(sourceSize, sourceSize, true); + if (!sources) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("getScreenCaptureSources failed")); + return; } - m_cmbScreenRegion.SetCurSel(0); + + for (unsigned int i = 0; i < sources->getCount(); ++i) { + ScreenCaptureSourceInfo source = sources->getSourceInfo(i); + if (source.type != ScreenCaptureSourceType_Screen) + continue; + + m_displaySources.push_back({ source.sourceId, source.position }); + CString displayName; + if (source.sourceName && source.sourceName[0] != '\0') + displayName = utf82cs(source.sourceName); + else + displayName.Format(_T("Display %u"), static_cast(m_displaySources.size())); + m_cmbScreenRegion.AddString(displayName); + } + sources->release(); + + if (!m_displaySources.empty()) + m_cmbScreenRegion.SetCurSel(0); } //The JoinChannel button's click handler. @@ -353,19 +373,25 @@ void CAgoraScreenCapture::OnBnClickedButtonStartShare() { if (!m_rtcEngine || !m_initialize) return; - HWND hWnd = NULL; if (m_cmbScreenCap.GetCurSel() < 0 || m_cmbScreenCap.GetCurSel() >= m_listWnd.GetSize()) { return; } - hWnd = (HWND)m_listWnd.GetAt(m_listWnd.FindIndex(m_cmbScreenCap.GetCurSel())).sourceId; + const int64_t windowId = + m_listWnd.GetAt(m_listWnd.FindIndex(m_cmbScreenCap.GetCurSel())).sourceId; int ret = 0; - m_windowShare = !m_windowShare; - if (m_windowShare) + if (!m_windowShare) { //start screen capture in the engine. agora::rtc::Rectangle rcCapWnd; m_screenParam.windowFocus = true; - ret = m_rtcEngine->startScreenCaptureByWindowId((int64_t)hWnd, rcCapWnd, m_screenParam); + ret = m_rtcEngine->startScreenCaptureByWindowId(windowId, rcCapWnd, m_screenParam); + if (ret != 0) { + CString strInfo; + strInfo.Format(_T("start share window failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return; + } + m_windowShare = true; //start preview in the engine. m_rtcEngine->startPreview(VIDEO_SOURCE_TYPE::VIDEO_SOURCE_SCREEN); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); @@ -384,10 +410,7 @@ void CAgoraScreenCapture::OnBnClickedButtonStartShare() m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("updateChannelMediaOptions")); } - if (ret== 0) - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start share window succees!")); - else - m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start share window failed!")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start share window success")); m_btnStartCap.SetWindowText(screenShareCtrlEndCap); m_btnShareScreen.EnableWindow(FALSE); @@ -403,6 +426,8 @@ void CAgoraScreenCapture::OnBnClickedButtonStartShare() m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stop screen share stop failed!")); m_btnStartCap.SetWindowText(screenShareCtrlStartCap); m_btnShareScreen.EnableWindow(TRUE); + if (ret == 0) + m_windowShare = false; } } @@ -804,29 +829,40 @@ bool CMonitors::GetWindowRect(HWND hWnd, agora::rtc::Rectangle& regionRect) void CAgoraScreenCapture::OnBnClickedButtonStartShareScreen() { - m_screenShare = !m_screenShare; - if (m_screenShare) { - int sel = m_cmbScreenRegion.GetCurSel(); - int heightX = GetSystemMetrics(SM_CXSCREEN); - int heightY = GetSystemMetrics(SM_CYSCREEN); - m_screenRegion = { 0,0,heightX,heightY }; - - CString displayId = L""; - m_cmbScreenRegion.GetWindowText(displayId); - int id = _ttoi(displayId.Mid(strlen("display"))) - 1; - float scale = m_sldScale.GetPos() * 1.0f / 100; - agora::rtc::Rectangle rcCapWnd = { m_screenRegion.x, m_screenRegion.y, (int)(m_screenRegion.width * scale), (int)(m_screenRegion.height * scale) }; + if (!m_rtcEngine || !m_initialize) + return; + + if (!m_screenShare) { + const int selected = m_cmbScreenRegion.GetCurSel(); + if (selected < 0 || selected >= static_cast(m_displaySources.size())) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("No shareable display selected")); + return; + } + + const DisplayCaptureSource& display = m_displaySources[selected]; + m_screenRegion = display.position; + const float scale = m_sldScale.GetPos() / 100.0f; + agora::rtc::Rectangle rcCapWnd = { + 0, 0, + static_cast(m_screenRegion.width * scale), + static_cast(m_screenRegion.height * scale) + }; m_screenParam.windowFocus = true; - m_rtcEngine->startScreenCaptureByDisplayId(id, rcCapWnd, m_screenParam); - - m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("startScreenCaptureByDisplayId")); + const int ret = m_rtcEngine->startScreenCaptureByDisplayId( + display.sourceId, rcCapWnd, m_screenParam); + CString strInfo; - strInfo.Format(_T("DisplayId: %d"), id); - m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, strInfo); + strInfo.Format(_T("startScreenCaptureByDisplayId: id=%lld, ret=%d"), + static_cast(display.sourceId), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + if (ret != 0) + return; + + m_screenShare = true; m_btnShareScreen.SetWindowText(screenShareCtrlStopShare); //start preview in the engine. - m_rtcEngine->startPreview(); - m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("startPreview ")); + m_rtcEngine->startPreview(VIDEO_SOURCE_SCREEN_PRIMARY); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); if (m_joinChannel) { ChannelMediaOptions option; option.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING; @@ -839,15 +875,22 @@ void CAgoraScreenCapture::OnBnClickedButtonStartShareScreen() m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("updateChannelMediaOptions")); } m_btnStartCap.EnableWindow(FALSE); - m_screenShare = true; m_btnWndExeclude.EnableWindow(TRUE); m_chkHighLight.EnableWindow(FALSE); } else { - m_rtcEngine->stopScreenCapture(); - m_btnShareScreen.SetWindowText(screenShareCtrlShareSCreen); - m_btnStartCap.EnableWindow(TRUE); - m_screenShare = false; + const int ret = m_rtcEngine->stopScreenCapture(); + if (ret == 0) { + m_btnShareScreen.SetWindowText(screenShareCtrlShareSCreen); + m_btnStartCap.EnableWindow(TRUE); + m_screenShare = false; + m_btnWndExeclude.EnableWindow(FALSE); + m_chkHighLight.EnableWindow(TRUE); + } else { + CString strInfo; + strInfo.Format(_T("stop screen capture failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } } } diff --git a/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h index fcc7b04c6..e9895c869 100755 --- a/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h +++ b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h @@ -137,8 +137,14 @@ class CAgoraScreenCapture : public CDialogEx virtual void DoDataExchange(CDataExchange* pDX); private: + struct DisplayCaptureSource { + int64_t sourceId; + agora::rtc::Rectangle position; + }; + CList m_listWnd; CList m_listWndExecluded; + std::vector m_displaySources; CScreenCaputreEventHandler m_eventHandler; IRtcEngine* m_rtcEngine = nullptr; diff --git a/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp index 9803e9e38..0c4500c2d 100755 --- a/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp +++ b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp @@ -8,6 +8,8 @@ CAGDShowAudioCapture::CAGDShowAudioCapture() , m_ptrCaptureGraphBuilder2(NULL) , m_nCapSelected(-1) { + engine_ = nullptr; + audioTrackId_ = agora::rtc::INVALID_TRACK_ID; memset(m_szActiveDeviceID, 0, MAX_PATH*sizeof(TCHAR)); filterSourceName = _T("Audio Filter"); @@ -639,7 +641,7 @@ void CAGDShowAudioCapture::Stop() void CAGDShowAudioCapture::Receive(bool video, IMediaSample *sample) { BYTE *pBuffer; - if (!sample || !engine_) + if (!sample || !engine_ || audioTrackId_ == agora::rtc::INVALID_TRACK_ID) return; int size = sample->GetActualDataLength(); @@ -655,7 +657,7 @@ void CAGDShowAudioCapture::Receive(bool video, IMediaSample *sample) int fps = m_audioFrame.samplesPerSec / m_audioFrame.samplesPerChannel; memcpy(m_audioFrame.buffer,pBuffer , size); SIZE_T nSize = m_audioFrame.samplesPerChannel * m_audioFrame.channels * m_audioFrame.bytesPerSample; - mediaEngine->pushAudioFrame(&m_audioFrame); + mediaEngine->pushAudioFrame(&m_audioFrame, audioTrackId_); //CircleBuffer::GetInstance()->writeBuffer(pBuffer, size, GetTickCount()); } @@ -735,4 +737,4 @@ BOOL CAGDShowAudioCapture::DisconnectPins() } ATLASSERT(SUCCEEDED(hr)); return TRUE; -} \ No newline at end of file +} diff --git a/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h index e2c790634..c022cc7e8 100644 --- a/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h +++ b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h @@ -49,6 +49,7 @@ class CAGDShowAudioCapture void InitAudioFrame(); agora::rtc::IRtcEngine* engine_; + agora::rtc::track_id_t audioTrackId_; agora::media::IAudioFrameObserver::AudioFrame m_audioFrame; protected: diff --git a/windows/APIExample/APIExample/Language.h b/windows/APIExample/APIExample/Language.h index 098007be8..fb1087431 100755 --- a/windows/APIExample/APIExample/Language.h +++ b/windows/APIExample/APIExample/Language.h @@ -36,7 +36,6 @@ extern wchar_t advancedCustomVideoCapture[INFO_LEN]; extern wchar_t advancedOriginalVideo[INFO_LEN]; extern wchar_t advancedCustomAudioCapture[INFO_LEN]; extern wchar_t advancedOriginalAudio[INFO_LEN]; -extern wchar_t advancedFaceCapture[INFO_LEN]; extern wchar_t advancedMediaPlayer[INFO_LEN]; extern wchar_t advancedMediaRecorder[INFO_LEN]; extern wchar_t advancedMultiChannel[INFO_LEN]; @@ -369,4 +368,3 @@ extern wchar_t CrossChannelCtrlUid[INFO_LEN]; extern wchar_t Multipath[INFO_LEN]; - diff --git a/windows/APIExample/APIExample/en.ini b/windows/APIExample/APIExample/en.ini index 4c1e7fc51..3c70dbad8 100755 --- a/windows/APIExample/APIExample/en.ini +++ b/windows/APIExample/APIExample/en.ini @@ -38,7 +38,6 @@ Advanced.MultiVideoSourceTracks=Multi Video Source Tracks Advanced.MediaEncrypt=Media Encrypt Advanced.CustomEncrypt=Custom Encrypt Advanced.MediaPlayer=MediaPlayer -Advanced.FaceCapture=FaceCapture LiveBroadcasting.Ctrl.Persons=Persons Advanced.AudioEffect=Audio Effect @@ -323,4 +322,4 @@ CrossChannel.Ctrl.StartMediaRelay = StartMediaRelay CrossChannel.Ctrl.StopMediaRelay = StopMediaRelay Advanced.Multipath=Multipath -Advanced.Simulcast=Simulcast \ No newline at end of file +Advanced.Simulcast=Simulcast diff --git a/windows/APIExample/APIExample/resource.h b/windows/APIExample/APIExample/resource.h index d58a677dc..d502306d3 100755 --- a/windows/APIExample/APIExample/resource.h +++ b/windows/APIExample/APIExample/resource.h @@ -43,7 +43,6 @@ #define IDD_DIALOG_PUSH_EXTERNAL_VIDEO 161 #define IDD_DIALOG_MULTI_VIDEO_SOURCE1 162 #define IDD_DIALOG_MULTI_VIDEO_SOURCE_TRACKS 162 -#define IDD_DIALOG_FACE_CAPTURE 163 #define IDD_DIALOG_JOINCHANNELVIDEOBYTOKEN 164 #define IDD_DIALOG_TRANSPARENT_BG 165 #define IDB_BITMAP_LOCAL 168 @@ -410,7 +409,6 @@ #define IDC_EDIT_CROSS_CHANNEL 1149 #define IDC_STATIC_PLAYER_STREAM 1149 #define IDC_EDIT_TOKEN 1150 -#define IDC_EDIT_METADATA_INFO 1150 #define IDC_STATIC_TOKEN 1151 #define IDC_STATIC_VIDEO_LEFT 1151 #define IDC_USER_ID 1152 diff --git a/windows/APIExample/APIExample/stdafx.cpp b/windows/APIExample/APIExample/stdafx.cpp index 9f7c894fa..98e2bf980 100755 --- a/windows/APIExample/APIExample/stdafx.cpp +++ b/windows/APIExample/APIExample/stdafx.cpp @@ -38,7 +38,6 @@ wchar_t advancedCustomVideoCapture[INFO_LEN] = { 0 }; wchar_t advancedOriginalVideo[INFO_LEN] = { 0 }; wchar_t advancedCustomAudioCapture[INFO_LEN] = { 0 }; wchar_t advancedOriginalAudio[INFO_LEN] = { 0 }; -wchar_t advancedFaceCapture[INFO_LEN] = { 0 }; wchar_t advancedMediaPlayer[INFO_LEN] = { 0 }; wchar_t advancedMediaRecorder[INFO_LEN] = { 0 }; wchar_t advancedMultiChannel[INFO_LEN] = { 0 }; @@ -497,7 +496,6 @@ void InitKeyInfomation() _tcscpy_s(advancedCustomAudioCapture, INFO_LEN, Str(_T("Advanced.CustomAudioCapture"))); _tcscpy_s(advancedOriginalAudio, INFO_LEN, Str(_T("Advanced.OriginalAudio"))); - _tcscpy_s(advancedFaceCapture, INFO_LEN, Str(_T("Advanced.FaceCapture"))); _tcscpy_s(advancedMediaPlayer, INFO_LEN, Str(_T("Advanced.MediaPlayer"))); _tcscpy_s(advancedMediaRecorder, INFO_LEN, Str(_T("Advanced.MediaRecorder"))); _tcscpy_s(advancedMultiChannel, INFO_LEN, Str(_T("Advanced.MultiChannel"))); diff --git a/windows/APIExample/APIExample/stdafx.h b/windows/APIExample/APIExample/stdafx.h index a218b07f2..3d02abe32 100644 --- a/windows/APIExample/APIExample/stdafx.h +++ b/windows/APIExample/APIExample/stdafx.h @@ -111,10 +111,8 @@ using namespace agora::media; #define EID_RECORDER_STATE_CHANGE 0x00000034 #define EID_RECORDER_INFO_UPDATE 0x00000035 #define EID_EXTENSION_EVENT 0x00000036 -#define EID_ON_CAPTURE_VIDEOFRAME 0x00000037 #define EID_ERROR 0x00000038 -#define EID_ON_FACE_INFO 0x00000039 #define EID_ON_RECV_AUDIO_METADATA 0x0000003A #define EID_MULTIPATH_STATS 0x0000003B #define EID_NETWORK_TYPE_CHANGED 0x0000003C diff --git a/windows/APIExample/APIExample/zh-cn.ini b/windows/APIExample/APIExample/zh-cn.ini index a4bd1208956e03faba6502d74248c7aed18f74db..ce140ef2ee643efe8e273a2892e2c98eb8a515cd 100644 GIT binary patch delta 18 ZcmZ3`&-kK`al;X*u2Pgml delta 58 zcmaFS$GD)MaldT4S%YTo37 L)`pv9IDG5@7FrTk diff --git a/windows/ARCHITECTURE.md b/windows/ARCHITECTURE.md index 7d234b67e..5affa57d0 100644 --- a/windows/ARCHITECTURE.md +++ b/windows/ARCHITECTURE.md @@ -104,10 +104,9 @@ All examples share utilities: | Beauty2.0 | `Advanced/Beauty2.0/` | `createVideoEffectObject()`, `setVideoEffectStringParam()`, `setVideoEffectFloatParam()` | Enhanced beauty effects with v2.0 API | | BeautyAudio | `Advanced/BeautyAudio/` | `setBeautyEffectOptions()`, `setAudioEffectPreset()` | Combined audio and video beauty effects | | CrossChannel | `Advanced/CrossChannel/` | `startChannelMediaRelay()`, `updateChannelMediaRelay()`, `stopChannelMediaRelay()` | Media relay across multiple channels | -| CustomAudioCapture | `Advanced/CustomAudioCapture/` | `setExternalAudioSource()`, `pushAudioFrame()` | Custom audio source capture | +| CustomAudioCapture | `Advanced/CustomAudioCapture/` | `createCustomAudioTrack()`, `pushAudioFrame()`, `destroyCustomAudioTrack()` | Custom audio source capture | | CustomEncrypt | `Advanced/CustomEncrypt/` | `setEncryptionConfig()`, `enableEncryption()` | Custom stream encryption | | CustomVideoCapture | `Advanced/CustomVideoCapture/` | `setExternalVideoSource()`, `pushVideoFrame()` | Custom video source capture | -| FaceCapture | `Advanced/FaceCapture/` | `enableFaceDetection()`, `getFaceDetectionResult()` | Face detection and capture | | LocalVideoTranscoding | `Advanced/LocalVideoTranscoding/` | `startLocalVideoTranscoding()`, `updateLocalTranscodingConfig()`, `stopLocalVideoTranscoding()` | Local video transcoding and composition | | MediaEncrypt | `Advanced/MediaEncrypt/` | `setEncryptionConfig()`, `enableEncryption()` | Media stream encryption | | MediaPlayer | `Advanced/MediaPlayer/` | `createMediaPlayer()`, `open()`, `play()`, `pause()`, `stop()` | Media file playback and control | From 784f4d4d6f67d3d8196169211e97859cc5093254 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Fri, 7 Aug 2026 18:40:18 +0800 Subject: [PATCH 23/49] docs: support hidden API example cases --- .../ai-engineering/case-maintenance-matrix.md | 4 ++-- .../tools/generate_case_backlog.py | 2 +- .../tools/generate_case_backlog_test.py | 21 +++++++++++++++++++ .../tools/validate_acceptance_manifest.py | 2 +- .../validate_acceptance_manifest_test.py | 15 +++++++++++++ 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/docs/ai-engineering/case-maintenance-matrix.md b/docs/ai-engineering/case-maintenance-matrix.md index f32a3a89f..9019b4474 100644 --- a/docs/ai-engineering/case-maintenance-matrix.md +++ b/docs/ai-engineering/case-maintenance-matrix.md @@ -15,6 +15,7 @@ This matrix is the planning artifact for cross-platform API Example parity work. | Status | Meaning | | --- | --- | | `DONE(path)` | Inventory says the case is implemented and expected to be registered at the listed path. Verify before editing. For new or changed parity work, use `DONE` only after the required reference, review, and build gates pass. | +| `HIDDEN(path)` | Source is retained at the listed path, but the case is intentionally not registered in the user-visible list. | | `PARTIAL(note)` | Exists but parity, registration, docs, verification, runtime smoke, or API coverage needs review. | | `MISSING` | Required for parity but no implementation is currently recorded here. | | `N/A(reason)` | Not applicable because the SDK family, platform, or project variant does not support it. | @@ -72,7 +73,7 @@ Seeded from the overseas `dev/4.6.4` project `ARCHITECTURE.md` indexes and sourc | Third-party beauty | Full RTC | extension enablement, third-party SDK initialization, video frame processing | `DONE(advanced/ThirdPartyBeauty.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/ThirdBeautify/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `MISSING` | `MISSING` | Only Android full and iOS UIKit have third-party beauty. Release work must check license expiration. | | Extension sample | Full RTC extension | `enableExtension`, extension property APIs, matching native headers | `DONE(advanced/SimpleExtension.java)` | `N/A(audio SDK)` | `PARTIAL(VideoProcessExtension covers enableExtension)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | `DONE(Advanced/SimpleFilter/)` | `N/A(audio SDK)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | During SDK bumps, verify Android extension include files and iOS/macOS native assets. | | Audio mixing | Full RTC or audio SDK | `startAudioMixing`, `stopAudioMixing`, `adjustAudioMixingVolume`, effect APIs | `DONE(advanced/PlayAudioFiles.java)` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(samples/PlayAudioFiles.kt)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage across all projects. | -| Rhythm player | Full RTC or audio SDK | `startRhythmPlayer`, `stopRhythmPlayer` | `DONE(advanced/RhythmPlayer.java)` | `DONE(advanced/RhythmPlayer.java)` | `DONE(samples/RhythmPlayer.kt)` | `DONE(Advanced/RhythmPlayer/)` | `DONE(Advanced/RhythmPlayer/)` | `DONE(Advanced/RhythmPlayer/)` | `DONE(Advanced/RhythmPlayer/)` | `MISSING` | `MISSING` | macOS and Windows have no rhythm player case. | +| Rhythm player | Full RTC or audio SDK | `startRhythmPlayer`, `stopRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(samples/RhythmPlayer.kt)` | `HIDDEN(Advanced/RhythmPlayer/)` | `HIDDEN(Advanced/RhythmPlayer/)` | `HIDDEN(Advanced/RhythmPlayer/)` | `HIDDEN(Advanced/RhythmPlayer/)` | `N/A(no case)` | `N/A(no case)` | All implemented Rhythm Player sources are retained for reference but hidden because the APIs are deprecated since RTC SDK 4.6.0. | | Media recorder | Full RTC | `createMediaRecorder`, `startRecording`, `stopRecording` | `DONE(advanced/MediaRecorder.java)` | `N/A(audio SDK)` | `DONE(samples/MediaRecorder.kt)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Advanced/MediaRecorder/)` | iOS/macOS embed recorder in a join-channel variant; Android/Compose/Windows have standalone cases. | | Local video transcoding | Full RTC | `startLocalVideoTranscoder`, `startCameraCapture`, compositing APIs | `DONE(advanced/LocalVideoTranscoding.java)` | `N/A(audio SDK)` | `DONE(samples/LocalVideoTranscoding.kt)` | `DONE(Advanced/LocalCompositeGraph/)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalCompositeGraph/)` | `N/A(audio SDK)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalVideoTranscoding/)` | iOS UIKit and OC name this LocalCompositeGraph; SwiftUI splits transcoding and composition. | | Multi-channel join | Full RTC | `joinChannelEx`, `leaveChannelEx`, multi-channel connection APIs | `DONE(advanced/JoinMultipleChannel.java)` | `N/A(audio SDK)` | `DONE(samples/JoinMultiChannel.kt)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `N/A(audio SDK)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/MultiChannel/)` | Full coverage across full-RTC projects. | @@ -96,7 +97,6 @@ These gaps are derived from the pilot matrix above and should be prioritized whe | Pre-call test | iOS Objective-C | Medium — full-RTC variant gap | | Third-party beauty | Android Compose, iOS SwiftUI, iOS OC, macOS, Windows | Low — third-party SDK and license overhead | | Extension sample | iOS SwiftUI, Windows | Medium — extension enablement gap | -| Rhythm player | macOS, Windows | Low — audio utility gap | | Multipath | Android Compose | Low — Compose variant gap | | Content inspect | Android Compose, Windows | Low — moderation feature gap | | Picture in picture | macOS, Windows | Low — platform feature gap | diff --git a/docs/ai-engineering/tools/generate_case_backlog.py b/docs/ai-engineering/tools/generate_case_backlog.py index b7aedeca6..7ea159cb2 100644 --- a/docs/ai-engineering/tools/generate_case_backlog.py +++ b/docs/ai-engineering/tools/generate_case_backlog.py @@ -59,7 +59,7 @@ def parse_matrix_cell(cell): value = strip_code(cell) if value in {"MISSING", "UNKNOWN"}: return value, "" - match = re.fullmatch(r"(DONE|PARTIAL|N/A)\((.+)\)", value) + match = re.fullmatch(r"(DONE|HIDDEN|PARTIAL|N/A)\((.+)\)", value) if match: return match.group(1), match.group(2) return "", "" diff --git a/docs/ai-engineering/tools/generate_case_backlog_test.py b/docs/ai-engineering/tools/generate_case_backlog_test.py index 447190e15..ebe3dcb46 100644 --- a/docs/ai-engineering/tools/generate_case_backlog_test.py +++ b/docs/ai-engineering/tools/generate_case_backlog_test.py @@ -10,6 +10,9 @@ REPO_ROOT = Path(__file__).resolve().parents[3] GENERATOR = REPO_ROOT / "docs/ai-engineering/tools/generate_case_backlog.py" +sys.path.insert(0, str(GENERATOR.parent)) +from generate_case_backlog import parse_matrix_cell + class CaseBacklogGeneratorTest(unittest.TestCase): def run_generator(self, matrix_text): @@ -81,6 +84,24 @@ def test_generates_execution_units_for_missing_and_partial_cells(self): self.assertEqual(payload["execution_units"][1]["severity"], "Unspecified") self.assertEqual(payload["execution_units"][1]["priority"], 95) + def test_hidden_cell_is_parsed_but_not_added_to_backlog(self): + matrix = textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Android full | Notes | + | --- | --- | --- | --- | --- | + | Rhythm player | Full RTC | `startRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | Deprecated API retained for reference. | + """ + ) + + result = self.run_generator(matrix) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)["execution_units"], []) + self.assertEqual( + parse_matrix_cell("HIDDEN(advanced/RhythmPlayer.java)"), + ("HIDDEN", "advanced/RhythmPlayer.java"), + ) + def test_unknown_platform_column_fails(self): matrix = textwrap.dedent( """ diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest.py b/docs/ai-engineering/tools/validate_acceptance_manifest.py index 9207a6267..215fec666 100644 --- a/docs/ai-engineering/tools/validate_acceptance_manifest.py +++ b/docs/ai-engineering/tools/validate_acceptance_manifest.py @@ -23,7 +23,7 @@ FINAL_STATUSES = {"PASS", "PASS WITH RISKS", "BLOCKED"} RESULT_STATUSES = {"PASS", "FAIL", "BLOCKED", "SKIPPED"} ROLE_STATUSES = {"PASS", "FAIL", "BLOCKED", "WAIVED"} -MATRIX_STATUSES = {"DONE", "PARTIAL", "MISSING", "N/A", "UNKNOWN"} +MATRIX_STATUSES = {"DONE", "HIDDEN", "PARTIAL", "MISSING", "N/A", "UNKNOWN"} REQUIRED_TOP_LEVEL = { "version", "final_status", diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py index e37974f26..b805d592f 100644 --- a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py +++ b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py @@ -370,6 +370,21 @@ def test_done_matrix_update_requires_platform_build_pass(self): self.assert_error_contains(manifest, "Windows matrix update to DONE requires build_result=PASS") + def test_hidden_matrix_update_is_allowed(self): + manifest = base_manifest() + manifest["platforms"]["android"]["implementation"]["output"]["matrix_updates"] = [ + { + "feature": "Join channel audio", + "platform_unit": "Android full", + "from": "DONE", + "to": "HIDDEN", + "to_cell": "HIDDEN(basic/JoinChannelAudio.java)", + "evidence": "The retained case is intentionally hidden.", + } + ] + + self.assertEqual(validate_manifest(manifest), []) + def test_matrix_update_must_match_current_requirement(self): manifest = base_manifest() manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ From eaf511ab73b8dfb062ea7db3b5568fd8eb767455 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Mon, 10 Aug 2026 12:17:02 +0800 Subject: [PATCH 24/49] feat(android): add SenseTime beauty example --- Android/APIExample/ARCHITECTURE.md | 2 +- Android/APIExample/README.md | 13 + Android/APIExample/README.zh.md | 13 + Android/APIExample/app/.gitignore | 4 +- Android/APIExample/app/build.gradle | 1 + .../main/assets/beauty_sensetime/PLACEHOLDER | 6 + .../examples/advanced/ThirdPartyBeauty.java | 7 + .../advanced/beauty/SenseTimeBeauty.java | 371 +++++++ .../advanced/beauty/SenseTimeBeautySDK.kt | 531 ++++++++++ .../beautyapi/sensetime/SenseTimeBeautyAPI.kt | 215 ++++ .../sensetime/SenseTimeBeautyAPIImpl.kt | 930 ++++++++++++++++++ .../beautyapi/sensetime/utils/APIReporter.kt | 156 +++ .../beautyapi/sensetime/utils/LogUtils.kt | 56 ++ .../beautyapi/sensetime/utils/StatsHelper.kt | 80 ++ .../sensetime/utils/egl/GLCopyHelper.java | 60 ++ .../sensetime/utils/egl/GLFrameBuffer.java | 204 ++++ .../sensetime/utils/egl/GLTestUtils.java | 150 +++ .../utils/egl/GLTextureBufferQueue.kt | 164 +++ .../beautyapi/sensetime/utils/egl/GlUtil.java | 255 +++++ .../utils/processor/Accelerometer.java | 154 +++ .../utils/processor/BeautyProcessor.kt | 499 ++++++++++ .../sensetime/utils/processor/FaceDetector.kt | 221 +++++ .../utils/processor/IBeautyProcessor.kt | 54 + .../res/layout/fragment_beauty_scenetime.xml | 100 ++ .../app/src/main/res/navigation/nav_graph.xml | 10 + Android/APIExample/app/vendors.gradle | 35 + Android/APIExample/cloud_build.sh | 6 +- 27 files changed, 4291 insertions(+), 6 deletions(-) create mode 100644 Android/APIExample/app/src/main/assets/beauty_sensetime/PLACEHOLDER create mode 100644 Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeauty.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeautySDK.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPI.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/LogUtils.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/StatsHelper.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLCopyHelper.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTestUtils.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTextureBufferQueue.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GlUtil.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/Accelerometer.java create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/BeautyProcessor.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/FaceDetector.kt create mode 100644 Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/IBeautyProcessor.kt create mode 100644 Android/APIExample/app/src/main/res/layout/fragment_beauty_scenetime.xml create mode 100644 Android/APIExample/app/vendors.gradle diff --git a/Android/APIExample/ARCHITECTURE.md b/Android/APIExample/ARCHITECTURE.md index a09ad7de4..b6835db13 100644 --- a/Android/APIExample/ARCHITECTURE.md +++ b/Android/APIExample/ARCHITECTURE.md @@ -142,7 +142,7 @@ APIExample/ | 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) | +| Third-party beauty | `advanced/ThirdPartyBeauty.java` | `registerVideoFrameObserver()` | Demonstrates integration with FaceUnity and SenseTime beauty SDKs | | 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 | diff --git a/Android/APIExample/README.md b/Android/APIExample/README.md index a7b17efb5..5a422fa8c 100644 --- a/Android/APIExample/README.md +++ b/Android/APIExample/README.md @@ -62,6 +62,19 @@ app/src/main/assets/beauty_agora/ The material directory is ignored by Git. +#### SenseTime + +1. Contact SenseTime customer service to obtain the download link and certificate of the beauty sdk +2. Unzip the beauty sdk, and copy the following resources to the corresponding path + +| SenseTime Beauty SDK Path | Location | +|----------------------------------------------------------------------|----------------------------------------------------------| +| Android/models | app/src/main/assets/beauty_sensetime/models | +| Android/smaple/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape | +| Android/smaple/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly | +| Android/smaple/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip | +| SenseME.lic | app/src/main/assets/beauty_sensetime/license/SenseME.lic | + #### FaceUnity 1. Contact FaceUnity customer service to obtain beauty resources and certificates diff --git a/Android/APIExample/README.zh.md b/Android/APIExample/README.zh.md index 83ea971ed..cf84f2549 100644 --- a/Android/APIExample/README.zh.md +++ b/Android/APIExample/README.zh.md @@ -59,6 +59,19 @@ app/src/main/assets/beauty_agora/ 该资源目录已被 Git 忽略。 +#### 商汤美颜 + +1. 联系商汤客服获取美颜sdk下载链接以及证书 +2. 解压美颜sdk,并将以下资源复制到对应路径 + +| 商汤SDK文件/目录 | 项目路径 | +|----------------------------------------------------------------------|----------------------------------------------------------| +| Android/models | app/src/main/assets/beauty_sensetime/models | +| Android/smaple/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape | +| Android/smaple/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly | +| Android/smaple/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip | +| SenseME.lic | app/src/main/assets/beauty_sensetime/license/SenseME.lic | + #### 相芯美颜 1. 联系相芯客服获取美颜资源以及证书 diff --git a/Android/APIExample/app/.gitignore b/Android/APIExample/app/.gitignore index 6de48b91b..d21bc5656 100644 --- a/Android/APIExample/app/.gitignore +++ b/Android/APIExample/app/.gitignore @@ -22,9 +22,9 @@ authpack.java src/main/assets/beauty_agora src/main/assets/beauty_bytedance src/main/assets/beauty_faceunity -src/main/assets/beauty_sensetime +src/main/assets/beauty_sensetime/* !src/main/assets/beauty_bytedance/PLACEHOLDER !src/main/assets/beauty_faceunity/PLACEHOLDER !src/main/assets/beauty_sensetime/PLACEHOLDER !src/main/assets/beauty_agora/PLACEHOLDER -libs \ No newline at end of file +libs diff --git a/Android/APIExample/app/build.gradle b/Android/APIExample/app/build.gradle index 6ab14395f..202d3da9c 100644 --- a/Android/APIExample/app/build.gradle +++ b/Android/APIExample/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' apply from: "${rootDir.absolutePath}/git-hooks.gradle" +apply from: 'vendors.gradle' def sdkVersionFile = file("../gradle.properties") def properties = new Properties() diff --git a/Android/APIExample/app/src/main/assets/beauty_sensetime/PLACEHOLDER b/Android/APIExample/app/src/main/assets/beauty_sensetime/PLACEHOLDER new file mode 100644 index 000000000..0aae234a9 --- /dev/null +++ b/Android/APIExample/app/src/main/assets/beauty_sensetime/PLACEHOLDER @@ -0,0 +1,6 @@ +filter_portrait +license +makeup_lip +models +sticker_face_shape +style_lightly \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java index 95de231a3..1105a709c 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java @@ -23,6 +23,7 @@ import io.agora.api.example.annotation.Example; import io.agora.api.example.common.BaseFragment; import io.agora.api.example.examples.advanced.beauty.FaceUnityBeautySDK; +import io.agora.api.example.examples.advanced.beauty.SenseTimeBeautySDK; /** * The type Third party beauty. @@ -52,8 +53,13 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat List beautyActionIds = new ArrayList<>(); List beautyLabels = new ArrayList<>(); + SenseTimeBeautySDK.INSTANCE.initBeautySDK(requireContext()); FaceUnityBeautySDK.INSTANCE.initBeauty(requireContext()); + // SenseTime Beauty + beautyActionIds.add(R.id.action_third_party_beauty_to_scene_time); + beautyLabels.add(getString(R.string.scenetime_beauty)); + // FaceUnity Beauty beautyActionIds.add(R.id.action_third_party_beauty_to_faceunity); beautyLabels.add(getString(R.string.faceunity_beauty)); @@ -87,6 +93,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void onDestroy() { super.onDestroy(); + SenseTimeBeautySDK.INSTANCE.unInitBeautySDK(); FaceUnityBeautySDK.INSTANCE.unInitBeauty(); } } 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 new file mode 100644 index 000000000..986c3d84f --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeauty.java @@ -0,0 +1,371 @@ +package io.agora.api.example.examples.advanced.beauty; + +import static io.agora.rtc2.video.VideoEncoderConfiguration.STANDARD_BITRATE; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import java.util.Random; + +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.widget.VideoReportLayout; +import io.agora.api.example.databinding.FragmentBeautyScenetimeBinding; +import io.agora.api.example.utils.TokenUtils; +import io.agora.beautyapi.sensetime.CameraConfig; +import io.agora.beautyapi.sensetime.CaptureMode; +import io.agora.beautyapi.sensetime.Config; +import io.agora.beautyapi.sensetime.STHandlers; +import io.agora.beautyapi.sensetime.SenseTimeBeautyAPI; +import io.agora.beautyapi.sensetime.SenseTimeBeautyAPIKt; +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.ColorEnhanceOptions; +import io.agora.rtc2.video.VideoCanvas; +import io.agora.rtc2.video.VideoEncoderConfiguration; + +/** + * The type Sense time beauty. + */ +public class SenseTimeBeauty extends BaseFragment { + private static final String TAG = "SceneTimeBeauty"; + + private FragmentBeautyScenetimeBinding mBinding; + private RtcEngine rtcEngine; + private String channelId; + + private boolean isFrontCamera = true; + + private VideoReportLayout mLocalVideoLayout; + private VideoReportLayout mRemoteVideoLayout; + private boolean isLocalFull = true; + private IRtcEngineEventHandler mRtcEngineEventHandler; + + private final SenseTimeBeautyAPI senseTimeBeautyAPI = SenseTimeBeautyAPIKt.createSenseTimeBeautyAPI(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mBinding = FragmentBeautyScenetimeBinding.inflate(inflater, container, false); + return mBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + boolean hasResource = false; + try { + hasResource = requireActivity().getAssets().list("beauty_sensetime").length > 1; + } catch (IOException e) { + // do nothing + } + if (!hasResource) { + mBinding.tvIntegrateTip.setVisibility(View.VISIBLE); + return; + } + + Bundle arguments = getArguments(); + if (arguments == null) { + return; + } + channelId = arguments.getString(getString(R.string.key_channel_name)); + initVideoView(); + if (!initRtcEngine()) { + return; + } + joinChannel(); + mBinding.switchVideoEffect.setOnCheckedChangeListener((buttonView, isChecked) -> { + ColorEnhanceOptions options = new ColorEnhanceOptions(); + options.strengthLevel = (float) 0.5f; + options.skinProtectLevel = (float) 0.5f; + rtcEngine.setColorEnhanceOptions(isChecked, options); + }); + + SenseTimeBeautySDK.INSTANCE.initMobileEffect(requireContext()); + senseTimeBeautyAPI.initialize(new Config( + requireContext(), + rtcEngine, + new STHandlers( + SenseTimeBeautySDK.INSTANCE.getMobileEffectNative(), + SenseTimeBeautySDK.INSTANCE.getHumanActionNative() + ), + null, + CaptureMode.Agora, + 0, + false, + new CameraConfig() + )); + senseTimeBeautyAPI.enable(true); + } + + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (rtcEngine != null) { + rtcEngine.leaveChannel(); + } + senseTimeBeautyAPI.release(); + SenseTimeBeautySDK.INSTANCE.unInitMobileEffect(); + handler.post(RtcEngine::destroy); + rtcEngine = null; + } + + @Override + protected void onBackPressed() { + mBinding.fullVideoContainer.removeAllViews(); + mBinding.smallVideoContainer.removeAllViews(); + super.onBackPressed(); + } + + private void initVideoView() { + mBinding.cbFaceBeautify.setOnCheckedChangeListener((buttonView, isChecked) -> { + SenseTimeBeautySDK.INSTANCE.getBeautyConfig().setWhiten( + isChecked ? 1.0f: 0.0f + ); + }); + mBinding.cbMakeup.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + SenseTimeBeautySDK.INSTANCE.getBeautyConfig().setMakeUp(new SenseTimeBeautySDK.MakeUpItem( + requireContext(), + "style_lightly" + File.separator + "hunxue.zip", + 1.0f + )); + } else { + SenseTimeBeautySDK.INSTANCE.getBeautyConfig().setMakeUp(null); + } + }); + mBinding.cbSticker.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + SenseTimeBeautySDK.INSTANCE.getBeautyConfig().setSticker(new SenseTimeBeautySDK.StickerItem( + requireContext(), + "sticker_face_shape" + File.separator + "ShangBanLe.zip" + )); + } else { + SenseTimeBeautySDK.INSTANCE.getBeautyConfig().setSticker(null); + } + }); + mBinding.ivCamera.setOnClickListener(v -> { + rtcEngine.switchCamera(); + isFrontCamera = !isFrontCamera; + }); + mBinding.smallVideoContainer.setOnClickListener(v -> updateVideoLayouts(!SenseTimeBeauty.this.isLocalFull)); + } + + private boolean initRtcEngine() { + Context context = getContext(); + if (context == null) { + return false; + } + try { + mRtcEngineEventHandler = new IRtcEngineEventHandler() { + @Override + public void onError(int err) { + super.onError(err); + runOnUIThread(() -> showLongToast(String.format(Locale.US, "msg:%s, code:%d", RtcEngine.getErrorDescription(err), err))); + } + + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + super.onJoinChannelSuccess(channel, uid, elapsed); + runOnUIThread(() -> { + if (mLocalVideoLayout != null) { + mLocalVideoLayout.setReportUid(uid); + } + }); + } + + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + runOnUIThread(() -> { + if (mRemoteVideoLayout == null) { + mRemoteVideoLayout = new VideoReportLayout(requireContext()); + mRemoteVideoLayout.setReportUid(uid); + TextureView videoView = new TextureView(requireContext()); + rtcEngine.setupRemoteVideo(new VideoCanvas(videoView, Constants.RENDER_MODE_HIDDEN, uid)); + mRemoteVideoLayout.addView(videoView); + updateVideoLayouts(isLocalFull); + } + }); + } + + @Override + public void onUserOffline(int uid, int reason) { + super.onUserOffline(uid, reason); + runOnUIThread(() -> { + if (mRemoteVideoLayout != null && mRemoteVideoLayout.getReportUid() == uid) { + mRemoteVideoLayout.removeAllViews(); + mRemoteVideoLayout = null; + updateVideoLayouts(isLocalFull); + } + }); + } + + @Override + public void onLocalAudioStats(LocalAudioStats stats) { + super.onLocalAudioStats(stats); + runOnUIThread(() -> { + if (mLocalVideoLayout != null) { + mLocalVideoLayout.setLocalAudioStats(stats); + } + }); + } + + @Override + public void onLocalVideoStats(Constants.VideoSourceType source, LocalVideoStats stats) { + super.onLocalVideoStats(source, stats); + runOnUIThread(() -> { + if (mLocalVideoLayout != null) { + mLocalVideoLayout.setLocalVideoStats(stats); + } + }); + } + + @Override + public void onRemoteAudioStats(RemoteAudioStats stats) { + super.onRemoteAudioStats(stats); + runOnUIThread(() -> { + if (mRemoteVideoLayout != null) { + mRemoteVideoLayout.setRemoteAudioStats(stats); + } + }); + } + + @Override + public void onRemoteVideoStats(RemoteVideoStats stats) { + super.onRemoteVideoStats(stats); + runOnUIThread(() -> { + if (mRemoteVideoLayout != null) { + mRemoteVideoLayout.setRemoteVideoStats(stats); + } + }); + } + }; + RtcEngineConfig config = new RtcEngineConfig(); + config.mContext = context.getApplicationContext(); + config.mAppId = getAgoraAppId(); + config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; + config.mEventHandler = mRtcEngineEventHandler; + config.mAreaCode = ((MainApplication) requireActivity().getApplication()) + .getGlobalSettings().getAreaCode(); + rtcEngine = RtcEngine.create(config); + rtcEngine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\"" + + RtcEngine.getSdkVersion() + "\"}}"); + LocalAccessPointConfiguration privateCloudConfig = ((MainApplication) requireActivity().getApplication()) + .getGlobalSettings().getPrivateCloudConfig(); + if (privateCloudConfig != null) { + rtcEngine.setLocalAccessPoint(privateCloudConfig); + } + + rtcEngine.enableExtension("agora_video_filters_clear_vision", "clear_vision", true); + + + // Setup video encoding configs + rtcEngine.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()) + )); + rtcEngine.enableVideo(); + rtcEngine.disableAudio(); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + + private void joinChannel() { + int uid = new Random(System.currentTimeMillis()).nextInt(1000) + 10000; + ChannelMediaOptions options = new ChannelMediaOptions(); + options.channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; + options.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER; + TokenUtils.gen(requireActivity(), channelId, uid, token -> { + RtcEngine engine = rtcEngine; + if (!isAdded() || engine == null) { + return; + } + int ret = engine.joinChannel(token, channelId, uid, options); + if (ret != Constants.ERR_OK) { + showAlert(String.format(Locale.US, "%s\ncode:%d", RtcEngine.getErrorDescription(ret), ret)); + } + }); + + mLocalVideoLayout = new VideoReportLayout(requireContext()); + TextureView videoView = new TextureView(requireContext()); + VideoCanvas local = new VideoCanvas(videoView, Constants.RENDER_MODE_HIDDEN, 0); + local.mirrorMode = Constants.VIDEO_MIRROR_MODE_DISABLED; + rtcEngine.setupLocalVideo(local); + mLocalVideoLayout.addView(videoView); + + updateVideoLayouts(isLocalFull); + } + + private void updateVideoLayouts(boolean isLocalFull) { + this.isLocalFull = isLocalFull; + if (isLocalFull) { + if (mLocalVideoLayout != null) { + ViewParent parent = mLocalVideoLayout.getParent(); + if (parent instanceof ViewGroup && parent != mBinding.fullVideoContainer) { + ((ViewGroup) parent).removeView(mLocalVideoLayout); + mBinding.fullVideoContainer.addView(mLocalVideoLayout); + } else if (parent == null) { + mBinding.fullVideoContainer.addView(mLocalVideoLayout); + } + } + + if (mRemoteVideoLayout != null) { + mRemoteVideoLayout.getChildAt(0).setOnClickListener(v -> updateVideoLayouts(!SenseTimeBeauty.this.isLocalFull)); + ViewParent parent = mRemoteVideoLayout.getParent(); + if (parent instanceof ViewGroup && parent != mBinding.smallVideoContainer) { + ((ViewGroup) parent).removeView(mRemoteVideoLayout); + mBinding.smallVideoContainer.addView(mRemoteVideoLayout); + } else if (parent == null) { + mBinding.smallVideoContainer.addView(mRemoteVideoLayout); + } + } + } else { + if (mLocalVideoLayout != null) { + mLocalVideoLayout.getChildAt(0).setOnClickListener(v -> updateVideoLayouts(!SenseTimeBeauty.this.isLocalFull)); + ViewParent parent = mLocalVideoLayout.getParent(); + if (parent instanceof ViewGroup && parent != mBinding.smallVideoContainer) { + ((ViewGroup) parent).removeView(mLocalVideoLayout); + mBinding.smallVideoContainer.addView(mLocalVideoLayout); + } else if (parent == null) { + mBinding.smallVideoContainer.addView(mLocalVideoLayout); + } + } + + if (mRemoteVideoLayout != null) { + ViewParent parent = mRemoteVideoLayout.getParent(); + if (parent instanceof ViewGroup && parent != mBinding.fullVideoContainer) { + ((ViewGroup) parent).removeView(mRemoteVideoLayout); + mBinding.fullVideoContainer.addView(mRemoteVideoLayout); + } else if (parent == null) { + mBinding.fullVideoContainer.addView(mRemoteVideoLayout); + } + } + } + } + + +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeautySDK.kt b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeautySDK.kt new file mode 100644 index 000000000..a8409629c --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/SenseTimeBeautySDK.kt @@ -0,0 +1,531 @@ +package io.agora.api.example.examples.advanced.beauty + +import android.content.Context +import android.text.TextUtils +import android.util.Log +import com.softsugar.stmobile.STMobileAuthentificationNative +import com.softsugar.stmobile.STMobileEffectNative +import com.softsugar.stmobile.STMobileEffectParams +import com.softsugar.stmobile.STMobileHumanActionNative +import com.softsugar.stmobile.params.STEffectBeautyType +import io.agora.api.example.examples.advanced.beauty.utils.FileUtils +import io.agora.beautyapi.sensetime.SenseTimeBeautyAPI + +object SenseTimeBeautySDK { + private const val TAG = "SenseTimeBeautySDK" + + private val resourcePath = "beauty_sensetime" + private val humanActionCreateConfig = 0 + + private const val MODEL_106 = "models/M_SenseME_Face_Video_Template_p_3.9.0.3.model" // 106 + // private const val MODEL_FACE_EXTRA = "models/M_SenseME_Face_Extra_Advanced_Template_p_2.0.0.model" // 282 + // private const val MODEL_AVATAR_HELP = "models/M_SenseME_Avatar_Help_p_2.3.7.model" // avatar face driving + // private const val MODEL_LIPS_PARSING = "models/M_SenseME_MouthOcclusion_p_1.3.0.1.model" // lips segmentation + // private const val MODEL_HAND = "models/M_SenseME_Hand_p_6.0.8.1.model" // gesture + // private const val MODEL_SEGMENT = "models/M_SenseME_Segment_Figure_p_4.14.1.1.model" // foreground and background segmentation + // private const val MODEL_SEGMENT_HAIR = "models/M_SenseME_Segment_Hair_p_4.4.0.model" // hair segmentation + // private const val MODEL_FACE_OCCLUSION = "models/M_SenseME_FaceOcclusion_p_1.0.7.1.model" // makeup occlusion + // private const val MODEL_SEGMENT_SKY = "models/M_SenseME_Segment_Sky_p_1.1.0.1.model" // sky segmentation + // private const val MODEL_SEGMENT_SKIN = "models/M_SenseME_Segment_Skin_p_1.0.1.1.model" // skin segmentation + // private const val MODEL_3DMESH = "models/M_SenseME_3DMesh_Face2396pt_280kpts_Ear_p_1.1.0v2.model" // 3DMesh + // private const val MODEL_HEAD_P_EAR = "models/M_SenseME_Ear_p_1.0.1.1.model" // ear model with mesh + // private const val MODEL_360HEAD_INSTANCE = "models/M_SenseME_3Dmesh_360Head2396pt_p_1.0.0.1.model" // 360-degree head mesh + // private const val MODEL_FOOT = "models/M_SenseME_Foot_p_2.10.7.model" // shoe detection model + // private const val MODEL_PANT = "models/M_SenseME_Segment_Trousers_p_1.1.10.model" // trouser leg detection + // private const val MODEL_WRIST = "models/M_SenseME_Wrist_p_1.4.0.model" // watch try-on + // private const val MODEL_CLOTH = "models/M_SenseME_Segment_Clothes_p_1.0.2.2.model" // clothing segmentation + // private const val MODEL_HEAD_INSTANCE = "models/M_SenseME_Segment_Head_Instance_p_1.1.0.1.model" // instance segmentation version + // private const val MODEL_HEAD_P_INSTANCE = "models/M_SenseME_Head_p_1.3.0.1.model" // 360-degree head model + // private const val MODEL_NAIL = "models/M_SenseME_Nail_p_2.4.0.model" // nail detection + + private var stickerPackageId = 0 + + // Effect handle + private var _mobileEffectNative: STMobileEffectNative? = null + val mobileEffectNative + get() = _mobileEffectNative ?: throw RuntimeException("Please initMobileEffect firstly!") + + // Face recognition handle + private var _humanActionNative: STMobileHumanActionNative? = null + val humanActionNative + get() = _humanActionNative ?: throw RuntimeException("Please initBeautySDK firstly!") + + // Beauty configuration + val beautyConfig = BeautyConfig() + + private var beautyAPI: SenseTimeBeautyAPI? = null + + private var authSuccess = false + + fun initBeautySDK(context: Context): Boolean { + if (checkLicense(context)) { + initHumanAction(context) + authSuccess = true + return true + } + initHumanAction(context) + return false + } + + fun isAuthSuccess(): Boolean { + return authSuccess + } + + fun unInitBeautySDK() { + beautyAPI = null + authSuccess = false + unInitHumanActionNative() + beautyConfig.reset() + } + + fun initMobileEffect(context: Context) { + if (_mobileEffectNative != null) { + return + } + _mobileEffectNative = STMobileEffectNative() + val result = + _mobileEffectNative?.createInstance(context, STMobileEffectNative.EFFECT_CONFIG_NONE) + _mobileEffectNative?.setParam(STMobileEffectParams.EFFECT_PARAM_QUATERNION_SMOOTH_FRAME, 5f) + Log.d(TAG, "SenseTime >> STMobileEffectNative create result : $result") + beautyConfig.resume() + } + + fun unInitMobileEffect() { + _mobileEffectNative?.destroyInstance() + _mobileEffectNative = null + } + + private fun checkLicense(context: Context): Boolean { + val license = FileUtils.getAssetsString( + context, + "$resourcePath/license/SenseME.lic" + ) + if(TextUtils.isEmpty(license)){ + return false + } + val activeCode = STMobileAuthentificationNative.generateActiveCodeFromBuffer( + context, + license, + license.length + ) + Log.d(TAG, "SenseTime >> checkLicense activeCode=$activeCode") + return activeCode.isNotEmpty() + } + + private fun initHumanAction(context: Context) { + if (_humanActionNative != null) { + return + } + _humanActionNative = STMobileHumanActionNative() + val assets = context.assets + val result = _humanActionNative?.createInstanceFromAssetFile("$resourcePath/$MODEL_106", humanActionCreateConfig, assets) + Log.d(TAG, "SenseTime >> STMobileHumanActionNative create result : $result") + + if (result != 0) { + return + } + + // Other model configurations + // _humanActionNative?.addSubModelFromAssetFile("$resourcePath/$MODEL_FACE_EXTRA", assets) + + // Background segmentation feathering degree [0,1] (default value 0.35), 0 means no feathering at all, 1 means maximum feathering. + // When strength is small, feathering remains basically unchanged. The larger the value, the wider the transition edge between foreground and background. + // _humanActionNative?.setParam( + // STHumanActionParamsType.ST_HUMAN_ACTION_PARAM_BACKGROUND_BLUR_STRENGTH, + // 0.35f + // ) + // Set face mesh result output coordinate system (0: screen coordinate system, 1: 3D world coordinate system, + // 2: 3D camera coordinate system, which is the camera perspective projection coordinate system, with origin at the camera, default is 0) + // _humanActionNative?.setParam( + // STHumanActionParamsType.ST_HUMAN_ACTION_PARAM_FACE_MESH_OUTPUT_FORMAT, + // 1.0f + // ) + // Set mesh rendering mode + // _humanActionNative?.setParam( + // STHumanActionParamsType.ST_HUMAN_ACTION_PARAM_MESH_MODE, + // STCommonNative.MESH_CONFIG.toFloat() + // ) + // Set head instance segmentation + // _humanActionNative?.setParam( + // STHumanActionParamsType.ST_HUMAN_ACTION_PARAM_HEAD_SEGMENT_INSTANCE, + // 1.0f + // ) + } + + + private fun unInitHumanActionNative() { + _humanActionNative?.destroyInstance() + _humanActionNative = null + } + + + internal fun setBeautyAPI(beautyAPI: SenseTimeBeautyAPI?){ + SenseTimeBeautySDK.beautyAPI = beautyAPI + beautyConfig.resume() + } + + private fun runOnBeautyThread(run: () -> Unit) { + beautyAPI?.runOnProcessThread(run) ?: run.invoke() + } + + open class BeautyConfig { + // Smoothing + var smooth = 0.75f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyMode( + STEffectBeautyType.EFFECT_BEAUTY_BASE_FACE_SMOOTH, + STEffectBeautyType.SMOOTH2_MODE + ) + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_BASE_FACE_SMOOTH, + value + ) + } + } + + // Whitening + var whiten = 0.75f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyMode( + STEffectBeautyType.EFFECT_BEAUTY_BASE_WHITTEN, + STEffectBeautyType.WHITENING3_MODE + ) + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_BASE_WHITTEN, + value + ) + } + } + + // Face thinning + var thinFace = 0.3f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_THIN_FACE, + value + ) + } + } + + // Eye enlargement + var enlargeEye = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_RESHAPE_ENLARGE_EYE, + value + ) + } + } + + // Skin redness + var redden = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_BASE_REDDEN, + value + ) + } + } + + // Cheekbone thinning + var shrinkCheekbone = 0.3f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_SHRINK_CHEEKBONE, + value + ) + } + } + + // Jawbone thinning + var shrinkJawbone = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_SHRINK_JAWBONE, + value + ) + } + } + + // Teeth whitening + var whiteTeeth = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_WHITE_TEETH, + value + ) + } + } + + // Hairline height + var hairlineHeight = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_HAIRLINE_HEIGHT, + value + ) + } + } + + // Nose narrowing + var narrowNose = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_NARROW_NOSE, + value + ) + } + } + + // Mouth size + var mouthSize = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_MOUTH_SIZE, + value + ) + } + } + + // Chin length + var chinLength = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_CHIN_LENGTH, value + ) + } + } + + // Bright eyes + var brightEye = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_BRIGHT_EYE, + value + ) + } + } + + // Remove dark circles + var darkCircles = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_REMOVE_DARK_CIRCLES, + value + ) + } + } + + // Remove nasolabial folds + var nasolabialFolds = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_REMOVE_NASOLABIAL_FOLDS, + value + ) + } + } + + // Saturation + var saturation = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_SATURATION, + value + ) + } + } + + // Contrast + var contrast = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_CONTRAST, + value + ) + } + } + + // Sharpen + var sharpen = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_SHARPEN, + value + ) + } + } + + // Clarity + var clear = 0.0f + set(value) { + field = value + val effectNative = _mobileEffectNative ?: return + runOnBeautyThread { + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_CLEAR, + value + ) + } + } + + // Makeup + var makeUp: MakeUpItem? = null + set(value) { + field = value + runOnBeautyThread { + if (value == null) { + _mobileEffectNative?.setBeauty( + STEffectBeautyType.EFFECT_BEAUTY_MAKEUP_ALL, + null + ) + } else { + val assets = value.context.assets + _mobileEffectNative?.setBeautyFromAssetsFile( + STEffectBeautyType.EFFECT_BEAUTY_MAKEUP_ALL, + "$resourcePath/${value.path}", + assets + ) + _mobileEffectNative?.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_MAKEUP_ALL, + value.strength + ) + } + } + } + + // Sticker + var sticker: StickerItem? = null + set(value) { + field = value + runOnBeautyThread { + if (value == null) { + if (stickerPackageId > 0) { + _mobileEffectNative?.removeEffect(stickerPackageId) + stickerPackageId = 0 + } + } else { + stickerPackageId = _mobileEffectNative?.changePackageFromAssetsFile( + "$resourcePath/${value.path}", + value.context.assets + ) ?: 0 + } + } + } + + internal fun reset() { + smooth = 0.75f + whiten = 0.75f + thinFace = 0.3f + enlargeEye = 0.0f + sharpen = 0.0f + clear = 0.0f + redden = 0.0f + shrinkCheekbone = 0.3f + shrinkJawbone = 0.0f + whiteTeeth = 0.0f + hairlineHeight = 0.0f + narrowNose = 0.0f + mouthSize = 0.0f + chinLength = 0.0f + brightEye = 0.0f + darkCircles = 0.0f + nasolabialFolds = 0.0f + saturation = 0.0f + contrast = 0.0f + + makeUp = null + sticker = null + } + + internal fun resume() { + smooth = smooth + whiten = whiten + thinFace = thinFace + enlargeEye = enlargeEye + sharpen = sharpen + clear = clear + redden = redden + shrinkCheekbone = shrinkCheekbone + shrinkJawbone = shrinkJawbone + whiteTeeth = whiteTeeth + hairlineHeight = hairlineHeight + narrowNose = narrowNose + mouthSize = mouthSize + chinLength = chinLength + brightEye = brightEye + darkCircles = darkCircles + nasolabialFolds = nasolabialFolds + saturation = saturation + contrast = contrast + + makeUp = makeUp + sticker = sticker + } + } + + data class MakeUpItem( + val context: Context, + val path: String, + val strength: Float + ) + + data class StickerItem( + val context: Context, + val path: String + ) +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPI.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPI.kt new file mode 100644 index 000000000..f3cd71c49 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPI.kt @@ -0,0 +1,215 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime + +import android.content.Context +import android.view.View +import com.softsugar.stmobile.STMobileEffectNative +import com.softsugar.stmobile.STMobileHumanActionNative +import io.agora.base.VideoFrame +import io.agora.rtc2.Constants +import io.agora.rtc2.RtcEngine + +const val VERSION = "1.0.10" + +enum class CaptureMode { + /** + * Use the internal raw data interface of Agora for processing. + * + */ + Agora, + + /** + * In custom mode, you need to call the [io.agora.rtc2.video.IVideoFrameObserver] interface + * yourself to pass the raw video frame to the BeautyAPI for processing. + * + */ + Custom +} + +interface IEventCallback { + + /** + * The statistics callback triggers once after each processed frame. + * + * @param stats Beauty statistics data. + */ + fun onBeautyStats(stats: BeautyStats) +} + +data class BeautyStats( + val minCostMs: Long, // Minimum value within the statistics interval + val maxCostMs: Long, // Maximum value within the statistics interval + val averageCostMs: Long // Average value within the statistics interval +) + +enum class MirrorMode { + + // Definition of no mirror normal view: + // For the front camera, the captured view is flipped horizontally on the phone screen; + // for the rear camera, it appears the same as seen on the phone. + + // Mirror both locally and remotely, default for front camera. Stickers are correctly oriented on both local and remote views. + MIRROR_LOCAL_REMOTE, + + // Mirror only locally, no remote mirroring. Used in calling or e-commerce live streaming (ensures signage in the background appears correct). + // Due to flipped local and remote views, one side's text/stickers will appear reversed. + MIRROR_LOCAL_ONLY, + + // Mirror only remotely, no local mirroring. Stickers are correctly oriented on the remote view, mirrored locally. + MIRROR_REMOTE_ONLY, + + // No mirroring for both local and remote views, default for rear camera. Stickers are correctly oriented on both views. + MIRROR_NONE +} + +data class CameraConfig( + val frontMirror: MirrorMode = MirrorMode.MIRROR_LOCAL_REMOTE, // Default front camera mirror: both local and remote mirrored + val backMirror: MirrorMode = MirrorMode.MIRROR_NONE // Default back camera mirror: neither local nor remote mirrored +) + +data class Config( + val context: Context, // Android context + val rtcEngine: RtcEngine, // Agora RTC engine + val stHandlers: STHandlers, // Beauty SDK handler + val eventCallback: IEventCallback? = null, // Event callback + val captureMode: CaptureMode = CaptureMode.Agora, // Capture mode + val statsDuration: Long = 1000, // Stats interval duration + val statsEnable: Boolean = false, // Enable stats or not + val cameraConfig: CameraConfig = CameraConfig() // Camera mirror configuration +) + +data class STHandlers( + val effectNative: STMobileEffectNative, + val humanActionNative: STMobileHumanActionNative +) + +enum class ErrorCode(val value: Int) { + ERROR_OK(0), // All operations are normal + ERROR_HAS_NOT_INITIALIZED(101), // Called other APIs without initializing or after failed initialization + ERROR_HAS_INITIALIZED(102), // Error when calling Initialize again after successful initialization + ERROR_HAS_RELEASED(103), // Called other APIs after release has been invoked + ERROR_PROCESS_NOT_CUSTOM(104), // Called onFrame to pass video frames externally when not in Custom processing mode + ERROR_VIEW_TYPE_ERROR(105), // Error returned if the view type is incorrect when setupLocalVideo is called + ERROR_FRAME_SKIPPED(106), // Returned in onFrame when a frame is skipped during processing +} + +enum class BeautyPreset { + CUSTOM, // Do not use the recommended beauty parameters + DEFAULT // Default preset +} + +fun createSenseTimeBeautyAPI(): SenseTimeBeautyAPI = SenseTimeBeautyAPIImpl() + +interface SenseTimeBeautyAPI { + + /** + * Initializes the API. + * + * @param config Configuration parameters + * @return [ErrorCode] corresponding to the result of initialization + */ + fun initialize(config: Config): Int + + /** + * Enable/Disable beauty effects. + * + * @param enable true: Enable; false: Disable + * @return [ErrorCode] corresponding to the result of the operation + */ + fun enable(enable: Boolean): Int + + /** + * Sets up local video rendering, with internal handling of mirror mode. + * + * @param view SurfaceView or TextureView for rendering the video + * @param renderMode Scaling mode for rendering (e.g., Constants.RENDER_MODE_HIDDEN) + * @return ErrorCode corresponding to the result of the operation + */ + fun setupLocalVideo(view: View, renderMode: Int = Constants.RENDER_MODE_HIDDEN): Int + + /** + * When ProcessMode == [CaptureMode.Custom], external input of raw video frames is required. + * + * @param videoFrame The raw video frame + * @return [ErrorCode] corresponding to the result of the operation + */ + fun onFrame(videoFrame: VideoFrame): Int + + /** + * Sets the best default beauty parameters provided by Agora. + * + * @param preset The beauty preset, defaulting to [BeautyPreset.DEFAULT] + * @return [ErrorCode] corresponding to the result of the operation + */ + fun setBeautyPreset(preset: BeautyPreset = BeautyPreset.DEFAULT): Int + + /** + * Updates the camera configuration. + * + * @param config New camera configuration to apply + * @return [ErrorCode] corresponding to the result of the operation + */ + fun updateCameraConfig(config: CameraConfig): Int + + /** + * Checks if the current camera is the front camera. + * Note: This returns an accurate value only during beauty processing; otherwise, it will always return true. + * + * @return true if the current camera is the front camera, false otherwise + */ + fun isFrontCamera(): Boolean + + /** + * Retrieves the current mirror status. + * + * @return true if mirroring is applied, false if it is not. + */ + fun getMirrorApplied(): Boolean + + + /** + * Executes an operation within the processing thread. + * + * @param run The operation to execute. + */ + fun runOnProcessThread(run: () -> Unit) + + /** + * Private parameter configuration for internal API calls, primarily for testing. + * + * @param key The parameter key. + * @param value The parameter value. + */ + fun setParameters(key: String, value: String) + + /** + * Releases resources. Once released, this instance can no longer be used. + * + * @return Refer to ErrorCode + */ + fun release(): Int + +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt new file mode 100644 index 000000000..cb3f6a7b8 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt @@ -0,0 +1,930 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime + +import android.annotation.TargetApi +import android.graphics.Matrix +import android.opengl.GLES11Ext +import android.opengl.GLES20 +import android.opengl.GLES30 +import android.os.Build +import android.view.SurfaceView +import android.view.TextureView +import android.view.View +import com.softsugar.stmobile.STCommonNative +import com.softsugar.stmobile.params.STEffectBeautyType +import io.agora.base.TextureBufferHelper +import io.agora.base.VideoFrame +import io.agora.base.VideoFrame.I420Buffer +import io.agora.base.VideoFrame.SourceType +import io.agora.base.VideoFrame.TextureBuffer +import io.agora.base.internal.video.RendererCommon +import io.agora.base.internal.video.TextureBufferPool +import io.agora.base.internal.video.YuvConverter +import io.agora.base.internal.video.YuvHelper +import io.agora.beautyapi.sensetime.utils.APIReporter +import io.agora.beautyapi.sensetime.utils.APIType +import io.agora.beautyapi.sensetime.utils.LogUtils +import io.agora.beautyapi.sensetime.utils.StatsHelper +import io.agora.beautyapi.sensetime.utils.processor.IBeautyProcessor +import io.agora.beautyapi.sensetime.utils.processor.InputInfo +import io.agora.beautyapi.sensetime.utils.processor.createBeautyProcessor +import io.agora.rtc2.Constants +import io.agora.rtc2.gl.EglBaseProvider +import io.agora.rtc2.video.IVideoFrameObserver +import io.agora.rtc2.video.VideoCanvas +import java.nio.ByteBuffer +import java.util.Collections +import java.util.concurrent.Callable +import java.util.concurrent.Executors + +class SenseTimeBeautyAPIImpl : SenseTimeBeautyAPI, IVideoFrameObserver { + private val TAG = "SenseTimeBeautyAPIImpl" + + /** + * Beauty mode + * 美颜模式 + * 0: Automatically switch based on buffer type, + * 根据缓冲类型自动切换, + * 1: Fixed use of OES texture, + * 固定使用 OES 纹理, + * 2: Fixed use of i420, + * 固定使用 i420。 + */ + private var beautyMode = 0 + + private var textureBufferHelper: TextureBufferHelper? = null + private var nv21ByteBuffer: ByteBuffer? = null + private var config: Config? = null + private var enable: Boolean = false + private var enableChange: Boolean = false + private var isReleased: Boolean = false + private var captureMirror = true + private var renderMirror = false + private var statsHelper: StatsHelper? = null + private var skipFrame = 0 + private val workerThreadExecutor = Executors.newSingleThreadExecutor() + private var beautyProcessor: IBeautyProcessor? = null + private var isFrontCamera = true + private var cameraConfig = CameraConfig() + private var localVideoRenderMode = Constants.RENDER_MODE_HIDDEN + private val pendingProcessRunList = Collections.synchronizedList(mutableListOf<()->Unit>()) + private val apiReporter by lazy { APIReporter(APIType.BEAUTY, VERSION, config!!.rtcEngine) } + + private enum class ProcessSourceType{ + UNKNOWN, + TEXTURE_OES_API26, + TEXTURE_2D_API26, + TEXTURE_OES, + TEXTURE_2D, + I420, + } + private var currProcessSourceType = ProcessSourceType.UNKNOWN + + /** + * Initializes the API. + * 初始化 API。 + * + * @param config Configuration parameters + * 配置参数 + * @return [ErrorCode] corresponding to the result of initialization + * 对应初始化结果的错误代码 + */ + override fun initialize(config: Config): Int { + if (this.config != null) { + LogUtils.e(TAG, "initialize >> The beauty api has been initialized!") + return ErrorCode.ERROR_HAS_INITIALIZED.value + } + this.config = config + this.cameraConfig = config.cameraConfig + if (config.captureMode == CaptureMode.Agora) { + config.rtcEngine.registerVideoFrameObserver(this) + } + statsHelper = StatsHelper(config.statsDuration) { + this.config?.eventCallback?.onBeautyStats(it) + } + cameraConfig = CameraConfig(config.cameraConfig.frontMirror, config.cameraConfig.backMirror) + LogUtils.i(TAG, "initialize >> config = $config") + LogUtils.i(TAG, "initialize >> beauty api version=$VERSION, beauty sdk version=${STCommonNative.getVersion()}") + apiReporter.reportFuncEvent( + "initialize", + mapOf( + "captureMode" to config.captureMode, + "statsDuration" to config.statsDuration, + "statsEnable" to config.statsEnable, + "cameraConfig" to config.cameraConfig, + ), + emptyMap() + ) + apiReporter.startDurationEvent("initialize-release") + return ErrorCode.ERROR_OK.value + } + + /** + * Enable/Disable beauty effects. + * 启用/禁用美颜效果。 + * + * @param enable true: Enable; false: Disable + * true: 启用;false: 禁用 + * @return [ErrorCode] corresponding to the result of the operation + * 对应操作结果的错误代码 + */ + override fun enable(enable: Boolean): Int { + LogUtils.i(TAG, "enable >> enable = $enable") + if (config == null) { + LogUtils.e(TAG, "enable >> The beauty api has not been initialized!") + return ErrorCode.ERROR_HAS_NOT_INITIALIZED.value + } + if (isReleased) { + LogUtils.e(TAG, "enable >> The beauty api has been released!") + return ErrorCode.ERROR_HAS_RELEASED.value + } + if(config?.captureMode == CaptureMode.Custom){ + skipFrame = 2 + LogUtils.i(TAG, "enable >> skipFrame = $skipFrame") + } + apiReporter.reportFuncEvent( + "enable", + mapOf("enable" to enable), + emptyMap() + ) + + if(this.enable != enable){ + this.enable = enable + this.enableChange = true + LogUtils.i(TAG, "enable >> enableChange") + } + + return ErrorCode.ERROR_OK.value + } + + /** + * Sets up local video rendering, with internal handling of mirror mode. + * 设置本地视频渲染,内部处理镜像模式。 + * + * @param view SurfaceView or TextureView for rendering the video + * 用于渲染视频的 SurfaceView 或 TextureView + * @param renderMode Scaling mode for rendering (e.g., Constants.RENDER_MODE_HIDDEN) + * 渲染的缩放模式(例如,Constants.RENDER_MODE_HIDDEN) + * @return [ErrorCode] corresponding to the result of the operation + * 对应操作结果的错误代码 + */ + override fun setupLocalVideo(view: View, renderMode: Int): Int { + val rtcEngine = config?.rtcEngine + if(rtcEngine == null){ + LogUtils.e(TAG, "setupLocalVideo >> The beauty api has not been initialized!") + return ErrorCode.ERROR_HAS_NOT_INITIALIZED.value + } + LogUtils.i(TAG, "setupLocalVideo >> view=$view, renderMode=$renderMode") + localVideoRenderMode = renderMode + apiReporter.reportFuncEvent( + "setupLocalVide", + mapOf("view" to view, "renderMode" to renderMode), + emptyMap() + ) + if(view is TextureView || view is SurfaceView){ + val canvas = VideoCanvas(view, renderMode, 0) + canvas.mirrorMode = Constants.VIDEO_MIRROR_MODE_DISABLED + rtcEngine.setupLocalVideo(canvas) + return ErrorCode.ERROR_OK.value + } + return ErrorCode.ERROR_VIEW_TYPE_ERROR.value + } + + /** + * When ProcessMode == [CaptureMode.Custom], external input of raw video frames is required. + * 当处理模式为 [CaptureMode.Custom] 时,需要外部输入原始视频帧。 + * + * @param videoFrame The raw video frame + * 原始视频帧 + * @return [ErrorCode] corresponding to the result of the operation + * 对应操作结果的错误代码 + */ + override fun onFrame(videoFrame: VideoFrame): Int { + val conf = config + if(conf == null){ + LogUtils.e(TAG, "onFrame >> The beauty api has not been initialized!") + return ErrorCode.ERROR_HAS_NOT_INITIALIZED.value + } + if (isReleased) { + LogUtils.e(TAG, "onFrame >> The beauty api has been released!") + return ErrorCode.ERROR_HAS_RELEASED.value + } + if (conf.captureMode != CaptureMode.Custom) { + LogUtils.e(TAG, "onFrame >> The capture mode is not Custom!") + return ErrorCode.ERROR_PROCESS_NOT_CUSTOM.value + } + if (processBeauty(videoFrame)) { + return ErrorCode.ERROR_OK.value + } + LogUtils.i(TAG, "onFrame >> Skip Frame.") + return ErrorCode.ERROR_FRAME_SKIPPED.value + } + + /** + * Sets the beauty preset. + * 设置美颜预设。 + * + * @param preset The beauty preset to apply + * 要应用的美颜预设 + * @return [ErrorCode] corresponding to the result of the operation + * 对应操作结果的错误代码 + */ + override fun setBeautyPreset(preset: BeautyPreset): Int { + val effectNative = config?.stHandlers?.effectNative + if(effectNative == null){ + LogUtils.e(TAG, "setBeautyPreset >> The beauty api has not been initialized!") + return ErrorCode.ERROR_HAS_NOT_INITIALIZED.value + } + if (isReleased) { + LogUtils.e(TAG, "setBeautyPreset >> The beauty api has been released!") + return ErrorCode.ERROR_HAS_RELEASED.value + } + LogUtils.i(TAG, "setBeautyPreset >> preset = $preset") + apiReporter.reportFuncEvent( + "setBeautyPreset", + mapOf("preset" to preset), + emptyMap() + ) + + val enable = preset == BeautyPreset.DEFAULT + workerThreadExecutor.submit { + // Sharpening + // 锐化 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_SHARPEN, + if(enable) 0.5f else 0.0f + ) + // Clarity + // 清晰度 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_CLEAR, + if(enable) 1.0f else 0.0f + ) + // Smooth skin + // 磨皮 + effectNative.setBeautyMode( + STEffectBeautyType.EFFECT_BEAUTY_BASE_FACE_SMOOTH, + STEffectBeautyType.SMOOTH2_MODE + ) + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_BASE_FACE_SMOOTH, + if(enable) 0.55f else 0.0f + ) + // Whitening + // 美白 + effectNative.setBeautyMode( + STEffectBeautyType.EFFECT_BEAUTY_BASE_WHITTEN, + STEffectBeautyType.WHITENING3_MODE + ) + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_BASE_WHITTEN, + if(enable) 0.2f else 0.0f + ) + // Slim face + // 瘦脸 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_THIN_FACE, + if(enable) 0.4f else 0.0f + ) + // Enlarged eyes + // 大眼 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_RESHAPE_ENLARGE_EYE, + if(enable) 0.3f else 0.0f + ) + // Reddening + // 红润 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_BASE_REDDEN, + if(enable) 0.0f else 0.0f + ) + // Slim cheekbones + // 瘦脸颊 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_SHRINK_CHEEKBONE, + if(enable) 0.0f else 0.0f + ) + // Jawbone + // 下颌骨 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_SHRINK_JAWBONE, + if(enable) 0.0f else 0.0f + ) + // White teeth + // 美牙 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_WHITE_TEETH, + if(enable) 0.0f else 0.0f + ) + // Hairline height + // 额头 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_HAIRLINE_HEIGHT, + if(enable) 0.0f else 0.0f + ) + // Slim nose + // 瘦鼻 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_NARROW_NOSE, + if(enable) 0.0f else 0.0f + ) + // Mouth shape + // 嘴形 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_MOUTH_SIZE, + if(enable) 0.0f else 0.0f + ) + // Chin length + // 下巴 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_CHIN_LENGTH, + if(enable) 0.0f else 0.0f + ) + // Bright eyes + // 亮眼 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_BRIGHT_EYE, + if(enable) 0.0f else 0.0f + ) + // Dark circle removal + // 祛黑眼圈 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_REMOVE_DARK_CIRCLES, + if(enable) 0.0f else 0.0f + ) + // Nasolabial folds removal + // 祛法令纹 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_PLASTIC_REMOVE_NASOLABIAL_FOLDS, + if(enable) 0.0f else 0.0f + ) + // Saturation + // 饱和度 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_SATURATION, + if(enable) 0.0f else 0.0f + ) + // Contrast + // 对比度 + effectNative.setBeautyStrength( + STEffectBeautyType.EFFECT_BEAUTY_TONE_CONTRAST, + if(enable) 0.0f else 0.0f + ) + } + return ErrorCode.ERROR_OK.value + } + + /** + * Executes an operation on the processing thread. + * 在处理线程中执行操作。 + * + * @param run The operation to execute. + * 要执行的操作。 + */ + override fun runOnProcessThread(run: () -> Unit) { + if (config == null) { + LogUtils.e(TAG, "runOnProcessThread >> The beauty api has not been initialized!") + return + } + if (isReleased) { + LogUtils.e(TAG, "runOnProcessThread >> The beauty api has been released!") + return + } + if (textureBufferHelper?.handler?.looper?.thread == Thread.currentThread()) { + run.invoke() + } else if (textureBufferHelper != null) { + textureBufferHelper?.invoke(run) + } else { + pendingProcessRunList.add(run) + } + } + + /** + * Updates the camera configuration. + * 更新摄像头配置。 + * + * @param config New camera configuration to apply + * 新的相机配置已应用 + * @return [ErrorCode] corresponding to the result of the operation + * 对应操作结果的错误代码 + */ + override fun updateCameraConfig(config: CameraConfig): Int { + LogUtils.i(TAG, "updateCameraConfig >> oldCameraConfig=$cameraConfig, newCameraConfig=$config") + cameraConfig = CameraConfig(config.frontMirror, config.backMirror) + apiReporter.reportFuncEvent( + "updateCameraConfig", + mapOf("config" to config), + emptyMap() + ) + + return ErrorCode.ERROR_OK.value + } + + /** + * Checks if the current camera is the front camera. + * 检查当前摄像头是否为前置摄像头。 + * Note: This returns an accurate value only during beauty processing; otherwise, it will always return true. + * 注意:此值仅在美颜处理期间返回准确值;否则,它将始终返回 true。 + * + * @return true if the current camera is the front camera, false otherwise + * 如果当前摄像头是前置摄像头,则返回 true,否则返回 false + */ + override fun isFrontCamera() = isFrontCamera + + /** + * Private parameter configuration for internal API calls, primarily for testing. + * 内部 API 调用的私有参数配置,主要用于测试。 + * + * @param key The parameter key. + * 参数键。 + * @param value The parameter value. + * 参数值。 + */ + override fun setParameters(key: String, value: String) { + apiReporter.reportFuncEvent("setParameters", mapOf("key" to key, "value" to value), emptyMap()) + when(key){ + "beauty_mode" -> beautyMode = value.toInt() + } + } + + /** + * Releases resources. Once released, this instance can no longer be used. + * 释放资源。一旦释放,该实例将无法再使用。 + * + * @return Refer to ErrorCode + * 参考 ErrorCode + */ + override fun release(): Int { + val conf = config + if(conf == null){ + LogUtils.e(TAG, "release >> The beauty api has not been initialized!") + return ErrorCode.ERROR_HAS_NOT_INITIALIZED.value + } + if (isReleased) { + LogUtils.e(TAG, "setBeautyPreset >> The beauty api has been released!") + return ErrorCode.ERROR_HAS_RELEASED.value + } + if (conf.captureMode == CaptureMode.Agora) { + conf.rtcEngine.registerVideoFrameObserver(null) + } + apiReporter.reportFuncEvent("release", emptyMap(), emptyMap()) + apiReporter.endDurationEvent("initialize-release", emptyMap()) + + LogUtils.i(TAG, "release") + isReleased = true + workerThreadExecutor.shutdown() + textureBufferHelper?.let { + textureBufferHelper = null + // it.handler.removeCallbacksAndMessages(null) + it.invoke { + beautyProcessor?.release() + null + } + it.dispose() + } + statsHelper?.reset() + statsHelper = null + pendingProcessRunList.clear() + return ErrorCode.ERROR_OK.value + } + + /** + * Processes the beauty effects on the given video frame. + * 在给定的视频帧上处理美颜效果。 + * + * @param videoFrame The video frame to process. + * 要处理的视频帧。 + * @return true if processing was successful, false otherwise. + * 如果处理成功则返回 true,否则返回 false。 + */ + private fun processBeauty(videoFrame: VideoFrame): Boolean { + if (isReleased) { + LogUtils.e(TAG, "processBeauty >> The beauty api has been released!") + return false + } + + + val cMirror = + if (isFrontCamera) { + when (cameraConfig.frontMirror) { + MirrorMode.MIRROR_LOCAL_REMOTE -> true + MirrorMode.MIRROR_LOCAL_ONLY -> false + MirrorMode.MIRROR_REMOTE_ONLY -> true + MirrorMode.MIRROR_NONE -> false + } + } else { + when (cameraConfig.backMirror) { + MirrorMode.MIRROR_LOCAL_REMOTE -> true + MirrorMode.MIRROR_LOCAL_ONLY -> false + MirrorMode.MIRROR_REMOTE_ONLY -> true + MirrorMode.MIRROR_NONE -> false + } + } + val rMirror = + if (isFrontCamera) { + when (cameraConfig.frontMirror) { + MirrorMode.MIRROR_LOCAL_REMOTE -> false + MirrorMode.MIRROR_LOCAL_ONLY -> true + MirrorMode.MIRROR_REMOTE_ONLY -> true + MirrorMode.MIRROR_NONE -> false + } + } else { + when (cameraConfig.backMirror) { + MirrorMode.MIRROR_LOCAL_REMOTE -> false + MirrorMode.MIRROR_LOCAL_ONLY -> true + MirrorMode.MIRROR_REMOTE_ONLY -> true + MirrorMode.MIRROR_NONE -> false + } + } + if (captureMirror != cMirror || renderMirror != rMirror) { + LogUtils.w(TAG, "processBeauty >> enable=$enable, captureMirror=$captureMirror->$cMirror, renderMirror=$renderMirror->$rMirror") + captureMirror = cMirror + if(renderMirror != rMirror){ + renderMirror = rMirror + config?.rtcEngine?.setLocalRenderMode( + localVideoRenderMode, + if(renderMirror) Constants.VIDEO_MIRROR_MODE_ENABLED else Constants.VIDEO_MIRROR_MODE_DISABLED + ) + } + textureBufferHelper?.invoke { + beautyProcessor?.reset() + } + apiReporter.startDurationEvent("first_beauty_frame") + return false + } + + val oldIsFrontCamera = isFrontCamera + isFrontCamera = videoFrame.sourceType == SourceType.kFrontCamera + if(oldIsFrontCamera != isFrontCamera){ + LogUtils.w(TAG, "processBeauty >> oldIsFrontCamera=$oldIsFrontCamera, isFrontCamera=$isFrontCamera") + return false + } + + if(enableChange){ + enableChange = false + textureBufferHelper?.invoke { + beautyProcessor?.reset() + } + } + + if(!enable){ + return true + } + + if (textureBufferHelper == null) { + textureBufferHelper = TextureBufferHelper.create( + "STRender", + EglBaseProvider.instance().rootEglBase.eglBaseContext + ) + textureBufferHelper?.invoke { + synchronized(pendingProcessRunList){ + val iterator = pendingProcessRunList.iterator() + while (iterator.hasNext()){ + iterator.next().invoke() + iterator.remove() + } + } + } + LogUtils.i(TAG, "processBeauty >> create texture buffer, beautyMode=$beautyMode") + } + + val startTime = System.currentTimeMillis() + + val processTexId = when(beautyMode){ + 1 -> processBeautyTexture(videoFrame) + 2 -> processBeautyI420(videoFrame) + else -> processBeautyAuto(videoFrame) + } + if(config?.statsEnable == true){ + val costTime = System.currentTimeMillis() - startTime + statsHelper?.once(costTime) + } + + if (processTexId < 0) { + LogUtils.w(TAG, "processBeauty >> processTexId < 0") + return false + } + + if(skipFrame > 0){ + skipFrame -- + LogUtils.w(TAG, "processBeauty >> skipFrame=$skipFrame") + return false + } + + val newFence = textureBufferHelper?.invoke { + val texBuffer = videoFrame.buffer as? TextureBuffer ?: return@invoke 0L + val fenceOpen = GLES30.glIsSync(texBuffer.fenceObject) + if (fenceOpen) { + val glFenceSync = GLES30.glFenceSync(GLES30.GL_SYNC_GPU_COMMANDS_COMPLETE, 0) + GLES20.glFlush() + return@invoke glFenceSync + } + GLES20.glFinish() + return@invoke 0L + } ?: 0L + + apiReporter.endDurationEvent("first_beauty_frame", emptyMap()) + + val processBuffer: TextureBuffer = textureBufferHelper?.wrapTextureBuffer( + videoFrame.rotatedWidth, + videoFrame.rotatedHeight, + TextureBuffer.Type.RGB, + processTexId, + newFence, + Matrix() + ) ?: return false + videoFrame.replaceBuffer(processBuffer, 0, videoFrame.timestampNs) + return true + } + + /** + * Processes beauty effects automatically based on the video frame. + * 根据视频帧自动处理美颜效果。 + * + * @param videoFrame The video frame to process + * 要处理的视频帧 + * @return The texture ID after processing + * 处理后的纹理 ID + */ + private fun processBeautyAuto(videoFrame: VideoFrame): Int { + val buffer = videoFrame.buffer + return if(buffer is TextureBuffer){ + processBeautyTexture(videoFrame) + } else { + processBeautyI420(videoFrame) + } + } + + private fun mayCreateBeautyProcess(){ + val stHandlers = config?.stHandlers ?: return + + if(beautyProcessor == null){ + beautyProcessor = createBeautyProcessor().apply { + initialize(stHandlers.effectNative, stHandlers.humanActionNative) + } + } + } + + /** + * Processes beauty effects using texture for API level 26 and above. + * 使用纹理处理美颜效果,适用于 API 级别 26 及以上。 + * + * @param videoFrame The video frame to process + * 要处理的视频帧 + * @return The texture ID after processing + * 处理后的纹理 ID + */ + @TargetApi(26) + private fun processBeautyTextureAPI26(videoFrame: VideoFrame): Int{ + val texBufferHelper = textureBufferHelper ?: return -1 + val buffer = videoFrame.buffer as? TextureBuffer ?: return -1 + val width = buffer.width + val height = buffer.height + + when(buffer.type){ + TextureBuffer.Type.OES -> { + if(currProcessSourceType != ProcessSourceType.TEXTURE_OES_API26){ + LogUtils.i(TAG, "processBeautyAuto >> process source type change old=$currProcessSourceType, new=${ProcessSourceType.TEXTURE_OES_API26}") + currProcessSourceType = ProcessSourceType.TEXTURE_OES_API26 + } + } + else -> { + if(currProcessSourceType != ProcessSourceType.TEXTURE_2D_API26){ + LogUtils.i(TAG, "processBeautyAuto >> process source type change old=$currProcessSourceType, new=${ProcessSourceType.TEXTURE_2D_API26}") + currProcessSourceType = ProcessSourceType.TEXTURE_2D_API26 + } + } + } + + val matrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(buffer.transformMatrix) + return texBufferHelper.invoke(Callable { + mayCreateBeautyProcess() + return@Callable beautyProcessor?.process( + InputInfo( + width = width, + height = height, + cameraOrientation = videoFrame.rotation, + isFrontCamera = isFrontCamera, + isMirror = (isFrontCamera && !captureMirror) || (!isFrontCamera && captureMirror), + timestamp = videoFrame.timestampNs, + textureId = buffer.textureId, + textureType = when (buffer.type) { + TextureBuffer.Type.OES -> GLES11Ext.GL_TEXTURE_EXTERNAL_OES + else -> GLES20.GL_TEXTURE_2D + }, + textureMatrix = matrix, + ) + )?.textureId ?: -1 + }) + } + + /** + * Processes beauty effects using I420 format. + * 使用 I420 格式处理美颜效果。 + * + * @param videoFrame The video frame to process + * 要处理的视频帧 + * @return The texture ID after processing + * 处理后的纹理 ID + */ + private fun processBeautyI420(videoFrame: VideoFrame): Int{ + val texBufferHelper = textureBufferHelper ?: return -1 + val nv21ByteArray = getNV21Buffer(videoFrame) ?: return -1 + val buffer = videoFrame.buffer + val width = buffer.width + val height = buffer.height + + if(currProcessSourceType != ProcessSourceType.I420){ + LogUtils.i(TAG, "processBeautyAuto >> process source type change old=$currProcessSourceType, new=${ProcessSourceType.I420}") + currProcessSourceType = ProcessSourceType.I420 + } + + return texBufferHelper.invoke(Callable { + mayCreateBeautyProcess() + return@Callable beautyProcessor?.process( + InputInfo( + width = width, + height = height, + cameraOrientation = videoFrame.rotation, + isFrontCamera = videoFrame.sourceType == SourceType.kFrontCamera, + isMirror = (isFrontCamera && !captureMirror) || (!isFrontCamera && captureMirror), + timestamp = videoFrame.timestampNs, + bytes = nv21ByteArray, + bytesType = STCommonNative.ST_PIX_FMT_NV21 + ) + )?.textureId ?: -1 + }) + } + + /** + * Processes beauty effects using texture. + * 使用纹理处理美颜效果。 + * + * @param videoFrame The video frame to process + * 要处理的视频帧 + * @return The texture ID after processing + * 处理后的纹理 ID + */ + private fun processBeautyTexture(videoFrame: VideoFrame): Int{ + if (Build.VERSION.SDK_INT >= 26) { + // For Android 8.0 and above, use single texture input. Internally, HardwareBuffer is used to convert to NV21 format. + return processBeautyTextureAPI26(videoFrame) + } + val texBufferHelper = textureBufferHelper ?: return -1 + val buffer = videoFrame.buffer as? TextureBuffer ?: return -1 + val nv21ByteArray = getNV21Buffer(videoFrame) ?: return -1 + val width = buffer.width + val height = buffer.height + + when(buffer.type){ + TextureBuffer.Type.OES -> { + if(currProcessSourceType != ProcessSourceType.TEXTURE_OES){ + LogUtils.i(TAG, "processBeautyAuto >> process source type change old=$currProcessSourceType, new=${ProcessSourceType.TEXTURE_OES}") + currProcessSourceType = ProcessSourceType.TEXTURE_OES + } + } + else -> { + if(currProcessSourceType != ProcessSourceType.TEXTURE_2D){ + LogUtils.i(TAG, "processBeautyAuto >> process source type change old=$currProcessSourceType, new=${ProcessSourceType.TEXTURE_2D}") + currProcessSourceType = ProcessSourceType.TEXTURE_2D + } + } + } + + val matrix = + RendererCommon.convertMatrixFromAndroidGraphicsMatrix(buffer.transformMatrix) + return texBufferHelper.invoke(Callable { + mayCreateBeautyProcess() + + TextureBufferPool.waitFenceSignal2(buffer.fenceObject, "BeautyProcess") + return@Callable beautyProcessor?.process( + InputInfo( + width = width, + height = height, + cameraOrientation = videoFrame.rotation, + isFrontCamera = videoFrame.sourceType == SourceType.kFrontCamera, + isMirror = (isFrontCamera && !captureMirror) || (!isFrontCamera && captureMirror), + timestamp = videoFrame.timestampNs, + bytes = nv21ByteArray, + bytesType = STCommonNative.ST_PIX_FMT_NV21, + textureId = buffer.textureId, + textureType = when (buffer.type) { + TextureBuffer.Type.OES -> GLES11Ext.GL_TEXTURE_EXTERNAL_OES + else -> GLES20.GL_TEXTURE_2D + }, + textureMatrix = matrix, + diffBetweenBytesAndTexture = 1 + ) + )?.textureId ?: -1 + }) + } + + /** + * Retrieves the NV21 buffer from the given video frame. + * 从给定的视频帧中获取 NV21 缓冲区。 + * + * @param videoFrame The video frame containing the buffer. + * 包含缓冲区的视频帧。 + * @return ByteArray? The NV21 buffer as a byte array, or null if it cannot be retrieved. + * NV21 缓冲区的字节数组,如果无法获取则返回 null。 + */ + private fun getNV21Buffer(videoFrame: VideoFrame) : ByteArray? { + val buffer = videoFrame.buffer + YuvConverter.setEnablePboOpt(true) + YuvConverter.setEnableConvertPerLog(true) + val i420Buffer = buffer as? I420Buffer ?: buffer.toI420() + val width = i420Buffer.width + val height = i420Buffer.height + val nv21Size = (width * height * 3.0f / 2.0f + 0.5f).toInt() + if (nv21ByteBuffer == null || nv21ByteBuffer?.capacity() != nv21Size) { + nv21ByteBuffer?.clear() + nv21ByteBuffer = ByteBuffer.allocateDirect(nv21Size) + return null + } + val nv21ByteArray = ByteArray(nv21Size) + + YuvHelper.I420ToNV12( + i420Buffer.dataY, i420Buffer.strideY, + i420Buffer.dataV, i420Buffer.strideV, + i420Buffer.dataU, i420Buffer.strideU, + nv21ByteBuffer, width, height + ) + nv21ByteBuffer?.position(0) + nv21ByteBuffer?.get(nv21ByteArray) + if (buffer !is I420Buffer) { + i420Buffer.release() + } + return nv21ByteArray + } + + // IVideoFrameObserver implements + + /** + * Callback when a video frame is captured. + * 采集视频帧时调用。 + * + * @param sourceType The source type of the video frame. + * 视频帧的源类型。 + * @param videoFrame The captured video frame. + * 采集的视频帧。 + * @return true if processing was successful, false otherwise. + * 如果处理成功则返回 true,否则返回 false。 + */ + override fun onCaptureVideoFrame(sourceType: Int, videoFrame: VideoFrame?): Boolean { + videoFrame ?: return false + return processBeauty(videoFrame) + } + + override fun onPreEncodeVideoFrame(sourceType: Int, videoFrame: VideoFrame?) : Boolean { + return true + } + + override fun onMediaPlayerVideoFrame(videoFrame: VideoFrame?, mediaPlayerId: Int) = false + + override fun onRenderVideoFrame( + channelId: String?, + uid: Int, + videoFrame: VideoFrame? + ) = false + + override fun getVideoFrameProcessMode() = IVideoFrameObserver.PROCESS_MODE_READ_WRITE + + override fun getVideoFormatPreference() = IVideoFrameObserver.VIDEO_PIXEL_DEFAULT + + override fun getRotationApplied() = false + + /** + * Checks if mirroring is applied. + * 检查是否应用了镜像。 + * + * @return true if mirroring is applied, false otherwise. + * 如果应用了镜像则返回 true,否则返回 false。 + */ + override fun getMirrorApplied() = captureMirror && !enable + + override fun getObservedFramePosition() = IVideoFrameObserver.POSITION_POST_CAPTURER + +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt new file mode 100644 index 000000000..2d5ce59e9 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt @@ -0,0 +1,156 @@ +package io.agora.beautyapi.sensetime.utils + +import io.agora.rtc2.RtcEngine +import org.json.JSONObject +import java.util.concurrent.Executors +import java.lang.ref.WeakReference + +enum class APIType(val value: Int) { + KTV(1), // Karaoke + CALL(2), // Call/Co-hosting + BEAUTY(3), // Beauty + VIDEO_LOADER(4), // Instant Loading + PK(5), // Team Battle + VIRTUAL_SPACE(6), // Virtual Space + SCREEN_SPACE(7), // Screen Sharing + AUDIO_SCENARIO(8) // Audio +} + +enum class ApiEventType(val value: Int) { + API(0), + COST(1), + CUSTOM(2) +} + +object ApiEventKey { + const val TYPE = "type" + const val DESC = "desc" + const val API_VALUE = "apiValue" + const val TIMESTAMP = "ts" + const val EXT = "ext" +} + +object ApiCostEvent { + const val CHANNEL_USAGE = "channelUsage" // Channel usage duration + const val FIRST_FRAME_ACTUAL = "firstFrameActual" // Actual first frame duration + const val FIRST_FRAME_PERCEIVED = "firstFramePerceived" // Perceived first frame duration +} + +class APIReporter( + private val type: APIType, + private val version: String, + rtcEngine: RtcEngine +) { + private val tag = "APIReporter" + private val messageId = "agora:scenarioAPI" + private val durationEventStartMap = HashMap() + private val category = "${type.value}_Android_$version" + private val executorService = Executors.newSingleThreadExecutor() + private val rtcEngineRef = WeakReference(rtcEngine) + + init { + configParameters() + } + + // Report regular scenario API + fun reportFuncEvent(name: String, value: Map, ext: Map) { + executorService.submit { + rtcEngineRef.get()?.let { + val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.API.value, ApiEventKey.DESC to name) + val labelMap = mapOf( + ApiEventKey.API_VALUE to value, + ApiEventKey.TIMESTAMP to getCurrentTs(), + ApiEventKey.EXT to ext + ) + val event = convertToJSONString(eventMap) ?: "" + val label = convertToJSONString(labelMap) ?: "" + it.sendCustomReportMessage(messageId, category, event, label, 0) + } + } + } + + fun startDurationEvent(name: String) { + durationEventStartMap[name] = getCurrentTs() + } + + fun endDurationEvent(name: String, ext: Map) { + val beginTs = durationEventStartMap[name] ?: return + durationEventStartMap.remove(name) + val ts = getCurrentTs() + val cost = (ts - beginTs).toInt() + + innerReportCostEvent(ts, name, cost, ext) + } + + // Report time-consuming event point information + fun reportCostEvent(name: String, cost: Int, ext: Map) { + durationEventStartMap.remove(name) + innerReportCostEvent( + ts = getCurrentTs(), + name = name, + cost = cost, + ext = ext + ) + } + + // Report custom information + fun reportCustomEvent(name: String, ext: Map) { + executorService.submit { + rtcEngineRef.get()?.let { + val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.CUSTOM.value, ApiEventKey.DESC to name) + val labelMap = mapOf(ApiEventKey.TIMESTAMP to getCurrentTs(), ApiEventKey.EXT to ext) + val event = convertToJSONString(eventMap) ?: "" + val label = convertToJSONString(labelMap) ?: "" + it.sendCustomReportMessage(messageId, category, event, label, 0) + } + } + } + + private fun writeLog(content: String, level: Int) { + rtcEngineRef.get()?.writeLog(level, content) + } + + fun cleanCache() { + durationEventStartMap.clear() + } + + // ---------------------- private ---------------------- + + private fun configParameters() { + executorService.submit { + rtcEngineRef.get()?.let { + // it.setParameters("{\"rtc.qos_for_test_purpose\": true}") // Used for test environment + // Data reporting + it.setParameters("{\"rtc.direct_send_custom_event\": true}") + // Log writing + it.setParameters("{\"rtc.log_external_input\": true}") + } + } + } + + private fun getCurrentTs(): Long { + return System.currentTimeMillis() + } + + private fun innerReportCostEvent(ts: Long, name: String, cost: Int, ext: Map) { + executorService.submit { + rtcEngineRef.get()?.let { +// writeLog("reportCostEvent: $name cost: $cost ms", Constants.LOG_LEVEL_INFO) + val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.COST.value, ApiEventKey.DESC to name) + val labelMap = mapOf(ApiEventKey.TIMESTAMP to ts, ApiEventKey.EXT to ext) + val event = convertToJSONString(eventMap) ?: "" + val label = convertToJSONString(labelMap) ?: "" + it.sendCustomReportMessage(messageId, category, event, label, cost) + } + } + } + + private fun convertToJSONString(dictionary: Map): String? { + return try { + JSONObject(dictionary).toString() + } catch (e: Exception) { + LogUtils.e(tag, "convert to json fail: $e dictionary: $dictionary") + null + } + } +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/LogUtils.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/LogUtils.kt new file mode 100644 index 000000000..81c551e54 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/LogUtils.kt @@ -0,0 +1,56 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils + +import io.agora.base.internal.Logging + +object LogUtils { + private const val beautyType = "SenseTime" + + @JvmStatic + fun i(tag: String, content: String, vararg args: Any) { + val consoleMessage = "[BeautyAPI][$beautyType] : ${String.format(content, args)}" + Logging.log(Logging.Severity.LS_INFO, tag, consoleMessage) + } + + @JvmStatic + fun d(tag: String, content: String, vararg args: Any) { + val consoleMessage = "[BeautyAPI][$beautyType] : ${String.format(content, args)}" + Logging.d(tag, consoleMessage) + } + + @JvmStatic + fun w(tag: String, content: String, vararg args: Any){ + val consoleMessage = "[BeautyAPI][$beautyType] : ${String.format(content, args)}" + Logging.w(tag, consoleMessage) + } + + @JvmStatic + fun e(tag: String, content: String, vararg args: Any){ + val consoleMessage = "[BeautyAPI][$beautyType] : ${String.format(content, args)}" + Logging.e(tag, consoleMessage) + } + +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/StatsHelper.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/StatsHelper.kt new file mode 100644 index 000000000..7391003ae --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/StatsHelper.kt @@ -0,0 +1,80 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils + +import android.os.Handler +import android.os.Looper +import io.agora.beautyapi.sensetime.BeautyStats +import kotlin.math.max +import kotlin.math.min + +class StatsHelper( + private val statsDuration: Long, + private val onStatsChanged: (BeautyStats) -> Unit +) { + + private val mMainHandler = Handler(Looper.getMainLooper()) + private var mStartTime = 0L + private var mCostList = mutableListOf() + private var mCostMax = 0L + private var mCostMin = Long.MAX_VALUE + + fun once(cost: Long) { + val curr = System.currentTimeMillis() + if (mStartTime == 0L) { + mStartTime = curr + } else if (curr - mStartTime >= statsDuration) { + mStartTime = curr + var total = 0L + mCostList.forEach { + total += it + } + val average = total / mCostList.size + val costMin = mCostMin + val costMax = mCostMax + mMainHandler.post { + onStatsChanged.invoke(BeautyStats(costMin, costMax, average)) + } + + mCostList.clear() + mCostMax = 0L + mCostMin = Long.MAX_VALUE + } + + mCostList.add(cost) + mCostMax = max(mCostMax, cost) + mCostMin = min(mCostMin, cost) + } + + fun reset() { + mMainHandler.removeCallbacksAndMessages(null) + mStartTime = 0 + mCostList.clear() + mCostMax = 0L + mCostMin = Long.MAX_VALUE + } + + +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLCopyHelper.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLCopyHelper.java new file mode 100644 index 000000000..f939bd62e --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLCopyHelper.java @@ -0,0 +1,60 @@ +package io.agora.beautyapi.sensetime.utils.egl; + +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLES30; + +public class GLCopyHelper { + private final int bufferCount; + + public GLCopyHelper(){ + this(1); + } + + public GLCopyHelper(int bufferCount){ + this.bufferCount = bufferCount; + } + + private int[] mDstFrameBuffer; + private int[] mSrcFrameBuffer; + + public void copy2DTextureToOesTexture( + int srcTexture, + int dstTexture, + int width, int height, + int index){ + if(mDstFrameBuffer == null){ + mDstFrameBuffer = new int[bufferCount]; + GLES20.glGenFramebuffers(bufferCount, mDstFrameBuffer, 0); + } + + if(mSrcFrameBuffer == null){ + mSrcFrameBuffer = new int[bufferCount]; + GLES20.glGenFramebuffers(bufferCount, mSrcFrameBuffer, 0); + } + + GLES30.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER, mSrcFrameBuffer[index]); + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, srcTexture); + GLES30.glFramebufferTexture2D(GLES30.GL_READ_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, srcTexture, 0); + GLES30.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER, mDstFrameBuffer[index]); + GLES30.glFramebufferTexture2D(GLES30.GL_DRAW_FRAMEBUFFER, + GLES30.GL_COLOR_ATTACHMENT0, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, dstTexture, 0); + GLES30.glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GLES30.GL_COLOR_BUFFER_BIT, GLES30.GL_LINEAR); + GLES30.glBindFramebuffer(GLES30.GL_DRAW_FRAMEBUFFER, 0); + GLES30.glBindFramebuffer(GLES30.GL_READ_FRAMEBUFFER, 0); + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0); + GLES30.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); + } + + public void release(){ + if(mDstFrameBuffer != null){ + GLES20.glDeleteFramebuffers(mDstFrameBuffer.length, mDstFrameBuffer, 0); + mDstFrameBuffer = null; + } + + if(mSrcFrameBuffer != null){ + GLES20.glDeleteFramebuffers(mSrcFrameBuffer.length, mSrcFrameBuffer, 0); + mSrcFrameBuffer = null; + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java new file mode 100644 index 000000000..4d8cf377f --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java @@ -0,0 +1,204 @@ +package io.agora.beautyapi.sensetime.utils.egl; + +import android.graphics.Matrix; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; + +import io.agora.base.internal.video.EglBaseLock; +import io.agora.base.internal.video.GlRectDrawer; +import io.agora.base.internal.video.RendererCommon; + +public class GLFrameBuffer { + + private int mFramebufferId = -1; + private int mTextureId = -1; + private int mWidth, mHeight, mRotation; + private boolean isFlipV, isFlipH, isTextureInner, isTextureChanged, isSizeChanged; + + private RendererCommon.GlDrawer drawer; + + private float[] mTexMatrix = GlUtil.IDENTITY_MATRIX; + + public GLFrameBuffer() { + + } + + public boolean setSize(int width, int height) { + if (mWidth != width || mHeight != height) { + mWidth = width; + mHeight = height; + isSizeChanged = true; + return true; + } + return false; + } + + public void setRotation(int rotation) { + if (mRotation != rotation) { + mRotation = rotation; + } + } + + public void setFlipV(boolean flipV) { + if (isFlipV != flipV) { + isFlipV = flipV; + } + } + + public void setFlipH(boolean flipH) { + if (isFlipH != flipH) { + isFlipH = flipH; + } + } + + public void setTextureId(int textureId){ + if(mTextureId != textureId){ + deleteTexture(); + mTextureId = textureId; + isTextureChanged = true; + } + } + + public int getTextureId(){ + return mTextureId; + } + + public void setTexMatrix(float[] matrix) { + if (matrix != null) { + mTexMatrix = matrix; + } else { + mTexMatrix = GlUtil.IDENTITY_MATRIX; + } + } + + public void resetTransform(){ + mTexMatrix = GlUtil.IDENTITY_MATRIX; + isFlipH = isFlipV = false; + mRotation = 0; + } + + public int process(int textureId, int textureType) { + if (mWidth <= 0 && mHeight <= 0) { + throw new RuntimeException("setSize firstly!"); + } + + if(mTextureId == -1){ + mTextureId = createTexture(mWidth, mHeight); + bindFramebuffer(mTextureId); + isTextureInner = true; + }else if(isTextureInner && isSizeChanged){ + GLES20.glDeleteTextures(1, new int[]{mTextureId}, 0); + mTextureId = createTexture(mWidth, mHeight); + bindFramebuffer(mTextureId); + }else if(isTextureChanged){ + bindFramebuffer(mTextureId); + } + isTextureChanged = false; + isSizeChanged = false; + + if(drawer == null){ + drawer = new GlRectDrawer(); + } + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferId); + GlUtil.checkGlError("glBindFramebuffer"); + + Matrix transform = RendererCommon.convertMatrixToAndroidGraphicsMatrix(mTexMatrix); + transform.preTranslate(0.5f, 0.5f); + transform.preRotate(mRotation, 0.f, 0.f); + transform.preScale( + isFlipH ? -1.f: 1.f, + isFlipV ? -1.f: 1.f + ); + transform.preTranslate(-0.5f, -0.5f); + float[] matrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(transform); + + EglBaseLock.lock(() -> { + if (textureType == GLES11Ext.GL_TEXTURE_EXTERNAL_OES) { + drawer.drawOes(textureId, 0, matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); + } else { + drawer.drawRgb(textureId, 0, matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); + } + }); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); + GLES20.glFinish(); + + return mTextureId; + } + + public void release(){ + deleteTexture(); + deleteFramebuffer(); + + if(drawer != null){ + drawer.release(); + drawer = null; + } + } + + + private void deleteFramebuffer() { + if (mFramebufferId != -1) { + GLES20.glDeleteFramebuffers(1, new int[]{mFramebufferId}, 0); + mFramebufferId = -1; + } + } + + public int createTexture(int width, int height){ + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + GlUtil.checkGlError("glGenTextures"); + int textureId = textures[0]; + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, GLES20.GL_NONE); + + return textureId; + } + + public void resizeTexture(int textureId, int width, int height) { + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, GLES20.GL_NONE); + } + + private void deleteTexture() { + if (isTextureInner && mTextureId != -1) { + GLES20.glDeleteTextures(1, new int[]{mTextureId}, 0); + } + isTextureInner = false; + mTextureId = -1; + } + + private void bindFramebuffer(int textureId) { + if(mFramebufferId == -1){ + int[] framebuffers = new int[1]; + GLES20.glGenFramebuffers(1, framebuffers, 0); + GlUtil.checkGlError("glGenFramebuffers"); + mFramebufferId = framebuffers[0]; + } + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFramebufferId); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, + GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, + textureId, 0); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, GLES20.GL_NONE); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE); + } + +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTestUtils.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTestUtils.java new file mode 100644 index 000000000..67f65cad1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTestUtils.java @@ -0,0 +1,150 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils.egl; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ImageFormat; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import io.agora.beautyapi.sensetime.utils.LogUtils; + +public class GLTestUtils { + private static final String TAG = "GLUtils"; + + public static Bitmap getTexture2DImage(int textureID, int width, int height) { + try { + int[] oldFboId = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, IntBuffer.wrap(oldFboId)); + + int[] framebuffers = new int[1]; + GLES20.glGenFramebuffers(1, framebuffers, 0); + int framebufferId = framebuffers[0]; + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebufferId); + + int[] renderbuffers = new int[1]; + GLES20.glGenRenderbuffers(1, renderbuffers, 0); + int renderId = renderbuffers[0]; + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, renderId); + GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height); + + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureID, 0); + GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, renderId); + if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) { + LogUtils.e(TAG, "Framebuffer error"); + } + + ByteBuffer rgbaBuf = ByteBuffer.allocateDirect(width * height * 4); + rgbaBuf.position(0); + GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaBuf); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(rgbaBuf); + + GLES20.glDeleteRenderbuffers(1, IntBuffer.wrap(framebuffers)); + GLES20.glDeleteFramebuffers(1, IntBuffer.allocate(framebufferId)); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, oldFboId[0]); + + return bitmap; + } catch (Exception e) { + LogUtils.e(TAG, "", e); + } + return null; + } + + public static Bitmap getTextureOESImage(int textureID, int width, int height) { + try { + int[] oldFboId = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, IntBuffer.wrap(oldFboId)); + + int[] framebuffers = new int[1]; + GLES20.glGenFramebuffers(1, framebuffers, 0); + int framebufferId = framebuffers[0]; + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebufferId); + + int[] renderbuffers = new int[1]; + GLES20.glGenRenderbuffers(1, renderbuffers, 0); + int renderId = renderbuffers[0]; + GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, renderId); + GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height); + + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureID, 0); + GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER, renderId); + if (GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER) != GLES20.GL_FRAMEBUFFER_COMPLETE) { + LogUtils.e(TAG, "Framebuffer error"); + } + + ByteBuffer rgbaBuf = ByteBuffer.allocateDirect(width * height * 4); + rgbaBuf.position(0); + GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaBuf); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(rgbaBuf); + + GLES20.glDeleteRenderbuffers(1, IntBuffer.wrap(framebuffers)); + GLES20.glDeleteFramebuffers(1, IntBuffer.allocate(framebufferId)); + + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, oldFboId[0]); + + return bitmap; + } catch (Exception e) { + LogUtils.e(TAG, e.toString()); + } + return null; + } + + public static Bitmap nv21ToBitmap(byte[] nv21, int width, int height) { + Bitmap bitmap = null; + try { + YuvImage image = new YuvImage(nv21, ImageFormat.NV21, width, height, null); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + image.compressToJpeg(new Rect(0, 0, width, height), 80, stream); + bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + return bitmap; + } + + private static Bitmap readBitmap(int width, int height){ + ByteBuffer rgbaBuf = ByteBuffer.allocateDirect(width * height * 4); + rgbaBuf.position(0); + GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, rgbaBuf); + + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(rgbaBuf); + return bitmap; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTextureBufferQueue.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTextureBufferQueue.kt new file mode 100644 index 000000000..d0cf57ef7 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLTextureBufferQueue.kt @@ -0,0 +1,164 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils.egl + +import android.opengl.GLES20 +import android.util.Size +import io.agora.beautyapi.sensetime.utils.LogUtils +import java.util.concurrent.ConcurrentLinkedQueue + +class GLTextureBufferQueue( + private val glFrameBuffer: GLFrameBuffer, + private val cacheCount: Int = 6 +) { + private val TAG = "GLTextureBufferQueue" + + private var cacheIndex = 0 + private val cacheTextureOuts = arrayOfNulls(cacheCount) + private val textureIdQueue = ConcurrentLinkedQueue() + + + fun enqueue(iN: TextureIn): Int { + var size = textureIdQueue.size + if (size < cacheCount) { + var out = cacheTextureOuts[cacheIndex] + val outSize = when (iN.rotation) { + 90, 270 -> Size(iN.height, iN.width) + else -> Size(iN.width, iN.height) + } + + if (out == null) { + val textureId = glFrameBuffer.createTexture(outSize.width, outSize.height) + out = TextureOut( + 0, + textureId, + GLES20.GL_TEXTURE_2D, + outSize.width, + outSize.height, + iN.isFrontCamera + ) + cacheTextureOuts[cacheIndex] = out + } else if (out.width != outSize.width || out.height != outSize.height) { + glFrameBuffer.resizeTexture(out.textureId, outSize.width, outSize.height) + out = TextureOut( + 0, + out.textureId, + out.textureType, + outSize.width, + outSize.height, + iN.isFrontCamera + ) + cacheTextureOuts[cacheIndex] = out + } else if(out.isFrontCamera != iN.isFrontCamera){ + out = TextureOut( + 0, + out.textureId, + out.textureType, + out.width, + out.height, + iN.isFrontCamera + ) + cacheTextureOuts[cacheIndex] = out + } + + var flipV = true + var flipH = false + glFrameBuffer.textureId = out.textureId + glFrameBuffer.setSize(out.width, out.height) + glFrameBuffer.resetTransform() + glFrameBuffer.setRotation(iN.rotation) + if (iN.transform != null) { + glFrameBuffer.setTexMatrix(iN.transform) + flipH = iN.isFrontCamera + } else { + flipH = !iN.isFrontCamera + } + if(iN.isMirror){ + flipH = !flipH + } + if(iN.rotation == 0 || iN.rotation == 180){ + flipV = !flipV + flipH = !flipH + } + glFrameBuffer.setFlipH(flipH) + glFrameBuffer.setFlipV(flipV) + glFrameBuffer.process(iN.textureId, iN.textureType) + GLES20.glFinish() + out.index = cacheIndex + textureIdQueue.offer(out) + cacheIndex = (cacheIndex + 1) % cacheCount + size++ + + } else { + LogUtils.e(TAG, "TextureIdQueue is full!!") + } + + return size + } + + fun dequeue(): TextureOut? { + val size = textureIdQueue.size + val poll = textureIdQueue.poll() + return poll + } + + fun size() = textureIdQueue.size + + fun reset() { + cacheIndex = 0 + textureIdQueue.clear() + } + + fun release() { + cacheIndex = 0 + cacheTextureOuts.forEachIndexed { index, textureOut -> + if (textureOut != null) { + GLES20.glDeleteTextures(1, intArrayOf(textureOut.textureId), 0) + cacheTextureOuts[index] = null + } + } + textureIdQueue.clear() + } + + data class TextureIn( + val textureId: Int, + val textureType: Int, + val width: Int, + val height: Int, + val rotation: Int, + val isFrontCamera: Boolean, + val isMirror: Boolean, + val transform: FloatArray? + ) + + data class TextureOut( + var index: Int = 0, + val textureId: Int, + val textureType: Int, + val width: Int, + val height: Int, + val isFrontCamera: Boolean + ) +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GlUtil.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GlUtil.java new file mode 100644 index 000000000..0f98657a8 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GlUtil.java @@ -0,0 +1,255 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils.egl; + +import android.content.Context; +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; + +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.Objects; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLContext; + +import io.agora.beautyapi.sensetime.utils.LogUtils; + +public class GlUtil { + private static final String TAG = "GlUtil"; + /** Identity matrix for general use. Don't modify or life will get weird. */ + + public static final int NO_TEXTURE = -1; + + private static final int SIZEOF_FLOAT = 4; + + public static final float[] IDENTITY_MATRIX = new float[16]; + static { + Matrix.setIdentityM(IDENTITY_MATRIX, 0); + } + + private GlUtil() { // do not instantiate + } + + public static int createProgram(Context applicationContext, @RawRes int vertexSourceRawId, + @RawRes int fragmentSourceRawId) { + + String vertexSource = readTextFromRawResource(applicationContext, vertexSourceRawId); + String fragmentSource = readTextFromRawResource(applicationContext, fragmentSourceRawId); + + return createProgram(vertexSource, fragmentSource); + } + + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + int program = GLES20.glCreateProgram(); + checkGlError("glCreateProgram"); + if (program == 0) { + LogUtils.e(TAG, "Could not create program"); + } + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + LogUtils.e(TAG, "Could not link program: "); + LogUtils.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + program = 0; + } + return program; + } + + public static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + checkGlError("glCreateShader type=" + shaderType); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + LogUtils.e(TAG, "Could not compile shader " + shaderType + ":"); + LogUtils.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + return shader; + } + + public static int createTexture(int textureTarget, @Nullable Bitmap bitmap, int minFilter, + int magFilter, int wrapS, int wrapT) { + int[] textureHandle = new int[1]; + + GLES20.glGenTextures(1, textureHandle, 0); + GlUtil.checkGlError("glGenTextures"); + GLES20.glBindTexture(textureTarget, textureHandle[0]); + GlUtil.checkGlError("glBindTexture " + textureHandle[0]); + GLES20.glTexParameterf(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, minFilter); + GLES20.glTexParameterf(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, magFilter); // Linear interpolation + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, wrapS); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, wrapT); + + if (bitmap != null) { + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); + } + + GlUtil.checkGlError("glTexParameter"); + return textureHandle[0]; + } + + public static int createTexture(int textureTarget) { + return createTexture(textureTarget, null, GLES20.GL_LINEAR, GLES20.GL_LINEAR, + GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE); + } + + public static int createTexture(int textureTarget, Bitmap bitmap) { + return createTexture(textureTarget, bitmap, GLES20.GL_LINEAR, GLES20.GL_LINEAR, + GLES20.GL_CLAMP_TO_EDGE, GLES20.GL_CLAMP_TO_EDGE); + } + + public static void initEffectTexture(int width, int height, int[] textureId, int type) { + int len = textureId.length; + if (len > 0) { + GLES20.glGenTextures(len, textureId, 0); + } + for (int i = 0; i < len; i++) { + GLES20.glBindTexture(type, textureId[i]); + GLES20.glTexParameterf(type, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(type, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(type, + GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(type, + GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexImage2D(type, 0, GLES20.GL_RGBA, width, height, 0, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null); + } + } + /** + * Checks to see if a GLES error has been raised. + */ + public static void checkGlError(String op) { + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + String msg = op + ": glError 0x" + Integer.toHexString(error); + LogUtils.e(TAG, msg); + throw new RuntimeException(msg); + } + } + + /** + * Allocates a direct float buffer, and populates it with the float array data. + */ + public static FloatBuffer createFloatBuffer(float[] coords) { + // Allocate a direct ByteBuffer, using 4 bytes per float, and copy coords into it. + ByteBuffer bb = ByteBuffer.allocateDirect(coords.length * SIZEOF_FLOAT); + bb.order(ByteOrder.nativeOrder()); + FloatBuffer fb = bb.asFloatBuffer(); + fb.put(coords); + fb.position(0); + return fb; + } + + public static String readTextFromRawResource(final Context applicationContext, + @RawRes final int resourceId) { + final InputStream inputStream = + applicationContext.getResources().openRawResource(resourceId); + final InputStreamReader inputStreamReader = new InputStreamReader(inputStream); + final BufferedReader bufferedReader = new BufferedReader(inputStreamReader); + String nextLine; + final StringBuilder body = new StringBuilder(); + try { + while ((nextLine = bufferedReader.readLine()) != null) { + body.append(nextLine); + body.append('\n'); + } + } catch (IOException e) { + return null; + } + + return body.toString(); + } + + public static float[] createTransformMatrix(int rotation, boolean flipH, boolean flipV){ + float[] renderMVPMatrix = new float[16]; + float[] tmp = new float[16]; + Matrix.setIdentityM(tmp, 0); + + boolean _flipH = flipH; + boolean _flipV = flipV; + if(rotation % 180 != 0){ + _flipH = flipV; + _flipV = flipH; + } + + if (_flipH) { + Matrix.rotateM(tmp, 0, tmp, 0, 180, 0, 1f, 0); + } + if (_flipV) { + Matrix.rotateM(tmp, 0, tmp, 0, 180, 1f, 0f, 0); + } + + float _rotation = rotation; + if (_rotation != 0) { + if(_flipH != _flipV){ + _rotation *= -1; + } + Matrix.rotateM(tmp, 0, tmp, 0, _rotation, 0, 0, 1); + } + + Matrix.setIdentityM(renderMVPMatrix, 0); + Matrix.multiplyMM(renderMVPMatrix, 0, tmp, 0, renderMVPMatrix, 0); + return renderMVPMatrix; + } + + public static EGLContext getCurrGLContext(){ + EGL10 egl = (EGL10)EGLContext.getEGL(); + if (egl != null && !Objects.equals(egl.eglGetCurrentContext(), EGL10.EGL_NO_CONTEXT)) { + return egl.eglGetCurrentContext(); + } + return null; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/Accelerometer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/Accelerometer.java new file mode 100644 index 000000000..3bf81e68f --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/Accelerometer.java @@ -0,0 +1,154 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils.processor; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +public class Accelerometer { + /** + * + * @author MatrixCV + * + * The CLOCKWISE_ANGLE represents the rotation angle of the phone, with Deg0 defined as shown in the diagram below. + * ___________________ + * | +--------------+ | + * | | | | + * | | | | + * | | | O| + * | | | | + * | |______________| | + * --------------------- + * After rotating clockwise, the angle becomes Deg90, which represents the phone’s portrait orientation with the top facing up, as shown in the diagram below. + * ___________ + * | | + * |+---------+| + * || || + * || || + * || || + * || || + * || || + * |+---------+| + * |_____O_____| + */ + public enum CLOCKWISE_ANGLE { + Deg0(0), Deg90(1), Deg180(2), Deg270(3); + private int value; + private CLOCKWISE_ANGLE(int value){ + this.value = value; + } + public int getValue() { + return value; + } + } + + private SensorManager sensorManager = null; + + private boolean hasStarted = false; + + private CLOCKWISE_ANGLE rotation; + + private SensorEvent sensorEvent; + + /** + * @param ctx + * Initialize the sensor using the Activity + */ + public Accelerometer(Context ctx) { + sensorManager = (SensorManager) ctx + .getSystemService(Context.SENSOR_SERVICE); + rotation = CLOCKWISE_ANGLE.Deg90; + } + + /** + * Start listening to the sensor + */ + public void start() { + if (hasStarted) return; + hasStarted = true; + rotation = CLOCKWISE_ANGLE.Deg90; + sensorManager.registerListener(accListener, + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), + SensorManager.SENSOR_DELAY_NORMAL); + } + + /** + * Stop listening to the sensor + */ + public void stop() { + if (!hasStarted) return; + hasStarted = false; + sensorManager.unregisterListener(accListener); + } + + /** + * @return + * Returns the current orientation of the phone + */ + public int getDirection() { + return rotation.getValue(); + } + + public SensorEvent getSensorEvent() { + return sensorEvent; + } + + /** + * The logic between the sensor and the phone's orientation + */ + private SensorEventListener accListener = new SensorEventListener() { + + @Override + public void onAccuracyChanged(Sensor arg0, int arg1) { + } + + @Override + public void onSensorChanged(SensorEvent arg0) { + if (arg0.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { + float x = arg0.values[0]; + float y = arg0.values[1]; + float z = arg0.values[2]; + if (Math.abs(x)>3 || Math.abs(y)>3) { + if (Math.abs(x)> Math.abs(y)) { + if (x > 0) { + rotation = CLOCKWISE_ANGLE.Deg0; + } else { + rotation = CLOCKWISE_ANGLE.Deg180; + } + } else { + if (y > 0) { + rotation = CLOCKWISE_ANGLE.Deg90; + } else { + rotation = CLOCKWISE_ANGLE.Deg270; + } + } + } + } + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/BeautyProcessor.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/BeautyProcessor.kt new file mode 100644 index 000000000..5f5adee7a --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/BeautyProcessor.kt @@ -0,0 +1,499 @@ +package io.agora.beautyapi.sensetime.utils.processor + +import android.annotation.TargetApi +import android.content.Context +import android.opengl.GLES20 +import android.os.Build +import com.softsugar.hardwarebuffer.STMobileHardwareBufferNative +import com.softsugar.stmobile.STCommonNative +import com.softsugar.stmobile.STMobileColorConvertNative +import com.softsugar.stmobile.STMobileEffectNative +import com.softsugar.stmobile.STMobileHumanActionNative +import com.softsugar.stmobile.model.STEffectCustomParam +import com.softsugar.stmobile.model.STEffectRenderInParam +import com.softsugar.stmobile.model.STEffectRenderOutParam +import com.softsugar.stmobile.model.STEffectTexture +import com.softsugar.stmobile.model.STQuaternion +import com.softsugar.stmobile.params.STEffectParam +import com.softsugar.stmobile.sticker_module_types.STCustomEvent +import io.agora.beautyapi.sensetime.utils.LogUtils +import io.agora.beautyapi.sensetime.utils.egl.GLCopyHelper +import io.agora.beautyapi.sensetime.utils.egl.GLFrameBuffer +import io.agora.beautyapi.sensetime.utils.egl.GLTextureBufferQueue +import io.agora.beautyapi.sensetime.utils.processor.Accelerometer.CLOCKWISE_ANGLE + +class BeautyProcessor : IBeautyProcessor { + private val TAG = this::class.java.simpleName + + private val glCopyHelper = GLCopyHelper() + private val glFrameBuffer = GLFrameBuffer() + private val glTextureBufferQueue = GLTextureBufferQueue(glFrameBuffer) + + private var mProcessWidth = 0 + private var mProcessHeight = 0 + private var processInTextureId = -1 + private var beautyOutTextureId = -1 + private var finalOutTextureId = -1 + + private lateinit var mSTMobileEffectNative: STMobileEffectNative + private lateinit var mFaceDetector: FaceDetector + private var mSTMobileColorConvertNative: STMobileColorConvertNative? = null + private var mSTMobileHardwareBufferNative: STMobileHardwareBufferNative? = null + + private var mCustomEvent = 0 + private var mInputWidth = 0 + private var mInputHeight = 0 + private var mInputOrientation = 0 + private var isLastFrontCamera = false + private var skipFrame = 0 + private var processMode = ProcessMode.DOUBLE_INPUT + + @Volatile + private var isReleased = false + + enum class ProcessMode { + DOUBLE_INPUT, + SINGLE_BYTES_INPUT, + SINGLE_TEXTURE_INPUT + } + + override fun initialize( + effectNative: STMobileEffectNative, + humanActionNative: STMobileHumanActionNative + ) { + this.mSTMobileEffectNative = effectNative + mFaceDetector = FaceDetector(humanActionNative, effectNative) + } + + override fun release() { + isReleased = true + mFaceDetector.release() + if (processInTextureId != -1) { + GLES20.glDeleteTextures(1, intArrayOf(processInTextureId), 0) + processInTextureId = -1 + } + if (beautyOutTextureId != -1) { + GLES20.glDeleteTextures(1, intArrayOf(beautyOutTextureId), 0) + beautyOutTextureId = -1 + } + if (finalOutTextureId != -1) { + GLES20.glDeleteTextures(1, intArrayOf(finalOutTextureId), 0) + finalOutTextureId = -1 + } + if (mSTMobileColorConvertNative != null) { + mSTMobileColorConvertNative?.destroyInstance() + mSTMobileColorConvertNative = null + } + glFrameBuffer.release() + glCopyHelper.release() + glTextureBufferQueue.release() + mSTMobileHardwareBufferNative?.release() + mSTMobileHardwareBufferNative = null + } + + override fun enableSensor(context: Context, enable: Boolean) { + mFaceDetector.enableSensor(context, enable) + } + + override fun triggerScreenTap(isDouble: Boolean) { + LogUtils.d( + TAG, + "changeCustomEvent() called:" + mSTMobileEffectNative.customEventNeeded + ) + mCustomEvent = mSTMobileEffectNative.customEventNeeded + mCustomEvent = if (isDouble) { + (mCustomEvent.toLong() and STCustomEvent.ST_CUSTOM_EVENT_SCREEN_TAP.inv()).toInt() + } else { + (mCustomEvent.toLong() and STCustomEvent.ST_CUSTOM_EVENT_SCREEN_DOUBLE_TAP.inv()).toInt() + } + } + + + override fun process(input: InputInfo): OutputInfo? { + if (isReleased) { + return null + } + return if (input.bytes != null && input.textureId != null) { + if(processMode != ProcessMode.DOUBLE_INPUT){ + processMode = ProcessMode.DOUBLE_INPUT + if (mInputWidth > 0 || mInputHeight > 0) { + skipFrame = 3 + } + } + processDoubleInput(input) + } else if (input.bytes != null) { + if(processMode != ProcessMode.SINGLE_BYTES_INPUT){ + processMode = ProcessMode.SINGLE_BYTES_INPUT + if (mInputWidth > 0 || mInputHeight > 0) { + skipFrame = 3 + } + } + processSingleBytesInput(input) + } else if (input.textureId != null && Build.VERSION.SDK_INT >= 26) { + if(processMode != ProcessMode.SINGLE_TEXTURE_INPUT){ + processMode = ProcessMode.SINGLE_TEXTURE_INPUT + if (mInputWidth > 0 || mInputHeight > 0) { + skipFrame = 3 + } + } + processSingleTextureInput(input) + } else { + throw RuntimeException("Single texture input is not supported when SDK_INT < 26!"); + } + } + + @TargetApi(26) + private fun processSingleTextureInput(input: InputInfo): OutputInfo? { + if (isReleased) { + return null + } + if (input.textureId == null) { + return null + } + val width = input.width + val height = input.height + + if(processInTextureId == -1){ + processInTextureId = glFrameBuffer.createTexture(width, height) + } + + if (mSTMobileHardwareBufferNative == null) { + mProcessWidth = width + mProcessHeight = height + glFrameBuffer.resizeTexture(processInTextureId, width, height) + mSTMobileHardwareBufferNative = STMobileHardwareBufferNative().apply { + init( + width, + height, + STMobileHardwareBufferNative.HARDWARE_BUFFER_FORMAT_RGBA, + STMobileHardwareBufferNative.HARDWARE_BUFFER_USAGE_DOWNLOAD + ) + } + } else if (mProcessWidth != width || mProcessHeight != height) { + mSTMobileHardwareBufferNative?.release() + mSTMobileHardwareBufferNative = null + glFrameBuffer.resizeTexture(processInTextureId, width, height) + return null + } + + glFrameBuffer.textureId = processInTextureId + glFrameBuffer.setSize(width, height) + glFrameBuffer.resetTransform() + glFrameBuffer.setTexMatrix(input.textureMatrix) + glFrameBuffer.setFlipV(true) + glFrameBuffer.process(input.textureId, input.textureType) + + val outBuffer = ByteArray(width * height * 4) + mSTMobileHardwareBufferNative?.let { + glCopyHelper.copy2DTextureToOesTexture( + processInTextureId, + it.textureId, + width, + height, + 0 + ) + it.downloadRgbaImage( + width, + height, + outBuffer + ) + } + + GLES20.glFinish() + + return processDoubleInput( + InputInfo( + outBuffer, + STCommonNative.ST_PIX_FMT_RGBA8888, + processInTextureId, + GLES20.GL_TEXTURE_2D, + null, + 1, + width, + height, + input.isFrontCamera, + input.isMirror, + input.cameraOrientation, + input.timestamp, + ) + ) + } + + private fun processSingleBytesInput(input: InputInfo): OutputInfo? { + if (isReleased) { + return null + } + if (input.bytes == null) { + return null + } + if (processInTextureId == -1) { + processInTextureId = glFrameBuffer.createTexture(input.width, input.height) + } + + if (mSTMobileColorConvertNative == null) { + mProcessWidth = input.width + mProcessHeight = input.height + glFrameBuffer.resizeTexture(processInTextureId, input.width, input.height) + mSTMobileColorConvertNative = STMobileColorConvertNative().apply { + createInstance() + setTextureSize(mProcessWidth, mProcessHeight) + } + } else if (mProcessWidth != input.width || mProcessHeight != input.height) { + mSTMobileColorConvertNative?.destroyInstance() + mSTMobileColorConvertNative = null + return null + } + // Upload NV21 buffer to texture + mSTMobileColorConvertNative?.nv21BufferToRgbaTexture( + input.width, + input.height, + 0, + false, + input.bytes, + processInTextureId + ) + return processDoubleInput( + InputInfo( + input.bytes, + input.bytesType, + processInTextureId, + GLES20.GL_TEXTURE_2D, + input.textureMatrix, + 0, + input.width, + input.height, + input.isFrontCamera, + input.isMirror, + input.cameraOrientation, + input.timestamp, + ) + ) + } + + private fun processDoubleInput(input: InputInfo): OutputInfo? { + if (isReleased) { + return null + } + if (input.bytes == null || input.textureId == null) { + return null + } + if (mInputWidth != input.width || mInputHeight != input.height || mInputOrientation != input.cameraOrientation || isLastFrontCamera != input.isFrontCamera) { + if(mInputWidth > 0 || mInputHeight > 0){ + skipFrame = 3 + } + mInputWidth = input.width + mInputHeight = input.height + mInputOrientation = input.cameraOrientation + isLastFrontCamera = input.isFrontCamera + reset() + return null + } + + + + val diff = glTextureBufferQueue.size() - mFaceDetector.size() + if(diff < input.diffBetweenBytesAndTexture){ + glTextureBufferQueue.enqueue( + GLTextureBufferQueue.TextureIn( + input.textureId, + input.textureType, + input.width, + input.height, + input.cameraOrientation, + input.isFrontCamera, + input.isMirror, + input.textureMatrix + ) + ) + return null + } else if(diff > input.diffBetweenBytesAndTexture){ + mFaceDetector.reset() + glTextureBufferQueue.reset() + return null + } else { + glTextureBufferQueue.enqueue( + GLTextureBufferQueue.TextureIn( + input.textureId, + input.textureType, + input.width, + input.height, + input.cameraOrientation, + input.isFrontCamera, + input.isMirror, + input.textureMatrix + ) + ) + } + + val detectorOut = mFaceDetector.dequeue() + var out : OutputInfo? = null + if (detectorOut != null) { + val textureOut = glTextureBufferQueue.dequeue() + if (textureOut != null) { + val outTextureId = effectApply( + textureOut.textureId, + detectorOut, + textureOut.width, + textureOut.height, + getCurrentOrientation(), + textureOut.isFrontCamera + ) + out = OutputInfo( + outTextureId, + GLES20.GL_TEXTURE_2D, + textureOut.width, + textureOut.height, + System.nanoTime() + ) + } else { + LogUtils.e(TAG, "The face detector out can not found its texture out!") + } + } + mFaceDetector.enqueue( + FaceDetector.DetectorIn( + input.bytes, + input.bytesType, + input.width, + input.height, + input.isFrontCamera, + input.isMirror, + input.cameraOrientation + ) + ) + + if(skipFrame > 0){ + skipFrame -- + return null + } + + return out + } + + private fun effectApply( + textureId: Int, + detectorInfo: FaceDetector.DetectorOut, + width: Int, + height: Int, + orientation: Int, + isFrontCamera: Boolean + ): Int { + var beautyOutTextureId = this@BeautyProcessor.beautyOutTextureId + if (beautyOutTextureId == -1) { + beautyOutTextureId = glFrameBuffer.createTexture(width, height) + this@BeautyProcessor.beautyOutTextureId = beautyOutTextureId + } + var finalOutTextureId = this@BeautyProcessor.finalOutTextureId + if (finalOutTextureId == -1) { + finalOutTextureId = glFrameBuffer.createTexture(width, height) + this@BeautyProcessor.finalOutTextureId = finalOutTextureId + } + + // Input texture + val stEffectTexture = + STEffectTexture( + textureId, + width, + height, + 0 + ) + // Output texture, needs to be initialized at a higher level + val stEffectTextureOut = + STEffectTexture(beautyOutTextureId, width, height, 0) + + // User-defined parameter settings + val event: Int = mCustomEvent + val customParam: STEffectCustomParam + val sensorEvent = mFaceDetector.getAccelerometer()?.sensorEvent + customParam = + if (sensorEvent?.values != null && sensorEvent.values.isNotEmpty()) { + STEffectCustomParam( + STQuaternion(sensorEvent.values), + isFrontCamera, + event + ) + } else { + STEffectCustomParam( + STQuaternion(0f, 0f, 0f, 1f), + isFrontCamera, + event + ) + } + + // Input parameters for the rendering interface + val sTEffectRenderInParam = STEffectRenderInParam( + detectorInfo.humanResult, + detectorInfo.animalResult, + orientation, + orientation, + false, + customParam, + stEffectTexture, + null + ) + // Output parameters for the rendering interface + val stEffectRenderOutParam = STEffectRenderOutParam( + stEffectTextureOut, + null, + null + ) + mSTMobileEffectNative.setParam( + STEffectParam.EFFECT_PARAM_USE_INPUT_TIMESTAMP, + 1.0f + ) + if (isReleased) { + return -1 + } + val ret = mSTMobileEffectNative.render( + sTEffectRenderInParam, + stEffectRenderOutParam, + false + ) + + if (event == mCustomEvent) { + mCustomEvent = 0 + } + + if (isReleased) { + return -1 + } + + var finalTextId = stEffectRenderOutParam.texture?.id ?: 0 + if(ret < 0){ + finalTextId = textureId + } + + glFrameBuffer.setSize(width, height) + glFrameBuffer.resetTransform() + glFrameBuffer.setFlipV(true) + glFrameBuffer.textureId = finalOutTextureId + glFrameBuffer.process( + finalTextId, + GLES20.GL_TEXTURE_2D + ) + GLES20.glFinish() + return finalOutTextureId + } + + override fun reset() { + mFaceDetector.reset() + glTextureBufferQueue.reset() + if (beautyOutTextureId != -1) { + GLES20.glDeleteTextures(1, intArrayOf(beautyOutTextureId), 0) + beautyOutTextureId = -1 + } + if (finalOutTextureId != -1) { + GLES20.glDeleteTextures(1, intArrayOf(finalOutTextureId), 0) + finalOutTextureId = -1 + } + } + + + private fun getCurrentOrientation(): Int { + val dir = mFaceDetector.getAccelerometer()?.direction ?: CLOCKWISE_ANGLE.Deg90.value + var orientation = dir - 1 + if (orientation < 0) { + orientation = dir xor 3 + } + return orientation + } +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/FaceDetector.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/FaceDetector.kt new file mode 100644 index 000000000..72215f598 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/FaceDetector.kt @@ -0,0 +1,221 @@ +/* + * MIT License + * + * Copyright (c) 2023 Agora Community + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.agora.beautyapi.sensetime.utils.processor + +import android.content.Context +import android.hardware.Camera +import android.util.Size +import com.softsugar.stmobile.STMobileEffectNative +import com.softsugar.stmobile.STMobileHumanActionNative +import com.softsugar.stmobile.model.STHumanAction +import com.softsugar.stmobile.model.STMobileAnimalResult +import com.softsugar.stmobile.params.STRotateType +import io.agora.beautyapi.sensetime.utils.LogUtils +import java.util.concurrent.Callable +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.Future + +class FaceDetector( + private val humanActionNative: STMobileHumanActionNative, + private val effectNative: STMobileEffectNative +) { + private val TAG = "FaceDetector" + + private val workerThread = Executors.newSingleThreadExecutor() + private var accelerometer: Accelerometer? = null + + private val cacheSize = 2 + private var cacheIndex = 0 + private val cacheFutureQueue = ConcurrentLinkedQueue>() + private var isDequeBegin = false + + fun enableSensor(context: Context, enable: Boolean) { + if (enable) { + if (accelerometer == null) { + accelerometer = + Accelerometer( + context + ) + accelerometer?.start() + } + } else { + accelerometer?.stop() + accelerometer = null + } + } + + fun getAccelerometer() = accelerometer + + fun reset() { + cacheIndex = 0 + isDequeBegin = false + var future = cacheFutureQueue.poll() + while (future != null){ + future.cancel(true) + future = cacheFutureQueue.poll() + } + } + + fun release(){ + reset() + accelerometer?.stop() + workerThread.shutdownNow() + } + + fun enqueue(iN: DetectorIn): Int { + val index = cacheIndex + val size = cacheFutureQueue.size + if (size <= cacheSize) { + cacheFutureQueue.offer( + workerThread.submit(Callable { + detectHuman(iN, index) + return@Callable index + }) + ) + cacheIndex = (cacheIndex + 1) % cacheSize + } else { + LogUtils.e(TAG, "Detector queue is full!!") + } + return size + } + + fun dequeue(): DetectorOut? { + val size = cacheFutureQueue.size + if(isDequeBegin || size >= cacheSize){ + isDequeBegin = true + val future = cacheFutureQueue.poll() + if(future != null){ + try { + val ret = future.get() + return DetectorOut( + humanActionNative.getNativeHumanActionResultCache(ret) + ) + }catch (e: Exception){ + LogUtils.e(TAG, "Detector dequeue timeout: $e") + } + } + } + return null + } + + fun size() = cacheFutureQueue.size + + private fun detectHuman(iN: DetectorIn, index: Int) { + val orientation: Int = + if (iN.orientation == 0) STRotateType.ST_CLOCKWISE_ROTATE_0 else getHumanActionOrientation( + iN.isFront, + iN.orientation + ) + val deviceOrientation: Int = + accelerometer?.direction ?: Accelerometer.CLOCKWISE_ANGLE.Deg90.value + val ret: Int = humanActionNative.nativeHumanActionDetectPtr( + iN.bytes, + iN.bytesType, + effectNative.humanActionDetectConfig, + orientation, + iN.width, + iN.height + ) + + // NV21 data is horizontal, and needs to be rotated according to the preview direction. + // For the front camera, mirroring is also required. + val rotatedSize = when (iN.orientation) { + 90, 270 -> Size(iN.height, iN.width) + else -> Size(iN.width, iN.height) + } + var mirror = iN.isFront + if(iN.isMirror){ + mirror = !mirror + } + if (iN.orientation == 0 || iN.orientation == 180) { + if (mirror) { + humanActionNative.nativeHumanActionMirrorPtr(rotatedSize.width) + } + if(iN.orientation == 180){ + humanActionNative.nativeHumanActionRotatePtr(rotatedSize.width, rotatedSize.height, STRotateType.ST_CLOCKWISE_ROTATE_180, false) + } + } else { + STHumanAction.nativeHumanActionRotateAndMirror( + humanActionNative, + humanActionNative.nativeHumanActionResultPtr, + rotatedSize.width, + rotatedSize.height, + if (mirror) Camera.CameraInfo.CAMERA_FACING_FRONT else Camera.CameraInfo.CAMERA_FACING_BACK, + iN.orientation, + deviceOrientation + ) + } + + + humanActionNative.updateNativeHumanActionCache(index) + } + + + /** + * Used for the humanActionDetect interface. Calculates the orientation of the face in the buffer + * based on the sensor direction for different device orientations. + * + * @return The orientation of the face in the buffer. + */ + private fun getHumanActionOrientation(frontCamera: Boolean, cameraRotation: Int): Int { + // Get the direction from the gravity sensor + var orientation: Int = accelerometer?.direction ?: Accelerometer.CLOCKWISE_ANGLE.Deg90.value + + // When using the rear camera, and the sensor direction is 0 or 2, + // the rear camera's orientation is opposite to that of the front camera + if (!frontCamera && orientation == STRotateType.ST_CLOCKWISE_ROTATE_0) { + orientation = STRotateType.ST_CLOCKWISE_ROTATE_180 + } else if (!frontCamera && orientation == STRotateType.ST_CLOCKWISE_ROTATE_180) { + orientation = STRotateType.ST_CLOCKWISE_ROTATE_0 + } + + // Note that the rotation definitions differ between the front and rear cameras, + // and different phones have different camera rotation definitions + if (cameraRotation == 270 && orientation and STRotateType.ST_CLOCKWISE_ROTATE_90 == STRotateType.ST_CLOCKWISE_ROTATE_90 + || cameraRotation == 90 && orientation and STRotateType.ST_CLOCKWISE_ROTATE_90 == STRotateType.ST_CLOCKWISE_ROTATE_0 + ) { + orientation = orientation xor STRotateType.ST_CLOCKWISE_ROTATE_180 + } + + return orientation + } + + + data class DetectorIn( + val bytes: ByteArray, + val bytesType: Int, + val width: Int, + val height: Int, + val isFront: Boolean, + val isMirror: Boolean, + val orientation: Int, + ) + + data class DetectorOut( + val humanResult: Long, + val animalResult: STMobileAnimalResult? = null + ) +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/IBeautyProcessor.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/IBeautyProcessor.kt new file mode 100644 index 000000000..37dfb3274 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/processor/IBeautyProcessor.kt @@ -0,0 +1,54 @@ +package io.agora.beautyapi.sensetime.utils.processor + +import android.content.Context +import android.opengl.GLES20 +import com.softsugar.stmobile.STCommonNative +import com.softsugar.stmobile.STMobileEffectNative +import com.softsugar.stmobile.STMobileHumanActionNative + +data class InputInfo( + val bytes: ByteArray? = null, + val bytesType: Int = STCommonNative.ST_PIX_FMT_NV21, + val textureId: Int? = null, + val textureType: Int = GLES20.GL_TEXTURE_2D, + val textureMatrix: FloatArray? = null, + val diffBetweenBytesAndTexture: Int = 0, + val width: Int, + val height: Int, + val isFrontCamera: Boolean, + val isMirror: Boolean, + val cameraOrientation: Int, + val timestamp: Long, + + ) + +class OutputInfo( + val textureId: Int = 0, + val textureType: Int = GLES20.GL_TEXTURE_2D, + val width: Int = 0, + val height: Int = 0, + val timestamp: Long = 0, + val errorCode: Int = 0, + val errorMessage: String = "", +) + +interface IBeautyProcessor { + + fun initialize( + effectNative: STMobileEffectNative, // Beauty effect processing handle + humanActionNative: STMobileHumanActionNative, // Face detection handle + ) + + fun process(input: InputInfo): OutputInfo? + + fun enableSensor(context: Context, enable: Boolean) + + fun triggerScreenTap(isDouble: Boolean) + + fun reset() + + fun release() + +} + +fun createBeautyProcessor(): IBeautyProcessor = BeautyProcessor() diff --git a/Android/APIExample/app/src/main/res/layout/fragment_beauty_scenetime.xml b/Android/APIExample/app/src/main/res/layout/fragment_beauty_scenetime.xml new file mode 100644 index 000000000..4a9fc6d71 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_beauty_scenetime.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/navigation/nav_graph.xml b/Android/APIExample/app/src/main/res/navigation/nav_graph.xml index 7befb025b..f364c7821 100755 --- a/Android/APIExample/app/src/main/res/navigation/nav_graph.xml +++ b/Android/APIExample/app/src/main/res/navigation/nav_graph.xml @@ -173,11 +173,21 @@ android:label="@string/item_third_party_beauty" tools:layout="@layout/fragment_third_party_beauty"> + + + + Date: Mon, 10 Aug 2026 14:41:32 +0800 Subject: [PATCH 25/49] feat(ios): add SenseTime beauty example --- .../APIExample.xcodeproj/project.pbxproj | 196 ++- .../Render/SenseRender/SenseBeautyRender.h | 28 + .../Render/SenseRender/SenseBeautyRender.m | 164 +++ .../Base.lproj/SenseBeautify.storyboard | 178 +++ .../SenseBeautify/Manager/EFGlobalSingleton.h | 27 + .../SenseBeautify/Manager/EFGlobalSingleton.m | 31 + .../SenseBeautify/Manager/EFMotionManager.h | 26 + .../SenseBeautify/Manager/EFMotionManager.m | 56 + .../SenseBeautify/Manager/EffectMacro.h | 50 + .../SenseBeautify/Manager/Effects.h | 172 +++ .../SenseBeautify/Manager/Effects.m | 628 ++++++++ .../SenseBeautify/Manager/EffectsAnimal.h | 53 + .../SenseBeautify/Manager/EffectsAnimal.m | 130 ++ .../SenseBeautify/Manager/EffectsAttribute.h | 48 + .../SenseBeautify/Manager/EffectsAttribute.m | 213 +++ .../Manager/EffectsCommonObject.h | 64 + .../Manager/EffectsCommonObject.m | 132 ++ .../SenseBeautify/Manager/EffectsDetector.h | 76 + .../SenseBeautify/Manager/EffectsDetector.m | 282 ++++ .../SenseBeautify/Manager/EffectsLicense.h | 24 + .../SenseBeautify/Manager/EffectsLicense.m | 76 + .../SenseBeautify/Manager/EffectsProcess.h | 260 ++++ .../SenseBeautify/Manager/EffectsProcess.m | 1264 +++++++++++++++++ .../Manager/VideoProcessingManager.h | 32 + .../Manager/VideoProcessingManager.m | 215 +++ .../SenseBeautify/SenseBeautifyVC.h | 15 + .../SenseBeautify/SenseBeautifyVC.m | 200 +++ .../zh-Hans.lproj/SenseBeautify.strings | 36 + .../ThirdBeautify/ThirdBeautify.swift | 4 + .../zh-Hans.lproj/Localizable.strings | 1 + iOS/APIExample/Podfile | 7 +- iOS/APIExample/cloud_build.sh | 6 + 32 files changed, 4658 insertions(+), 36 deletions(-) create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Base.lproj/SenseBeautify.storyboard create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectMacro.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.h create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.m create mode 100644 iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/zh-Hans.lproj/SenseBeautify.strings diff --git a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index 2ee49eaf0..cfb7bde61 100644 --- a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj +++ b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj @@ -76,6 +76,11 @@ 03DF1D9424CFC29700DF7151 /* AudioController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03DF1D8D24CFC29700DF7151 /* AudioController.m */; }; 03F8733224C8696600EDB1A3 /* EntryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03F8733124C8696600EDB1A3 /* EntryViewController.swift */; }; 0AC9459113B4F0B1EFAF4EFE /* Pods_SimpleFilter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC25C1A6D9E6B8827D095985 /* Pods_SimpleFilter.framework */; }; + 10166B872AA1DDAFFDC71269 /* SenseBeautyRender.m in Sources */ = {isa = PBXBuildFile; fileRef = 72375779AFD65DD129FAF105 /* SenseBeautyRender.m */; }; + 28423C7DAE300A777F786938 /* VideoProcessingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 224A00264C169D6854B06570 /* VideoProcessingManager.m */; }; + 3A39663442C88AA53AEF94BC /* EffectsCommonObject.m in Sources */ = {isa = PBXBuildFile; fileRef = C999757ADC0F02649B176208 /* EffectsCommonObject.m */; }; + 49613930F283D92C8E32833D /* SenseBeautifyVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 30D9083FB1BBB6D04AF5BE03 /* SenseBeautifyVC.m */; }; + 5054DEDFD3B8622CF3192CD2 /* EffectsProcess.m in Sources */ = {isa = PBXBuildFile; fileRef = 6234843FF07B935FFC02AB7A /* EffectsProcess.m */; }; 5744CE0925BA99FF0099AB66 /* VideoChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5744CE0525BA99FF0099AB66 /* VideoChat.swift */; }; 5744CE0A25BA99FF0099AB66 /* VideoChat.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5744CE0625BA99FF0099AB66 /* VideoChat.storyboard */; }; 576CA80A25A9CC3A0091520B /* output.raw in Resources */ = {isa = PBXBuildFile; fileRef = 576CA80925A9CC3A0091520B /* output.raw */; }; @@ -84,6 +89,7 @@ 576EA54825AC3523000B3D79 /* CustomPcmAudioSource.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 576EA54A25AC3523000B3D79 /* CustomPcmAudioSource.storyboard */; }; 576EA59025AEDD3C000B3D79 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; 57FE7C4B26B2D103002D9043 /* CircularBuffer.c in Sources */ = {isa = PBXBuildFile; fileRef = 57FE7C4726B2D103002D9043 /* CircularBuffer.c */; }; + 66983AE1EB1868311773D0E5 /* EffectsDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = AA7E29F940E47D3EB9FD0957 /* EffectsDetector.m */; }; 670936FD282DFE1600BC3954 /* ContentInspect.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 670936FF282DFE1600BC3954 /* ContentInspect.storyboard */; }; 6709B23B2806B0EA000BCC58 /* RawAudioData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6709B23A2806B0EA000BCC58 /* RawAudioData.swift */; }; 6709B23C2806BB4A000BCC58 /* RawAudioData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6709B23E2806BB4A000BCC58 /* RawAudioData.storyboard */; }; @@ -95,6 +101,7 @@ 67B8C7B628057D1500195106 /* RawVideoData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 67B8C7B828057D1500195106 /* RawVideoData.storyboard */; }; 67CB2F0C27EB318200CB19D2 /* SpatialAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67CB2F0A27EB318100CB19D2 /* SpatialAudio.swift */; }; 67CB2F0D27EB318200CB19D2 /* SpatialAudio.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */; }; + 7A4FF31E1ACA8DACEF0CC6DE /* EffectsLicense.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B317938C6E8C6A4EE12AB83 /* EffectsLicense.m */; }; 8407E0942472320800AC5DE8 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; 8B10BE1126AFFFA6002E1373 /* SimpleFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B10BE0F26AFFFA6002E1373 /* SimpleFilter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8B1422C226B50AB500FFF91B /* AudioProcessor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B1422BC26B50AB400FFF91B /* AudioProcessor.mm */; }; @@ -121,6 +128,7 @@ 8BC751D6273E502700552265 /* LiveStreaming.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8BC751D2273E502700552265 /* LiveStreaming.storyboard */; }; 8BC751D7273E502700552265 /* LiveStreaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC751D4273E502700552265 /* LiveStreaming.swift */; }; 8BC751DA273E57C900552265 /* VideoProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC751D9273E57C900552265 /* VideoProcess.swift */; }; + 8F57EC2E3001C94625E36005 /* EffectsAnimal.m in Sources */ = {isa = PBXBuildFile; fileRef = 8DBD6198BDA74EC494123339 /* EffectsAnimal.m */; }; A7847F922458062900469187 /* StatisticsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7847F912458062900469187 /* StatisticsInfo.swift */; }; A7847F942458089E00469187 /* AgoraExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7847F932458089E00469187 /* AgoraExtension.swift */; }; A7BD7660247CC6920062A6B3 /* UITypeAlias.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7BD765F247CC6920062A6B3 /* UITypeAlias.swift */; }; @@ -129,8 +137,11 @@ ABB399F22DF9605B00AF5033 /* Simulcast.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB399EC2DF9605B00AF5033 /* Simulcast.swift */; }; ABB399F32DF9605B00AF5033 /* Multipath.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ABB399EA2DF9605B00AF5033 /* Multipath.storyboard */; }; ABB399F42DF9605B00AF5033 /* Simulcast.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ABB399EF2DF9605B00AF5033 /* Simulcast.storyboard */; }; + B636350BC3C1E206016620E0 /* Effects.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BB4BF66ABB1F733D265CF71 /* Effects.m */; }; B9C914453E92C7F49C93E1F5 /* Pods_APIExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8551A4BC255CE49B383BC575 /* Pods_APIExample.framework */; }; CBCDE23FB64E60D6A79F3723 /* Pods_Agora_ScreenShare_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09E72C5D1AABD812866E41A6 /* Pods_Agora_ScreenShare_Extension.framework */; }; + D4962B442F6689A001EBE668 /* EFMotionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3875B62F3243B3CF0D453CAF /* EFMotionManager.m */; }; + DC121667695DB246115030CD /* EffectsAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = ACED6A4CBD7DB10EBCC981E8 /* EffectsAttribute.m */; }; DD6DCEE22CBD1D68008D7B72 /* hlg-hdr.yuv in Resources */ = {isa = PBXBuildFile; fileRef = DD6DCEE12CBD1D68008D7B72 /* hlg-hdr.yuv */; }; DD6ED7F12CB69FFB007B3B5D /* LocalCompositeGraph.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */; }; DD6ED7F32CB69FFB007B3B5D /* LocalCompositeGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6ED7EF2CB69FFB007B3B5D /* LocalCompositeGraph.swift */; }; @@ -185,6 +196,8 @@ E7A49D342907E74A00F06DD4 /* BundleUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = E7A49D332907E74A00F06DD4 /* BundleUtil.m */; }; E7AD0DE129C85FFB00C9A4B0 /* sample.mov in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DE029C85FFB00C9A4B0 /* sample.mov */; }; E7AD0DE329C95EB500C9A4B0 /* PickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7AD0DE229C95EB500C9A4B0 /* PickerView.swift */; }; + E986961B46231F8163D6456D /* SenseBeautify.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BA7993DC75813765F1AF9A03 /* SenseBeautify.storyboard */; }; + EDA97A2F343A3BA3624AAA5D /* EFGlobalSingleton.m in Sources */ = {isa = PBXBuildFile; fileRef = 846F0C22D2FE5CBAEDFC42C9 /* EFGlobalSingleton.m */; }; F728B9D72CA295D7007813BB /* PictureInPicture.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F728B9CD2CA295D7007813BB /* PictureInPicture.storyboard */; }; F728B9D82CA295D7007813BB /* ChannelViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9CE2CA295D7007813BB /* ChannelViewController.swift */; }; F728B9DB2CA295D7007813BB /* PIPBaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D22CA295D7007813BB /* PIPBaseViewController.swift */; }; @@ -335,8 +348,17 @@ 03DF1D8F24CFC29700DF7151 /* UIColor+CSRGB.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+CSRGB.h"; sourceTree = ""; }; 03F8733124C8696600EDB1A3 /* EntryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryViewController.swift; sourceTree = ""; }; 04225D244BD950D6A60AA5E3 /* Pods-audioFilter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-audioFilter.debug.xcconfig"; path = "Target Support Files/Pods-audioFilter/Pods-audioFilter.debug.xcconfig"; sourceTree = ""; }; + 0621233B3B3FD80035BA62CB /* Base */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SenseBeautify.storyboard; sourceTree = ""; }; 09E72C5D1AABD812866E41A6 /* Pods_Agora_ScreenShare_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Agora_ScreenShare_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0B317938C6E8C6A4EE12AB83 /* EffectsLicense.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EffectsLicense.m; sourceTree = ""; }; + 12393E14422D0C1C67F7DABD /* EffectsAttribute.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectsAttribute.h; sourceTree = ""; }; + 224A00264C169D6854B06570 /* VideoProcessingManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = VideoProcessingManager.m; sourceTree = ""; }; 23B93D5F4D8304FBAAE2A23E /* Pods-SimpleFilter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleFilter.debug.xcconfig"; path = "Target Support Files/Pods-SimpleFilter/Pods-SimpleFilter.debug.xcconfig"; sourceTree = ""; }; + 2D4154965AA9C052B405E7F6 /* SenseBeautifyVC.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SenseBeautifyVC.h; sourceTree = ""; }; + 30D9083FB1BBB6D04AF5BE03 /* SenseBeautifyVC.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SenseBeautifyVC.m; sourceTree = ""; }; + 32DF89EDFDDE9581BEA4F7D5 /* EffectsAnimal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectsAnimal.h; sourceTree = ""; }; + 3875B62F3243B3CF0D453CAF /* EFMotionManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EFMotionManager.m; sourceTree = ""; }; + 3BB4BF66ABB1F733D265CF71 /* Effects.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = Effects.m; sourceTree = ""; }; 3EA7D4B4D7C9540659392B7F /* Pods-APIExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample.debug.xcconfig"; path = "Target Support Files/Pods-APIExample/Pods-APIExample.debug.xcconfig"; sourceTree = ""; }; 4B422A0CD53DE0FBBEBCB2CA /* Pods-SimpleFilter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleFilter.release.xcconfig"; path = "Target Support Files/Pods-SimpleFilter/Pods-SimpleFilter.release.xcconfig"; sourceTree = ""; }; 5744CE0525BA99FF0099AB66 /* VideoChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoChat.swift; sourceTree = ""; }; @@ -352,6 +374,8 @@ 57FE7C4D26B2D1A4002D9043 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; 5A45F3A50A20714335E46364 /* Pods_Agora_ScreenShare_Extension_Socket_.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Agora_ScreenShare_Extension_Socket_.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5B3A20457BD332886F0CC52F /* Pods-SimpleAudioFilter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleAudioFilter.debug.xcconfig"; path = "Target Support Files/Pods-SimpleAudioFilter/Pods-SimpleAudioFilter.debug.xcconfig"; sourceTree = ""; }; + 5FBAFF115B372D5EDE5DF458 /* EffectsProcess.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectsProcess.h; sourceTree = ""; }; + 6234843FF07B935FFC02AB7A /* EffectsProcess.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EffectsProcess.m; sourceTree = ""; }; 670936FE282DFE1600BC3954 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ContentInspect.storyboard; sourceTree = ""; }; 67093701282DFE1B00BC3954 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ContentInspect.strings"; sourceTree = ""; }; 6709B23A2806B0EA000BCC58 /* RawAudioData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawAudioData.swift; sourceTree = ""; }; @@ -367,11 +391,15 @@ 67B8C7B728057D1500195106 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/RawVideoData.storyboard; sourceTree = ""; }; 67B8C7BA28057D1800195106 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RawVideoData.strings"; sourceTree = ""; }; 67CB2F0A27EB318100CB19D2 /* SpatialAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpatialAudio.swift; sourceTree = ""; }; - F7CB2F0B27EB318200CB19D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SpatialAudio.storyboard; sourceTree = ""; }; - F7CB2F0C27EB318200CB19D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SpatialAudio.strings"; sourceTree = ""; }; + 6E972D48B62C5DF86EEB1DAF /* VideoProcessingManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = VideoProcessingManager.h; sourceTree = ""; }; 6F2725A37161C72C3445F0D7 /* Pods-Agora-ScreenShare-Extension(Socket).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScreenShare-Extension(Socket).debug.xcconfig"; path = "Target Support Files/Pods-Agora-ScreenShare-Extension(Socket)/Pods-Agora-ScreenShare-Extension(Socket).debug.xcconfig"; sourceTree = ""; }; + 72375779AFD65DD129FAF105 /* SenseBeautyRender.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SenseBeautyRender.m; sourceTree = ""; }; + 7CD9DA3435391D65537F1E32 /* EFGlobalSingleton.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EFGlobalSingleton.h; sourceTree = ""; }; + 7DB5758B38DCA9DE302090EB /* SenseBeautyRender.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SenseBeautyRender.h; sourceTree = ""; }; 846AE4340F81DCC00B6F9543 /* Pods-APIExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample.release.xcconfig"; path = "Target Support Files/Pods-APIExample/Pods-APIExample.release.xcconfig"; sourceTree = ""; }; + 846F0C22D2FE5CBAEDFC42C9 /* EFGlobalSingleton.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EFGlobalSingleton.m; sourceTree = ""; }; 8551A4BC255CE49B383BC575 /* Pods_APIExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8918483B31EF4D04AD69528C /* EFMotionManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EFMotionManager.h; sourceTree = ""; }; 8B10BE0D26AFFFA6002E1373 /* SimpleFilter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleFilter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8B10BE0F26AFFFA6002E1373 /* SimpleFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleFilter.h; sourceTree = ""; }; 8B10BE1026AFFFA6002E1373 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -406,12 +434,17 @@ 8BC751D4273E502700552265 /* LiveStreaming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveStreaming.swift; sourceTree = ""; }; 8BC751D9273E57C900552265 /* VideoProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoProcess.swift; sourceTree = ""; }; 8BF2243D275FA1A500B65EF8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SimpleFilter.strings"; sourceTree = ""; }; + 8DBD6198BDA74EC494123339 /* EffectsAnimal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EffectsAnimal.m; sourceTree = ""; }; 92577CFC05C5519F5AF5591C /* Pods-audioFilter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-audioFilter.release.xcconfig"; path = "Target Support Files/Pods-audioFilter/Pods-audioFilter.release.xcconfig"; sourceTree = ""; }; 92EACE913B50B28F1588FE03 /* Pods-Agora-ScreenShare-Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScreenShare-Extension.release.xcconfig"; path = "Target Support Files/Pods-Agora-ScreenShare-Extension/Pods-Agora-ScreenShare-Extension.release.xcconfig"; sourceTree = ""; }; + 9844B3E7D162DC43FFF87D13 /* Effects.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = Effects.h; sourceTree = ""; }; + A211C8B11C9A8E8E28BBD191 /* EffectsDetector.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectsDetector.h; sourceTree = ""; }; A7847F912458062900469187 /* StatisticsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticsInfo.swift; sourceTree = ""; }; A7847F932458089E00469187 /* AgoraExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraExtension.swift; sourceTree = ""; }; A7BD765F247CC6920062A6B3 /* UITypeAlias.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITypeAlias.swift; sourceTree = ""; }; A7CA48C324553CF600507435 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Popover.storyboard; sourceTree = ""; }; + A8CA55BBA9FD9089D1FE876D /* EffectsLicense.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectsLicense.h; sourceTree = ""; }; + AA7E29F940E47D3EB9FD0957 /* EffectsDetector.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EffectsDetector.m; sourceTree = ""; }; AB8473EC2DF960D800B4DB73 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Simulcast.strings; sourceTree = ""; }; ABB399E72DF9605B00AF5033 /* Multipath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Multipath.swift; sourceTree = ""; }; ABB399E82DF9605B00AF5033 /* Multipath.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Multipath.storyboard; path = Base.lproj/Multipath.storyboard; sourceTree = ""; }; @@ -420,8 +453,13 @@ ABB399F62DF9608700AF5033 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Multipath.strings; sourceTree = ""; }; ABB90ACE2DF72E2F007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Simulcast.strings"; sourceTree = ""; }; ABB90AD02DF72E8D007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Multipath.strings"; sourceTree = ""; }; + ACED6A4CBD7DB10EBCC981E8 /* EffectsAttribute.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EffectsAttribute.m; sourceTree = ""; }; + B5BED75DAF7F75615310244E /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SenseBeautify.strings"; sourceTree = ""; }; + BA06B2EBCD5509FBE031F777 /* EffectsCommonObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectsCommonObject.h; sourceTree = ""; }; BC25C1A6D9E6B8827D095985 /* Pods_SimpleFilter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SimpleFilter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C999757ADC0F02649B176208 /* EffectsCommonObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = EffectsCommonObject.m; sourceTree = ""; }; CC6D08A23527C200339E4FD6 /* Pods-SimpleAudioFilter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SimpleAudioFilter.release.xcconfig"; path = "Target Support Files/Pods-SimpleAudioFilter/Pods-SimpleAudioFilter.release.xcconfig"; sourceTree = ""; }; + D15BB1E763F188A30470219A /* EffectMacro.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = EffectMacro.h; sourceTree = ""; }; DD6DCEE12CBD1D68008D7B72 /* hlg-hdr.yuv */ = {isa = PBXFileReference; lastKnownFileType = file; path = "hlg-hdr.yuv"; sourceTree = ""; }; DD6ED7EB2CB69FFB007B3B5D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LocalCompositeGraph.storyboard; sourceTree = ""; }; DD6ED7ED2CB69FFB007B3B5D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LocalCompositeGraph.strings"; sourceTree = ""; }; @@ -432,8 +470,6 @@ DDCED0302CBAA093004B1C9D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RtePlayer.strings"; sourceTree = ""; }; DDCED0322CBAA093004B1C9D /* RtePlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RtePlayer.swift; sourceTree = ""; }; DDFD50252C205BF20050B344 /* TransparentRender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentRender.swift; sourceTree = ""; }; - F7FD50272C205CF40050B344 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TransparentRender.storyboard; sourceTree = ""; }; - F7FD50282C205CF40050B344 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/TransparentRender.strings"; sourceTree = ""; }; DDFD50292C2065D00050B344 /* yuv_limit_range_alpha_1280_540_right.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = yuv_limit_range_alpha_1280_540_right.mp4; sourceTree = ""; }; E7163F7D2964149800EBBD55 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ARKit.strings"; sourceTree = ""; }; E7163F7E2964149800EBBD55 /* ARKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARKit.swift; sourceTree = ""; }; @@ -479,8 +515,6 @@ E7899BD82861673600851463 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CreateDataStream.strings"; sourceTree = ""; }; E7899BDA2861673600851463 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CreateDataStream.storyboard; sourceTree = ""; }; E7899BDB2861673600851463 /* CreateDataStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDataStream.swift; sourceTree = ""; }; - F7A49CB02900FC6400F06DD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/KtvCopyrightMusic.storyboard; sourceTree = ""; }; - F7A49CB12900FC6400F06DD4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KtvCopyrightMusic.strings"; sourceTree = ""; }; E7A49CB429011E7500F06DD4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MutliCamera.strings"; sourceTree = ""; }; E7A49CB629011E7500F06DD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MutliCamera.storyboard; sourceTree = ""; }; E7A49CB729011E7500F06DD4 /* MutliCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutliCamera.swift; sourceTree = ""; }; @@ -513,6 +547,12 @@ F73479252DF5CAEB0030C0CE /* AgoraBeautyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraBeautyManager.swift; sourceTree = ""; }; F73B01D22CCB84590077B7D2 /* APIReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = APIReporter.h; sourceTree = ""; }; F73B01D32CCB84590077B7D2 /* APIReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = APIReporter.m; sourceTree = ""; }; + F7A49CB02900FC6400F06DD4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/KtvCopyrightMusic.storyboard; sourceTree = ""; }; + F7A49CB12900FC6400F06DD4 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/KtvCopyrightMusic.strings"; sourceTree = ""; }; + F7CB2F0B27EB318200CB19D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/SpatialAudio.storyboard; sourceTree = ""; }; + F7CB2F0C27EB318200CB19D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SpatialAudio.strings"; sourceTree = ""; }; + F7FD50272C205CF40050B344 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/TransparentRender.storyboard; sourceTree = ""; }; + F7FD50282C205CF40050B344 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/TransparentRender.strings"; sourceTree = ""; }; FAAC2AEE355D103B9E8527B5 /* Pods-Agora-ScreenShare-Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScreenShare-Extension.debug.xcconfig"; path = "Target Support Files/Pods-Agora-ScreenShare-Extension/Pods-Agora-ScreenShare-Extension.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -879,6 +919,17 @@ path = ExternalAudio; sourceTree = ""; }; + 047A5C4316EA23A3E1583092 /* SenseBeautify */ = { + isa = PBXGroup; + children = ( + 2D4154965AA9C052B405E7F6 /* SenseBeautifyVC.h */, + 30D9083FB1BBB6D04AF5BE03 /* SenseBeautifyVC.m */, + E9A9E9421071189FCADF07E4 /* Manager */, + BA7993DC75813765F1AF9A03 /* SenseBeautify.storyboard */, + ); + path = SenseBeautify; + sourceTree = ""; + }; 5744CE0225BA99FF0099AB66 /* VideoChat */ = { isa = PBXGroup; children = ( @@ -1132,6 +1183,15 @@ path = TransparentRender; sourceTree = ""; }; + E6A5AF82C3B9A87CA080B694 /* SenseRender */ = { + isa = PBXGroup; + children = ( + 7DB5758B38DCA9DE302090EB /* SenseBeautyRender.h */, + 72375779AFD65DD129FAF105 /* SenseBeautyRender.m */, + ); + path = SenseRender; + sourceTree = ""; + }; E7163F7B2964149800EBBD55 /* ARKit */ = { isa = PBXGroup; children = ( @@ -1232,6 +1292,7 @@ isa = PBXGroup; children = ( E76347BD2AAEF4AF005D130F /* FURender */, + E6A5AF82C3B9A87CA080B694 /* SenseRender */, ); path = Render; sourceTree = ""; @@ -1279,6 +1340,7 @@ E7A49CEC29029E0000F06DD4 /* ThirdBeautify.storyboard */, E7A49CEE29029E0000F06DD4 /* ThirdBeautify.swift */, E7A49CEF29029E0000F06DD4 /* FUBeautify */, + 047A5C4316EA23A3E1583092 /* SenseBeautify */, ); path = ThirdBeautify; sourceTree = ""; @@ -1303,6 +1365,34 @@ path = Manager; sourceTree = ""; }; + E9A9E9421071189FCADF07E4 /* Manager */ = { + isa = PBXGroup; + children = ( + 7CD9DA3435391D65537F1E32 /* EFGlobalSingleton.h */, + 846F0C22D2FE5CBAEDFC42C9 /* EFGlobalSingleton.m */, + 8918483B31EF4D04AD69528C /* EFMotionManager.h */, + 3875B62F3243B3CF0D453CAF /* EFMotionManager.m */, + D15BB1E763F188A30470219A /* EffectMacro.h */, + 9844B3E7D162DC43FFF87D13 /* Effects.h */, + 3BB4BF66ABB1F733D265CF71 /* Effects.m */, + 32DF89EDFDDE9581BEA4F7D5 /* EffectsAnimal.h */, + 8DBD6198BDA74EC494123339 /* EffectsAnimal.m */, + 12393E14422D0C1C67F7DABD /* EffectsAttribute.h */, + ACED6A4CBD7DB10EBCC981E8 /* EffectsAttribute.m */, + BA06B2EBCD5509FBE031F777 /* EffectsCommonObject.h */, + C999757ADC0F02649B176208 /* EffectsCommonObject.m */, + A211C8B11C9A8E8E28BBD191 /* EffectsDetector.h */, + AA7E29F940E47D3EB9FD0957 /* EffectsDetector.m */, + A8CA55BBA9FD9089D1FE876D /* EffectsLicense.h */, + 0B317938C6E8C6A4EE12AB83 /* EffectsLicense.m */, + 5FBAFF115B372D5EDE5DF458 /* EffectsProcess.h */, + 6234843FF07B935FFC02AB7A /* EffectsProcess.m */, + 6E972D48B62C5DF86EEB1DAF /* VideoProcessingManager.h */, + 224A00264C169D6854B06570 /* VideoProcessingManager.m */, + ); + path = Manager; + sourceTree = ""; + }; F728B9D62CA295D7007813BB /* PixelBufferPIPViewController */ = { isa = PBXGroup; children = ( @@ -1401,6 +1491,7 @@ 1B6F6CF9B678035E221EAFDE /* [CP] Embed Pods Frameworks */, 0339BEBA25205B80007D4FDD /* Embed App Extensions */, E76F80122AF0A7A200CCB9D6 /* ShellScript */, + DB2429109D6D95174925E0A2 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1552,6 +1643,7 @@ E728B85128B60D5B00674A4A /* VideoViewSampleBufferDisplayView.xib in Resources */, 03D13BD72448758900B599B3 /* Main.storyboard in Resources */, F728B9D72CA295D7007813BB /* PictureInPicture.storyboard in Resources */, + E986961B46231F8163D6456D /* SenseBeautify.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1648,6 +1740,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DB2429109D6D95174925E0A2 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; E76F80122AF0A7A200CCB9D6 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1782,6 +1891,18 @@ F728B9DE2CA295D7007813BB /* PixelBufferRenderView.swift in Sources */, 03BCEC50244938C500ED7177 /* BaseViewController.swift in Sources */, 67CB2F0C27EB318200CB19D2 /* SpatialAudio.swift in Sources */, + 49613930F283D92C8E32833D /* SenseBeautifyVC.m in Sources */, + EDA97A2F343A3BA3624AAA5D /* EFGlobalSingleton.m in Sources */, + D4962B442F6689A001EBE668 /* EFMotionManager.m in Sources */, + B636350BC3C1E206016620E0 /* Effects.m in Sources */, + 8F57EC2E3001C94625E36005 /* EffectsAnimal.m in Sources */, + DC121667695DB246115030CD /* EffectsAttribute.m in Sources */, + 3A39663442C88AA53AEF94BC /* EffectsCommonObject.m in Sources */, + 66983AE1EB1868311773D0E5 /* EffectsDetector.m in Sources */, + 7A4FF31E1ACA8DACEF0CC6DE /* EffectsLicense.m in Sources */, + 5054DEDFD3B8622CF3192CD2 /* EffectsProcess.m in Sources */, + 28423C7DAE300A777F786938 /* VideoProcessingManager.m in Sources */, + 10166B872AA1DDAFFDC71269 /* SenseBeautyRender.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1815,33 +1936,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F7CB2F0B27EB318200CB19D2 /* Base */, - F7CB2F0C27EB318200CB19D2 /* zh-Hans */, - ); - name = SpatialAudio.storyboard; - sourceTree = ""; - }; - DDFD50272C205CF40050B344 /* TransparentRender.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F7FD50272C205CF40050B344 /* Base */, - F7FD50282C205CF40050B344 /* zh-Hans */, - ); - name = TransparentRender.storyboard; - sourceTree = ""; - }; - E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F7A49CB02900FC6400F06DD4 /* Base */, - F7A49CB12900FC6400F06DD4 /* zh-Hans */, - ); - name = KtvCopyrightMusic.storyboard; - sourceTree = ""; - }; 033A9F25252D70E400BC26E1 /* JoinChannelVideo.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2056,6 +2150,15 @@ name = RawVideoData.storyboard; sourceTree = ""; }; + 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7CB2F0B27EB318200CB19D2 /* Base */, + F7CB2F0C27EB318200CB19D2 /* zh-Hans */, + ); + name = SpatialAudio.storyboard; + sourceTree = ""; + }; 8B5E5B55274CBF760040E97D /* VideoProcess.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2121,6 +2224,15 @@ name = Simulcast.storyboard; sourceTree = ""; }; + BA7993DC75813765F1AF9A03 /* SenseBeautify.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0621233B3B3FD80035BA62CB /* Base */, + B5BED75DAF7F75615310244E /* zh-Hans */, + ); + name = SenseBeautify.storyboard; + sourceTree = ""; + }; DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2139,6 +2251,15 @@ name = RtePlayer.storyboard; sourceTree = ""; }; + DDFD50272C205CF40050B344 /* TransparentRender.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7FD50272C205CF40050B344 /* Base */, + F7FD50282C205CF40050B344 /* zh-Hans */, + ); + name = TransparentRender.storyboard; + sourceTree = ""; + }; E7163F7F2964149800EBBD55 /* ARKit.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2202,6 +2323,15 @@ name = CreateDataStream.storyboard; sourceTree = ""; }; + E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F7A49CB02900FC6400F06DD4 /* Base */, + F7A49CB12900FC6400F06DD4 /* zh-Hans */, + ); + name = KtvCopyrightMusic.storyboard; + sourceTree = ""; + }; E7A49CB529011E7500F06DD4 /* MutliCamera.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.h new file mode 100644 index 000000000..fea0065d8 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.h @@ -0,0 +1,28 @@ +// +// SenseBeatuyRender.h +// BeautyAPi +// +// Created by zhaoyongqiang on 2023/6/30. +// + +#define Sensetime "st_mobile_common.h" + +#import +#if __has_include("VideoProcessingManager.h") +#import "VideoProcessingManager.h" +#endif +#import "BeautyAPI.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface SenseBeautyRender : NSObject +@property (nonatomic, assign) BOOL isSuccessLicense; +@property (nonatomic, strong) void (^licenseEventCallback)(BOOL); +#if __has_include("VideoProcessingManager.h") +@property (nonatomic, strong) VideoProcessingManager *videoProcessing; +#endif + + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m new file mode 100644 index 000000000..9b1423c67 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m @@ -0,0 +1,164 @@ +// +// SenseBeatuyRender.m +// BeautyAPi +// +// Created by zhaoyongqiang on 2023/6/30. +// + +#import "SenseBeautyRender.h" + +@interface SenseBeautyRender () + +#if __has_include(Sensetime) +@property (nonatomic, strong) NSTimer *timer; +///贴纸id +@property (nonatomic, assign) int stickerId; +@property (nonatomic, assign) int filterId; +#endif + +@end + +@implementation SenseBeautyRender + +- (instancetype)init { + if (self == [super init]) { +#if __has_include(Sensetime) + [self checkSensetimeLicense]; +#endif + } + return self; +} + +- (NSDictionary *)sensetimeDefault { + NSDictionary *params = @{ + @"103": @(0.55), + @"101": @(0.2), + @"201": @(0.2), + @"318": @(0), + @"202": @(0.3), + @"306": @(0), + @"303": @(0), + @"320": @(0), + @"304": @(0), + @"309": @(0), + @"317": @(0), + }; + return params; +} + +#if __has_include("VideoProcessingManager.h") +- (VideoProcessingManager *)videoProcessing { + if (_videoProcessing == nil) { + _videoProcessing = [VideoProcessingManager new]; + } + return _videoProcessing; +} +#endif + +#if __has_include(Sensetime) +- (void)checkSensetimeLicense { + NSString *licensePath = [[NSBundle mainBundle] pathForResource:@"SENSEME" ofType:@"lic"]; + self.isSuccessLicense = [EffectsProcess authorizeWithLicensePath:licensePath]; + __weak SenseBeautyRender *weakSelf = self; + self.timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { + if (weakSelf.isSuccessLicense) { + [weakSelf.timer invalidate]; + weakSelf.timer = nil; + } else { + weakSelf.isSuccessLicense = weakSelf.videoProcessing.effectsProcess.isAuthrized; + } + if (weakSelf.licenseEventCallback) { + weakSelf.licenseEventCallback(weakSelf.isSuccessLicense); + } + }]; + [[NSRunLoop mainRunLoop]addTimer:self.timer forMode:NSRunLoopCommonModes]; +} +#endif + +- (void)destroy { +#if __has_include(Sensetime) + [self.timer invalidate]; + self.timer = nil; + [self reset]; + _videoProcessing = nil; +#endif +} + +- (nonnull CVPixelBufferRef)onCapture:(nonnull CVPixelBufferRef)pixelBuffer { +#if __has_include(Sensetime) + if (self.isSuccessLicense) { + return [self.videoProcessing videoProcessHandler:pixelBuffer]; + } + return pixelBuffer; +#endif + return pixelBuffer; +} + +#if __has_include() +- (AgoraVideoFormat)getVideoFormatPreference { + return AgoraVideoFormatCVPixelNV12; +} +#endif + +- (void)reset { +#if __has_include(Sensetime) + for (NSString *key in [self sensetimeDefault].allKeys) { + int type = key.intValue; + [self.videoProcessing setEffectType:type value: 0]; + } +#endif +} + +- (void)setBeautyPreset { + if (self.isSuccessLicense == NO) { + __weak SenseBeautyRender *weakSelf = self; + self.licenseEventCallback = ^(BOOL isSuccess) { + if (isSuccess) { + [weakSelf setBeautyDefault]; + } + }; + return; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self setBeautyDefault]; + }); +} + +- (void)setBeautyDefault { +#if __has_include(Sensetime) + for (NSString *key in [self sensetimeDefault].allKeys) { + int type = key.intValue; + float value = [[self sensetimeDefault][key]floatValue]; + [self.videoProcessing setEffectType:type value: value]; + } +#endif +} + +- (void)setMakeup:(BOOL)isSelected { +#if __has_include(Sensetime) + if (isSelected) { + __weak SenseBeautyRender *weakself = self; + [self.videoProcessing addStylePath:@"hunxue.zip" groupId:0 strength:0.5 callBack:^(int sticker) { + weakself.stickerId = sticker; + }]; + } else { + [self.videoProcessing removeStickerId:self.stickerId]; + self.stickerId = 0; + } +#endif +} + +- (void)setSticker:(BOOL)isSelected { +#if __has_include(Sensetime) + if (isSelected) { + NSString *path = [[NSBundle mainBundle] pathForResource:@"ShangBanLe.zip" ofType:nil]; + [self.videoProcessing.effectsProcess setStickerWithPath:path callBack:^(st_result_t state, int stickerId, uint64_t action) { + + }]; + } else { + [self.videoProcessing cleareStickers]; + } +#endif +} + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Base.lproj/SenseBeautify.storyboard b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Base.lproj/SenseBeautify.storyboard new file mode 100644 index 000000000..406c1b5cb --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Base.lproj/SenseBeautify.storyboard @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.h new file mode 100644 index 000000000..d914ab213 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.h @@ -0,0 +1,27 @@ +// +// EFGlobalSingleton.h +// SenseMeEffects +// +// Created by 马浩萌 on 2021/11/30. +// Copyright © 2021 SoftSugar. All rights reserved. +// + +#import + +static NSString * const EFGlobalSingletonMaleKey = @"EFGlobalSingletonMaleKey"; + +@interface EFGlobalSingleton : NSObject + +@property (nonatomic, assign) int efTouchTriggerAction; // 点击屏幕触发事件保存 +@property (nonatomic, assign) BOOL efHasSegmentCapability; // 标识是否有皮肤分割capability +@property (nonatomic, assign) BOOL isMale; // 标识当前用户性别(不同默认参数) +@property (nonatomic, assign) BOOL needDelay; // 是否是tryon试鞋/试表需要开启未来帧 +@property (nonatomic, assign) BOOL isTryonShoes; // 是否是tryon试鞋(光脚提示) +@property (nonatomic, assign) BOOL isPortraitOnly; // 是否是gan image(只支持横屏) + ++(instancetype)sharedInstance; +-(instancetype)init NS_UNAVAILABLE; ++(instancetype)new NS_UNAVAILABLE; ++(instancetype)alloc NS_UNAVAILABLE; + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.m new file mode 100644 index 000000000..dd6b6bd41 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFGlobalSingleton.m @@ -0,0 +1,31 @@ +// +// EFGlobalSingleton.m +// SenseMeEffects +// +// Created by 马浩萌 on 2021/11/30. +// Copyright © 2021 SoftSugar. All rights reserved. +// + +#import "EFGlobalSingleton.h" + +@implementation EFGlobalSingleton + ++(instancetype)sharedInstance { + static EFGlobalSingleton * _shared = nil; + static dispatch_once_t sharedOnceToken; + dispatch_once(&sharedOnceToken, ^{ + _shared = [[self alloc] init]; + }); + return _shared; +} + +-(BOOL)isMale { + NSNumber *isMaleNumberValue = (NSNumber *)[[NSUserDefaults standardUserDefaults] objectForKey:EFGlobalSingletonMaleKey]; + return isMaleNumberValue.boolValue; +} + +-(void)setIsMale:(BOOL)isMale { + [[NSUserDefaults standardUserDefaults] setObject:[NSNumber numberWithBool:isMale] forKey:EFGlobalSingletonMaleKey]; +} + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.h new file mode 100644 index 000000000..a301ca5d9 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.h @@ -0,0 +1,26 @@ +// +// EFMotionManager.h +// SenseMeEffects +// +// Created by sunjian on 2021/7/15. +// Copyright © 2021 SenseTime. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface EFMotionManager : NSObject + +@property (nonatomic, strong) CMMotionManager *motionManager; + ++ (instancetype)sharedInstance; + +- (void)start; + +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.m new file mode 100644 index 000000000..52e9f567f --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EFMotionManager.m @@ -0,0 +1,56 @@ +// +// EFMotionManager.m +// SenseMeEffects +// +// Created by sunjian on 2021/7/15. +// Copyright © 2021 SenseTime. All rights reserved. +// + +#import "EFMotionManager.h" +#import + +@interface EFMotionManager () +{ + BOOL _begin; +} +@end + +@implementation EFMotionManager + ++ (instancetype)sharedInstance{ + static EFMotionManager *manager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[EFMotionManager alloc] init]; + }); + return manager; +} + +#pragma mark - getter/setter +- (CMMotionManager *)motionManager{ + if (!_motionManager) { + _motionManager = [[CMMotionManager alloc] init]; + _motionManager.accelerometerUpdateInterval = 0.5; + _motionManager.deviceMotionUpdateInterval = 1 / 25.0; + } + return _motionManager; +} + +- (void)start{ + if(_begin) return; + _begin = YES; + if ([self.motionManager isAccelerometerAvailable]) { + [self.motionManager startAccelerometerUpdates]; + } + if ([self.motionManager isDeviceMotionAvailable]) { + [self.motionManager startDeviceMotionUpdates]; + } +} + +- (void)stop{ + _begin = NO; + [self.motionManager stopAccelerometerUpdates]; + [self.motionManager stopDeviceMotionUpdates]; +} + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectMacro.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectMacro.h new file mode 100644 index 000000000..d93b20da6 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectMacro.h @@ -0,0 +1,50 @@ +// +// EffectMacro.h +// SenseMeEffects +// +// Created by sunjian on 2021/9/17. +// Copyright © 2021 SenseTime. All rights reserved. +// + +#ifndef EffectMacro_h +#define EffectMacro_h + +#import + +typedef NS_ENUM(NSUInteger, EffectsType){ + EffectsTypePreview, + EffectsTypePhoto, + EffectsTypeVideo, +}; + +typedef enum : NSUInteger { + EFDetectConfigModeOther = 0, + EFDetectConfigModeItsMe, +} EFDetectConfigMode; + +#define EFFECTS_LOG 1 +#ifdef DEBUG +#if EFFECTS_LOG +#define EFFECTSLog(format , ...) NSLog((format) , ##__VA_ARGS__); +#else +#define EFFECTSLog(format , ...) +#endif +#else +#define EFFECTSLog(format , ...) +#endif + +#ifdef DEBUG +#if EFFECTS_LOG +#define NSLog(format , ...) NSLog((format) , ##__VA_ARGS__); +#endif +#endif + +#if EFFECTS_LOG +#define EFFECTSTIMELOG(key) double key = CFAbsoluteTimeGetCurrent(); +#define EFFECTSTIMEPRINT(key , dsc) printf("%s\t%.1f \n" , dsc , (CFAbsoluteTimeGetCurrent() - key) * 1000); +#else +#define EFFECTSTIMELOG(key) +#define EFFECTSTIMEPRINT(key , dsc) +#endif + +#endif /* EffectMacro_h */ diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.h new file mode 100644 index 000000000..cead3ed4a --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.h @@ -0,0 +1,172 @@ +// +// Effects.h +// SenseMeEffects +// +// Created by sunjian on 2021/6/4. +// + +#import +#if __has_include("st_mobile_common.h") +#import "st_mobile_common.h" +#import "st_mobile_effect.h" +#endif +#import "EffectMacro.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface EffectsMotionManager : NSObject +@end + +@interface Effects : NSObject +@property (nonatomic, strong) EAGLContext *glContext; +#if __has_include("st_mobile_common.h") +@property (nonatomic) st_handle_t handle; +@property (nonatomic) st_handle_t hConvertHandle; +#endif +@property (nonatomic, assign) uint64_t config; +@property (nonatomic, assign) GLuint outputTexture; +@property (nonatomic) CVPixelBufferRef outputPixelBuffer; +@property (nonatomic) CVOpenGLESTextureRef outputCVTexture; +@property (nonatomic, assign) int width; +@property (nonatomic, assign) int height; +@property (nonatomic) AVCaptureDevicePosition cameraPosition; + +- (instancetype)initWithType:(EffectsType)type context:(EAGLContext *)glContext; + +/// 设置当前EAGLContext +/// @param glContext 当前GLContext +- (void)setCurrentEAGLContext:(EAGLContext*)glContext; + +#if __has_include("st_mobile_common.h") +/// 设置特效 +/// @param type 特效类型 +/// @param path 特效素材路径 +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type path:(NSString *)path; + +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type model:(int)model; + +/// 设置特效程度值 +/// @param type 特效类型 +/// @param value 强度值 +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type value:(float)value; + +/// 设置风格强度 +/// @param packageId packageID +/// @param type 类型 +/// @param value 强度值 +- (st_result_t)setPackageId:(int)packageId groupType:(st_effect_beauty_group_t)type strength:(float)value; + +- (st_result_t)setTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType; + +- (st_result_t)getTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType; + +- (st_result_t)setBeautyParam:(st_effect_beauty_param_t)param andVal:(float)val; + +- (st_result_t)getBeautyParam:(st_effect_beauty_param_t)param andVal:(float *)val; + +- (st_result_t)get3dBeautyPartsSize:(int *)partSize; + +- (st_result_t)get3dBeautyParts:(st_effect_3D_beauty_part_info_t[])parts fromSize:(int)partSize; + +- (st_result_t)set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t *)parts andVal:(int)partSize; + +- (st_result_t)f_set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t[])parts andVal:(int)partSize; + +- (st_result_t)disableOverlap:(BOOL)isDisableOverlap; +- (st_result_t)disableModuleReorder:(BOOL)isDisableModuleReorder; + +/// 设置贴纸 +/// @param stickerPath stickerPath 2D贴纸 +/// @param callback 回调Block +- (void)setStickerWithPath:(NSString *)stickerPath + callBack:(void(^)(st_result_t state, int stickerId, uint64_t action))callback; + +- (void)setStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback; + +/// 添加贴纸 +/// @param stickerPath stickerPath 贴纸资源路径 +/// @param callback 回调Block +- (void)addStickerWithPath:(NSString *)stickerPath + callBack:(void(^)(st_result_t state, int sticker, uint64_t action))callback; + +- (void)addStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback; + +-(void)changeStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback; + +/// 获取获取素材的贴纸信息 +/// @param package_id package_id +/// @param modules 贴纸信息 +-(st_result_t)getModulesInPackage:(int)package_id modules:(st_effect_module_info_t*)modules; + +/// 设置贴纸信息 +/// @param module_info 贴纸信息 +-(st_result_t)setModuleInfo:(st_effect_module_info_t *)module_info; + +/// 重新播放贴纸 +/// @param packageId packageId +-(void)replayStickerWithPackage:(int)packageId; + + +/// 获取覆盖生效的美颜的信息 +/// @param callback 回调block +- (void)getOverLap:(void(^)(st_effect_beauty_info_t *beauty_info))callback; + +/// 获取覆盖生效的美颜的信息 +- (st_effect_beauty_info_t *)getOverlapInfo:(int *)count; + +/// 移除贴纸 +/// @param stickerId 贴纸Id +- (st_result_t)removeSticker:(int)stickerId; + +/// 清空贴纸 +- (st_result_t)cleareStickers; + +- (uint64_t)getDetectConfig; +- (uint64_t)getDetectConfigWithMode:(EFDetectConfigMode)configMode; + +- (uint64_t)getAnimalDetectConfig; + +- (GLuint)processTexture:(GLuint)inputTexture + inputData:(unsigned char*)inputData + inputFormat:(st_pixel_format)inputFormat + outputTexture:(GLuint)outputTexture + width:(int)width + height:(int)height + stride:(int)stride + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outDetectResult:(st_mobile_human_action_t)outDetectResult + withCache:(CVOpenGLESTextureCacheRef)cache + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out; + +- (GLuint)processTexture:(GLuint)inputTexture + inputData:(unsigned char*)inputData + inputFormat:(st_pixel_format)inputFormat + outputTexture:(GLuint)outputTexture + width:(int)width + height:(int)height + stride:(int)stride + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outDetectResult:(st_mobile_human_action_t)outDetectResult + withCache:(CVOpenGLESTextureCacheRef)cache + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out + meshList:(st_mobile_face_mesh_list_t)mesh_list; + +- (void)convertYUVBuffer:(unsigned char *)buffer + rgba:(GLuint)texture + size:(CGSize)size; + +-(st_result_t)setParam:(st_effect_param_t)param andValue:(float)value; + +-(st_result_t)setFaceMeshList:(st_mobile_face_mesh_list_t)mesh_list; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.m new file mode 100644 index 000000000..0e3355771 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/Effects.m @@ -0,0 +1,628 @@ +// +// Effects.m +// SenseMeEffects +// +// Created by sunjian on 2021/6/4. +// + +#import "Effects.h" +#if __has_include("st_mobile_common.h") +#import "st_mobile_effect.h" +#import "st_mobile_color_convert.h" +#endif +#import +#import +#import +#import "EFGlobalSingleton.h" + +@interface EffectsMotionManager () +{ + BOOL _begin; +} +@property (nonatomic, strong) CMMotionManager *motionManager; +@end + +@implementation EffectsMotionManager + ++ (instancetype)sharedInstance{ + static EffectsMotionManager *manager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[EffectsMotionManager alloc] init]; + }); + return manager; +} + +#pragma mark - getter/setter +- (CMMotionManager *)motionManager{ + if (!_motionManager) { + _motionManager = [[CMMotionManager alloc] init]; + _motionManager.accelerometerUpdateInterval = 0.5; + _motionManager.deviceMotionUpdateInterval = 1 / 25.0; + } + return _motionManager; +} + +- (void)start{ + if(_begin) return; + _begin = YES; + if ([self.motionManager isAccelerometerAvailable]) { + [self.motionManager startAccelerometerUpdates]; + } + if ([self.motionManager isDeviceMotionAvailable]) { + [self.motionManager startDeviceMotionUpdates]; + } +} + +- (void)stop{ + _begin = NO; + [self.motionManager stopAccelerometerUpdates]; + [self.motionManager stopDeviceMotionUpdates]; +} + +@end + + +@interface Effects () +{ + EffectsType _type; +} +@end + +@implementation Effects + +- (void)dealloc{ +#if __has_include("st_mobile_common.h") + if (self.handle) { + st_mobile_effect_destroy_handle(self.handle); + } + if (self.hConvertHandle) { + st_mobile_color_convert_destroy(self.hConvertHandle); + } +#endif + [self destoryGLResource]; + [[EffectsMotionManager sharedInstance] stop]; +} + +- (void)destoryGLResource{ + if(self.outputTexture){ + GLuint texture = self.outputTexture; + glDeleteTextures(1, &texture); + self.outputTexture = 0; + } + if(self.outputPixelBuffer){ + CVPixelBufferRef pixelBuffer = self.outputPixelBuffer; + CVPixelBufferRelease(pixelBuffer); + } + if(self.outputCVTexture){ + CVOpenGLESTextureRef cvTexture = self.outputCVTexture; + CFRelease(cvTexture); + } +} + + +- (instancetype)initWithType:(EffectsType)type context:(EAGLContext *)glContext{ + self = [super init]; + [self createHandleWithType:type context:glContext]; + return self; +} + +- (void)createHandleWithType:(EffectsType)type context:(EAGLContext *)glContext{ +#if __has_include("st_mobile_common.h") + st_handle_t handle; + st_result_t ret; + _type = type; + switch (type) { + case EffectsTypePreview:{ + ret = st_mobile_effect_create_handle(EFFECT_CONFIG_NONE, &handle); + if (ret != ST_OK) { + NSLog(@"st_mobile_effect_create_handle error %d", ret); + return; + } + st_result_t setRet = ST_OK; + setRet = st_mobile_effect_set_module_state_change_callback(handle, _modul_state_change_callback); + if (setRet != ST_OK) { + NSLog(@"st_mobile_effect_set_module_state_change_callback error %d", setRet); + } + self.glContext = glContext; + } + break; + case EffectsTypeVideo: + ret = st_mobile_effect_create_handle(EFFECT_CONFIG_IMAGE_MODE, &handle); + st_result_t setRet = ST_OK; + setRet = st_mobile_effect_set_module_state_change_callback(handle, _modul_state_change_callback); + if (setRet != ST_OK) { + NSLog(@"st_mobile_effect_set_module_state_change_callback error %d", setRet); + } + break; + case EffectsTypePhoto: + ret = st_mobile_effect_create_handle(EFFECT_CONFIG_IMAGE_MODE, &handle); + ret = st_mobile_effect_set_module_state_change_callback(handle, _modul_state_change_callback); + if (setRet != ST_OK) { + NSLog(@"st_mobile_effect_set_module_state_change_callback error %d", setRet); + } + break; + } + st_mobile_effect_set_param(handle, EFFECT_PARAM_MAX_MEMORY_BUDGET_MB, 1000.0); +// st_mobile_effect_set_param(handle, EFFECT_PARAM_QUATERNION_SMOOTH_FRAME, 5.0); /// x< 设置相机姿态平滑参数,表示平滑多少帧, 越大延迟越高,抖动越微弱 + + if (ret != ST_OK) { + NSLog(@"st_mobile_effect_create_handle %d", ret); + return; + } + ret = st_mobile_color_convert_create(&_hConvertHandle); + if (ret != ST_OK) { + NSLog(@"st_mobile_color_convert_create %d", ret); + return; + } + [[EffectsMotionManager sharedInstance] start]; + + self.handle = handle; +#endif +} + +#if __has_include("st_mobile_common.h") +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type path:(NSString *)path{ + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_beauty(self.handle, type, path.UTF8String); + if (iRet != ST_OK) { + NSLog(@"st mobile beautify set beautiy type %d failed: %d", type, iRet); + } + return iRet; +} + +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type model:(int)model{ + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_beauty_mode(self.handle, type, model); + if (iRet != ST_OK) { + NSLog(@"st mobile beautify set beautiy type %d failed: %d", type, iRet); + } + return iRet; +} + +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type value:(float)value{ + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_beauty_strength(self.handle, type, value); + if (iRet != ST_OK) { + NSLog(@"st mobile beautify set beautiy type %d failed: %d", type, iRet); + } + return iRet; +} + +- (st_result_t)setPackageId:(int)packageId groupType:(st_effect_beauty_group_t)type strength:(float)value{ + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_package_beauty_group_strength(self.handle, packageId, type, value); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_package_beauty_group_strength failed"); + } + return iRet; +} + +- (st_result_t)setTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_tryon_param(self.handle, tryonType, tryonInfo); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_tryon_param failed"); + } + return iRet; +} + +- (st_result_t)getTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_get_tryon_param(self.handle, tryonType, tryonInfo); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_get_tryon_param failed"); + } + return iRet; +} + +- (st_result_t)setBeautyParam:(st_effect_beauty_param_t)param andVal:(float)val { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_beauty_param(self.handle, param, val); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_beauty_param failed"); + } + return iRet; +} + +- (st_result_t)getBeautyParam:(st_effect_beauty_param_t)param andVal:(float *)val { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_get_beauty_param(self.handle, param, val); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_get_beauty_param failed"); + } + return iRet; +} + +- (st_result_t)get3dBeautyPartsSize:(int *)partSize { + st_result_t iRet = ST_OK; + iRet = st_moobile_effect_get_3d_beauty_parts_count(self.handle, partSize); + if (iRet != ST_OK) { + NSLog(@"st_moobile_effect_get_3d_beauty_parts_count failed %d", iRet); + } + return iRet; +} + +- (st_result_t)get3dBeautyParts:(st_effect_3D_beauty_part_info_t[])parts fromSize:(int)partSize{ + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_get_3d_beauty_parts(self.handle, parts, partSize); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_get_3d_beauty_parts failed"); + } + return iRet; +} + +- (st_result_t)set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t *)parts andVal:(int)partSize { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_3d_beauty_parts_strength(self.handle, parts, partSize); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_3d_beauty_parts_strength failed"); + } + return iRet; +} + +- (st_result_t)f_set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t[])parts andVal:(int)partSize { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_3d_beauty_parts_strength(self.handle, parts, partSize); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_3d_beauty_parts_strength failed"); + } + return iRet; +} + +- (st_result_t)disableOverlap:(BOOL)isDisableOverlap { + st_result_t iRet = ST_OK; + st_mobile_effect_set_param(self.handle, EFFECT_PARAM_DISABLE_BEAUTY_OVERLAP, isDisableOverlap); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_param EFFECT_PARAM_DISABLE_BEAUTY_OVERLAP failed"); + } + return iRet; +} + +- (st_result_t)disableModuleReorder:(BOOL)isDisableModuleReorder { + st_result_t iRet = ST_OK; + st_mobile_effect_set_param(self.handle, EFFECT_PARAM_DISABLE_MODULE_REORDER, isDisableModuleReorder); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_param EFFECT_PARAM_DISABLE_MODULE_REORDER failed"); + } + return iRet; +} + +- (void)setStickerWithPath:(NSString *)stickerPath callBack:(void(^)(st_result_t state, int stickerId, uint64_t action))callback{ + int packageId = 0; + st_result_t iRet = st_mobile_effect_change_package(self.handle, stickerPath.UTF8String, &packageId); + if (ST_OK != iRet) { + NSLog(@"st_mobile_sticker_change_package error %d", iRet); + } + uint64_t action = 0; + st_mobile_effect_get_detect_config(self.handle, &action); + if (callback) { + callback(iRet, packageId, action); + } +} + +- (void)setStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback{ + int packageId = 0; + st_result_t iRet = st_mobile_effect_change_package(self.handle, stickerPath.UTF8String, &packageId); + if (ST_OK != iRet) { + NSLog(@"st_mobile_sticker_change_package error %d", iRet); + } + uint64_t action = 0; + st_mobile_effect_get_detect_config(self.handle, &action); + + uint64_t customAciton = 0; + st_mobile_effect_get_custom_event_config(self.handle, &customAciton); + + if (callback) { + callback(iRet, packageId, action, customAciton); + } +} + +- (void)addStickerWithPath:(NSString *)stickerPath callBack:(void (^)(st_result_t, int, uint64_t))callback{ + if (!stickerPath) { + return; + } + int packageId = 0; + st_result_t iRet = st_mobile_effect_add_package(self.handle, stickerPath.UTF8String, &packageId); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_add_package error %d", iRet); + return; + } + uint64_t action = 0; + st_mobile_effect_get_detect_config(self.handle, &action); + if (callback) { + callback(iRet, packageId, action); + } +} + +- (void)addStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback { + if (!stickerPath) { + return; + } + int packageId = 0; + st_result_t iRet = st_mobile_effect_add_package(self.handle, stickerPath.UTF8String, &packageId); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_add_package error %d", iRet); + return; + } + uint64_t action = 0; + st_mobile_effect_get_detect_config(self.handle, &action); + + uint64_t customAciton = 0; + st_mobile_effect_get_custom_event_config(self.handle, &customAciton); + + if (callback) { + callback(iRet, packageId, action, customAciton); + } +} + +-(void)changeStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback { + if (!stickerPath) { + return ; + } + int packageId = 0; + st_result_t iRet = st_mobile_effect_change_package(self.handle, stickerPath.UTF8String, &packageId); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_add_package error %d", iRet); + return; + } + uint64_t action = 0; + st_mobile_effect_get_detect_config(self.handle, &action); + + uint64_t customAciton = 0; + st_mobile_effect_get_custom_event_config(self.handle, &customAciton); + + if (callback) { + callback(iRet, packageId, action, customAciton); + } +} + +-(st_result_t)getModulesInPackage:(int)package_id modules:(st_effect_module_info_t*)modules { + st_effect_package_info_t *p_package_info = malloc(sizeof(st_effect_package_info_t)); + st_result_t iRet = st_mobile_effect_get_package_info(self.handle, package_id, p_package_info); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_get_package_info error %d", iRet); + return iRet; + } + iRet = st_mobile_effect_get_modules_in_package(self.handle, package_id, modules, p_package_info->module_count); + free(p_package_info); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_get_modules_in_package error %d", iRet); + return iRet; + } + return iRet; +} + +-(st_result_t)setModuleInfo:(st_effect_module_info_t *)module_info { + st_result_t iRet = st_mobile_effect_set_module_info(self.handle, module_info); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_set_module_info error %d", iRet); + } + return iRet; +} + +-(void)replayStickerWithPackage:(int)packageId { + st_result_t iRet = st_mobile_effect_replay_package(self.handle, packageId); + if (ST_OK != iRet) { + NSLog(@"st_mobile_effect_replay_package error %d", iRet); + return; + } +} + +- (void)getOverLap:(void (^)(st_effect_beauty_info_t * _Nonnull))callback{ + int beauty_num = 0; + st_mobile_effect_get_overlapped_beauty_count(self.handle, &beauty_num); + st_effect_beauty_info_t beauty_info[beauty_num]; + st_mobile_effect_get_overlapped_beauty(self.handle, beauty_info, beauty_num); + if (beauty_num && callback) { + callback(beauty_info); + } +} + +- (st_effect_beauty_info_t *)getOverlapInfo:(int *)count{ + int beauty_num = 0; + st_result_t ret = st_mobile_effect_get_overlapped_beauty_count(self.handle, &beauty_num); + if (ST_OK != ret || !beauty_num) { + return nil; + } + *count = beauty_num; + st_effect_beauty_info_t *beauty_info = (st_effect_beauty_info_t *)malloc(sizeof(st_effect_beauty_info_t) * beauty_num); + ret = st_mobile_effect_get_overlapped_beauty(self.handle, beauty_info, beauty_num); + if (ST_OK != ret) { + return nil; + } + return beauty_info; +} + + +- (st_result_t)removeSticker:(int)stickerId{ + return st_mobile_effect_remove_package(self.handle, stickerId); +} + +- (st_result_t)cleareStickers{ + return st_mobile_effect_clear_packages(self.handle); +} + +- (GLuint)processTexture:(GLuint)inputTexture + inputData:(unsigned char*)inputData + inputFormat:(st_pixel_format)inputFormat + outputTexture:(GLuint)outputTexture + width:(int)width + height:(int)height + stride:(int)stride + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outDetectResult:(st_mobile_human_action_t)outDetectResult + withCache:(CVOpenGLESTextureCacheRef)cache + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out { + st_mobile_face_mesh_list_t tmp = {}; + return [self processTexture:inputTexture inputData:inputData inputFormat:inputFormat outputTexture:outputTexture width:width height:height stride:stride rotate:rotate detectResult:detectResult animalResult:animalResult outDetectResult:outDetectResult withCache:cache outPixelFormat:fmt_out outBuffer:img_out meshList:tmp]; +} + +- (GLuint)processTexture:(GLuint)inputTexture + inputData:(unsigned char*)inputData + inputFormat:(st_pixel_format)inputFormat + outputTexture:(GLuint)outputTexture + width:(int)width + height:(int)height + stride:(int)stride + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outDetectResult:(st_mobile_human_action_t)outDetectResult + withCache:(CVOpenGLESTextureCacheRef)cache + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out + meshList:(st_mobile_face_mesh_list_t)mesh_list { + if (!self.handle) return 0; + if (self.glContext)[self setCurrentEAGLContext:self.glContext]; + if (self.width != width || self.height != height) { + [self destoryGLResource]; + self.width = width; + self.height = height; + } + st_handle_t handle = self.handle; + if (!glIsTexture(outputTexture)) return 0; + st_result_t ret = ST_OK; + if (handle) { + st_effect_custom_param_t inputEvent; + memset(&inputEvent, 0, sizeof(st_effect_custom_param_t)); + uint64_t type = EFFECT_CUSTOM_NONE; + //get custom param + st_mobile_effect_get_custom_param_config(handle, &type); + if (CHECK_FLAG(type, EFFECT_CUSTOM_CAMERA_QUATION)) { + if (_type == EffectsTypePreview) { + CMDeviceMotion *motion = [EffectsMotionManager sharedInstance].motionManager.deviceMotion; + inputEvent.camera_quat.x = motion.attitude.quaternion.x; + inputEvent.camera_quat.y = motion.attitude.quaternion.y; + inputEvent.camera_quat.z = motion.attitude.quaternion.z; + inputEvent.camera_quat.w = motion.attitude.quaternion.w; + } else { // 获取默认相机四元数 + st_quaternion_t p_default_quat; + ret = st_mobile_effect_get_default_camera_quaternion(self.handle, self.cameraPosition == AVCaptureDevicePositionFront, &p_default_quat); + if (ret != ST_OK) { + NSLog(@"st_mobile_effect_get_default_camera_quaternion error %d", ret); + } + inputEvent.camera_quat = p_default_quat; + } + } + if(CHECK_FLAG(type, EFFECT_CUSTOM_CAMERA_FACING)){ + inputEvent.front_camera = self.cameraPosition == AVCaptureDevicePositionFront; + } + + EFGlobalSingleton *globalSingleton = [EFGlobalSingleton sharedInstance]; + if (globalSingleton.efTouchTriggerAction > 0) { + inputEvent.event = globalSingleton.efTouchTriggerAction; + globalSingleton.efTouchTriggerAction = 0; + } + + st_mobile_texture_t input_texture = {inputTexture, width, height, ST_PIX_FMT_BGRA8888}; + st_effect_render_in_param_t input_param ={}; + input_param.p_custom_param = &inputEvent; + input_param.p_human = &detectResult; + input_param.p_animal = animalResult; + input_param.rotate = rotate; + input_param.front_rotate = rotate; + input_param.need_mirror = false; + input_param.p_tex = &input_texture; + + st_image_t inputImage = {.data = inputData, .pixel_format = inputFormat, .width = width, .height = height, .stride = stride, .time_stamp= 0.0}; + st_effect_in_image_t effectImag = {.image = inputImage, .rotate = ST_CLOCKWISE_ROTATE_0, .b_mirror = GL_FALSE}; + input_param.p_image = &effectImag; + st_mobile_texture_t output_texture = {outputTexture, width, height, ST_PIX_FMT_BGRA8888}; + st_effect_render_out_param_t output_param = {}; + output_param.p_tex = &output_texture; + st_mobile_human_action_t human_out_param; + memset(&human_out_param, 0, sizeof(st_mobile_human_action_t)); + output_param.p_human = &human_out_param; + st_handle_t handle = self.handle; + EFFECTSTIMELOG(key) + st_result_t iRet = st_mobile_effect_render(handle, &input_param, &output_param); + + st_mobile_human_action_delete(&human_out_param); + + if (iRet != ST_OK) { + NSLog(@"st_mobile_process_texture failed: %d", iRet); + } + EFFECTSTIMEPRINT(key, "st_mobile_effect_render") + } + if (ret != ST_OK) { + NSLog(@"st_mobile_beautify_process_texture error %d", ret); + return 0; + } + return outputTexture; +} + + +- (uint64_t)getDetectConfig{ + uint64_t config; + st_handle_t handle = self.handle; + st_mobile_effect_get_detect_config(handle, &config); + return config; +} + +- (uint64_t)getDetectConfigWithMode:(EFDetectConfigMode)configMode { + uint64_t config = [self getDetectConfig]; + if (configMode == EFDetectConfigModeItsMe) { + config |= ST_MOBILE_FACE_DETECT; + } + return config; +} + +- (uint64_t)getAnimalDetectConfig{ + uint64_t config; + st_handle_t handle = self.handle; + st_mobile_effect_get_animal_detect_config(handle, &config); + return config; +} + +- (void)convertYUVBuffer:(unsigned char *)buffer + rgba:(GLuint)texture + size:(CGSize)size{ + if (!buffer || !glIsTexture(texture) || CGSizeEqualToSize(CGSizeZero, size)) { + NSLog(@"%s input param error", __func__); + return; + } + st_result_t ret = st_mobile_nv12_buffer_to_rgba_tex(_hConvertHandle, size.width, size.height, ST_CLOCKWISE_ROTATE_0, false, buffer, texture); + if (ret != ST_OK) { + NSLog(@"st_mobile_nv12_buffer_to_rgba_tex error %d", ret); + } +} + + +-(st_result_t)setParam:(st_effect_param_t)param andValue:(float)value { + st_result_t iRet = st_mobile_effect_set_param(self.handle, param, value); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_param error %d", iRet); + } + return iRet; +} + +-(st_result_t)setFaceMeshList:(st_mobile_face_mesh_list_t)mesh_list { + st_result_t iRet = ST_OK; + iRet = st_mobile_effect_set_face_mesh_list(self.handle, &mesh_list); + if (iRet != ST_OK) { + NSLog(@"st_mobile_effect_set_face_mesh_list error: %d", iRet); + } + return iRet; +} + +- (void)setCurrentEAGLContext:(EAGLContext*)glContext{ + if ([EAGLContext currentContext] != glContext) { + [EAGLContext setCurrentContext:glContext]; + } +} + +st_result_t _modul_state_change_callback(st_handle_t handle, const st_effect_module_info_t* p_module_info) { + if (p_module_info->type == EFFECT_MODULE_GAN_IMAGE) { // GAN +// _gan_modul_state_change_callback(handle, p_module_info); + } else if (p_module_info->type == EFFECT_MODULE_SEGMENT) { // 绿幕分割 +// _segment_modul_state_change_callback(handle, p_module_info); + } + return ST_OK; +} +#endif + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.h new file mode 100644 index 000000000..e47d11d7a --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.h @@ -0,0 +1,53 @@ +// +// EffectsAnimal.h +// SenseMeEffects +// +// Created by sunjian on 2021/7/16. +// Copyright © 2021 SoftSugar. All rights reserved. +// + +#import +#if __has_include("st_mobile_effect.h") +#import "st_mobile_common.h" +#import "st_mobile_animal.h" +#endif +#import "EffectMacro.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface EffectsAnimal : NSObject +- (instancetype)initWithType:(EffectsType)type; + +#if __has_include("st_mobile_effect.h") +/// 动物检测函数 +/// @param pixelBuffer 每帧图像数据 +/// @param rotate 手机旋转方向 +/// @param detectResult 检测结果 +- (st_result_t)detectAnimalWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + config:(st_mobile_animal_type)config + detectResult:(st_mobile_animal_result_t *)detectResult; + +/// 动物检测函数 +/// @param buffer 每帧图像数据 +/// @param rotate 手机旋转方向 +/// @param pixelFormat 视频数据格式(YUV/RGBA/BGRA......) +/// @param width 图像宽度 +/// @param height 图像高度 +/// @param stride 图像的stride +/// @param detectResult 检测结果 +- (st_result_t)detectAnimalWithBuffer:(unsigned char *)buffer + rotate:(st_rotate_type)rotate + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + config:(st_mobile_animal_type)config + detectResult:(st_mobile_animal_result_t *)detectResult; + +-(st_result_t)resetAnimalFaceTracker; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.m new file mode 100644 index 000000000..34351987d --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAnimal.m @@ -0,0 +1,130 @@ +// +// EffectsAnimal.m +// SenseMeEffects +// +// Created by sunjian on 2021/7/16. +// Copyright © 2021 SoftSugar. All rights reserved. +// + +#import "EffectsAnimal.h" +#if __has_include("st_mobile_effect.h") +#import "st_mobile_effect.h" +#endif +#import + +@interface EffectsAnimal () +{ +#if __has_include("st_mobile_effect.h") + st_handle_t _hAnimalHandle; +#endif +} +@end + +@implementation EffectsAnimal + +- (void)dealloc{ +#if __has_include("st_mobile_effect.h") + if (_hAnimalHandle) { + st_mobile_tracker_animal_face_destroy(_hAnimalHandle); + _hAnimalHandle = NULL; + } +#endif +} + +- (instancetype)initWithType:(EffectsType)type{ + if ((self = [super init])) { + dispatch_async(dispatch_get_global_queue(0, 0), ^{ + [self createHandlerWithType:type]; + }); + } + return self; +} + +- (void)createHandlerWithType:(EffectsType)type{ + NSString *catFaceModelPath = [[NSBundle mainBundle] pathForResource:@"M_SenseME_CatFace_p_3.2.0.1" ofType:@"model"]; + NSString *dogFaceModelPath = [[NSBundle mainBundle] pathForResource:@"M_SenseME_DogFace_p_2.0.0.1" ofType:@"model"]; +#if __has_include("st_mobile_effect.h") + int config = ST_MOBILE_TRACKING_MULTI_THREAD; + switch (type) { + case EffectsTypePhoto: + config = ST_MOBILE_DETECT_MODE_IMAGE; + break; + + case EffectsTypeVideo: + config = ST_MOBILE_TRACKING_SINGLE_THREAD; + break; + + default: + break; + } + st_result_t ret = st_mobile_tracker_animal_face_create(NULL, + config, + &_hAnimalHandle); + if (ret != ST_OK) { + NSLog(@"st_mobile_tracker_animal_face_create error %d", ret); + } + ret = st_mobile_tracker_animal_face_add_sub_model(_hAnimalHandle, catFaceModelPath.UTF8String); + if (ret != ST_OK) { + NSLog(@"st_mobile_tracker_animal_face_add_sub_model %@ error %d", catFaceModelPath, ret); + } + ret = st_mobile_tracker_animal_face_add_sub_model(_hAnimalHandle, dogFaceModelPath.UTF8String); + if (ret != ST_OK) { + NSLog(@"st_mobile_tracker_animal_face_add_sub_model %@ error %d", dogFaceModelPath, ret); + } +#endif +} + +#if __has_include("st_mobile_effect.h") +- (st_result_t)detectAnimalWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + config:(st_mobile_animal_type)config + detectResult:(nonnull st_mobile_animal_result_t *)detectResult{ + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char* pixelData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRow(pixelBuffer); + int iWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int iHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + st_result_t state = [self detectAnimalWithBuffer:pixelData + rotate:rotate + pixelFormat:ST_PIX_FMT_BGRA8888 + width:iWidth + height:iHeight + stride:iBytesPerRow + config:config + detectResult:detectResult + ]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return state; +} + +- (st_result_t)detectAnimalWithBuffer:(unsigned char *)buffer + rotate:(st_rotate_type)rotate + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + config:(st_mobile_animal_type)config + detectResult:(st_mobile_animal_result_t *)detectResult { + EFFECTSTIMELOG(key) + st_result_t iRet = st_mobile_tracker_animal_face_track(_hAnimalHandle, + buffer, + pixelFormat, + width, + height, + stride, + rotate, + config, + detectResult); + EFFECTSTIMEPRINT(key, "st_mobile_tracker_animal_face_track"); + return iRet; +} + +-(st_result_t)resetAnimalFaceTracker { + st_result_t iRet; + if (_hAnimalHandle) { + iRet = st_mobile_tracker_animal_face_reset(_hAnimalHandle); + } + return iRet; +} +#endif +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.h new file mode 100644 index 000000000..8b5131c92 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.h @@ -0,0 +1,48 @@ +// +// EffectsAttribute.h +// SenseMeEffects +// +// Created by sunjian on 2021/7/16. +// Copyright © 2021 SoftSugar. All rights reserved. +// + +#import +#if __has_include("st_mobile_common.h") +#import "st_mobile_common.h" +#import "st_mobile_human_action.h" +#import "st_mobile_face_attribute.h" +#endif +#import "EffectMacro.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface EffectsAttribute : NSObject + +#if __has_include("st_mobile_common.h") +- (st_result_t)detectAttributeWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + detectResult:(st_mobile_human_action_t)detectResult + attrArray:(st_mobile_attributes_t *)pAttrArray; + +- (st_result_t)detectAttributeWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + detectResult:(st_mobile_human_action_t)detectResult + attrArray:(st_mobile_attributes_t *)pAttrArray withGenderCallback:(void(^)(BOOL isMale))callback; + +- (st_result_t)detectAttributeWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + faces:(st_mobile_106_t *)faces + attrArray:(st_mobile_attributes_t *)pAttrArray; + +- (st_result_t)detectAttributeWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + faces:(st_mobile_106_t *)faces + attrArray:(st_mobile_attributes_t *)pAttrArray withGenderCallback:(void(^)(BOOL isMale))callback; +#endif +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.m new file mode 100644 index 000000000..29dac0d45 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsAttribute.m @@ -0,0 +1,213 @@ +// +// EffectsAttribute.m +// SenseMeEffects +// +// Created by sunjian on 2021/7/16. +// Copyright © 2021 SoftSugar. All rights reserved. +// + +#import "EffectsAttribute.h" +#import + +@interface EffectsAttribute () +{ +#if __has_include("st_mobile_common.h") + st_handle_t _hAttributeHandle; +#endif +} +@end + +@implementation EffectsAttribute +- (void)dealloc{ +#if __has_include("st_mobile_common.h") + if (_hAttributeHandle) { + st_mobile_face_attribute_destroy(_hAttributeHandle); + } +#endif +} + +- (instancetype)init{ + if ((self = [super init])) { + [self createHandler]; + } + return self; +} + +- (void)createHandler{ + NSString *strAttriModelPath = [[NSBundle mainBundle] pathForResource:@"M_SenseME_Attribute_p_1.2.8.1" ofType:@"model"]; +#if __has_include("st_mobile_common.h") + st_result_t ret = st_mobile_face_attribute_create(strAttriModelPath.UTF8String, &_hAttributeHandle); + if (ret != ST_OK) { + NSLog(@"st_mobile_face_attribute_create error %d", ret); + } +#endif +} + +#if __has_include("st_mobile_common.h") +- (st_result_t)detectAttributeWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + detectResult:(st_mobile_human_action_t)detectResult + attrArray:(st_mobile_attributes_t *)pAttrArray{ + if (detectResult.face_count == 0) return ST_E_INVALIDARG; + st_mobile_106_t *faces = &detectResult.p_faces[0].face106; + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char* pixelData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRow(pixelBuffer); + int iWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int iHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + st_result_t ret = [self detectAttributeWithBuffer:pixelData + pixelFormat:ST_PIX_FMT_BGRA8888 + width:iWidth + height:iHeight + stride:iBytesPerRow + faces:faces + attrArray:pAttrArray]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return ret; +} + +- (st_result_t)detectAttributeWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + faces:(st_mobile_106_t *)faces + attrArray:(st_mobile_attributes_t *)pAttrArray{ + EFFECTSTIMELOG(key) + st_result_t iRet = st_mobile_face_attribute_detect(_hAttributeHandle, + buffer, + pixelFormat, + width, + height, + stride, + faces, + 1, // 这里仅取一张脸也就是第一张脸的属性作为演示 + &pAttrArray); + EFFECTSTIMEPRINT(key, "st_mobile_face_attribute_detect") + st_mobile_attributes_t attributeDisplay = pAttrArray[0]; + NSString *strAttrDescription = [self getDescriptionOfAttribute:attributeDisplay]; + NSLog(@"@@@ %@", strAttrDescription); + return iRet; +} + + +- (NSString *)getDescriptionOfAttribute:(st_mobile_attributes_t)attribute { + NSString *strAge , *strGender , *strAttricative = nil; + + for (int i = 0; i < attribute.attribute_count; i ++) { + + // 读取一条属性 + st_mobile_attribute_t attributeOne = attribute.p_attributes[i]; + + // 获取属性类别 + const char *attr_category = attributeOne.category; + const char *attr_label = attributeOne.label; + + // 年龄 + if (0 == strcmp(attr_category, "age")) { + + strAge = [NSString stringWithUTF8String:attr_label]; + } + + // 颜值 + if (0 == strcmp(attr_category, "attractive")) { + + strAttricative = [NSString stringWithUTF8String:attr_label]; + } + + // 性别 + if (0 == strcmp(attr_category, "gender")) { + + if (0 == strcmp(attr_label, "male") ) { + + strGender = @"男"; + } + + if (0 == strcmp(attr_label, "female") ) { + + strGender = @"女"; + } + } + } + + NSString *strAttrDescription = [NSString stringWithFormat:@"颜值:%@ 性别:%@ 年龄:%@" , strAttricative , strGender , strAge]; + + return strAttrDescription; +} + +- (st_result_t)detectAttributeWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + detectResult:(st_mobile_human_action_t)detectResult + attrArray:(st_mobile_attributes_t *)pAttrArray withGenderCallback:(void(^)(BOOL isMale))callback { + if (detectResult.face_count == 0) return ST_E_INVALIDARG; + st_mobile_106_t *faces = &detectResult.p_faces[0].face106; + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char* pixelData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRow(pixelBuffer); + int iWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int iHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + st_result_t ret = [self detectAttributeWithBuffer:pixelData + pixelFormat:ST_PIX_FMT_BGRA8888 + width:iWidth + height:iHeight + stride:iBytesPerRow + faces:faces + attrArray:pAttrArray withGenderCallback:callback]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return ret; +} + +- (st_result_t)detectAttributeWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + faces:(st_mobile_106_t *)faces + attrArray:(st_mobile_attributes_t *)pAttrArray withGenderCallback:(void(^)(BOOL isMale))callback { + if (!_hAttributeHandle) { + if (callback) { + callback(NO); + } + return ST_E_HANDLE; + } + EFFECTSTIMELOG(key) + st_result_t iRet = st_mobile_face_attribute_detect(_hAttributeHandle, + buffer, + pixelFormat, + width, + height, + stride, + faces, + 1, // 这里仅取一张脸也就是第一张脸的属性作为演示 + &pAttrArray); + if (iRet != ST_OK) { + NSLog(@"st_mobile_face_attribute_detect error %d", iRet); + } + EFFECTSTIMEPRINT(key, "st_mobile_face_attribute_detect") + st_mobile_attributes_t attributeDisplay = pAttrArray[0]; + [self getDescriptionOfAttribute:attributeDisplay withGenderCallback:callback]; + return iRet; +} + +- (void)getDescriptionOfAttribute:(st_mobile_attributes_t)attribute withGenderCallback:(void(^)(BOOL isMale))callback { + if (callback) { + for (int i = 0; i < attribute.attribute_count; i ++) { + + // 读取一条属性 + st_mobile_attribute_t attributeOne = attribute.p_attributes[i]; + + // 获取属性类别 + const char *attr_category = attributeOne.category; + const char *attr_label = attributeOne.label; + // 性别 + if (0 == strcmp(attr_category, "gender")) { + + if (0 == strcmp(attr_label, "male") ) { + callback(YES); + } else if (0 == strcmp(attr_label, "female") ) { + callback(NO); + } + } + } + } +} +#endif +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.h new file mode 100644 index 000000000..f39154ac5 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.h @@ -0,0 +1,64 @@ +// +// EffectsTracker.h +// SenseMeEffects +// +// Created by sunjian on 2021/7/19. +// Copyright © 2021 SenseTime. All rights reserved. +// + +#import +#if __has_include("st_mobile_common.h") +#import "st_mobile_common.h" +#import "st_mobile_object.h" +#endif +#import "EffectMacro.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface EffectsCommonObject : NSObject + +#if __has_include("st_mobile_common.h") +- (void)setObjectRect:(st_rect_t)rect; + +/// 通用物体跟踪 +/// @param pixelBuffer 每帧图像数据 +/// @param rect 通用物体位置 +- (st_result_t)detectObjectWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + rect:(st_rect_t*)rect + score:(float*)score; + +/// 设置通用物体跟踪 +/// @param buffer 每帧图像数据 +/// @param pixelFormat 视频数据格式(YUV/RGBA/BGRA......) +/// @param width 图像宽度 +/// @param height 图像高度 +/// @param stride 图像的stride +/// @param rect 通用物体位置 +- (st_result_t)setObjectWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + rect:(st_rect_t*)rect; + +/// 通用物体跟踪 +/// @param buffer 每帧图像数据 +/// @param pixelFormat 视频数据格式(YUV/RGBA/BGRA......) +/// @param width 图像宽度 +/// @param height 图像高度 +/// @param stride 图像的stride +/// @param rect 通用物体位置 +- (st_result_t)detectObjectWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + rect:(st_rect_t*)rect + score:(float *)result_score; + + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.m new file mode 100644 index 000000000..70bb30e08 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsCommonObject.m @@ -0,0 +1,132 @@ +// +// EffectsTracker.m +// SenseMeEffects +// +// Created by sunjian on 2021/7/19. +// Copyright © 2021 SenseTime. All rights reserved. +// + +#import "EffectsCommonObject.h" + +@interface EffectsCommonObject () +{ +#if __has_include("st_mobile_common.h") + st_handle_t _hTrackerHandle; + st_rect_t _rect; +#endif + BOOL _bTracking; +} +@end + +@implementation EffectsCommonObject +- (void)dealloc{ +#if __has_include("st_mobile_common.h") + if (_hTrackerHandle) { + st_mobile_object_tracker_destroy(_hTrackerHandle); + _hTrackerHandle = NULL; + } +#endif +} + +- (instancetype)init{ + if ((self = [super init])) { + [self createHandler]; + } + return self; +} + +- (void)createHandler{ +#if __has_include("st_mobile_common.h") + st_result_t ret = st_mobile_object_tracker_create(&_hTrackerHandle); + if (ret != ST_OK) { + NSLog(@"st_mobile_object_tracker_create error %d", ret); + } +#endif +} + + +#if __has_include("st_mobile_common.h") +- (void)setObjectRect:(st_rect_t)rect{ + _rect = rect; + _bTracking = YES; +} +- (st_result_t)detectObjectWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + rect:(st_rect_t*)rect + score:(float*)score +{ + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char* pixelData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRow(pixelBuffer); + int iWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int iHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + st_result_t state = ST_OK; + if (_bTracking) { + [self setObjectWithBuffer:pixelData + pixelFormat:ST_PIX_FMT_BGRA8888 + width:iWidth + height:iHeight + stride:iBytesPerRow + rect:&_rect]; + _bTracking = NO; + } + [self detectObjectWithBuffer:pixelData + pixelFormat:ST_PIX_FMT_BGRA8888 + width:iWidth + height:iHeight + stride:iBytesPerRow + rect:rect + score:score]; + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return state; +} + + +- (st_result_t)setObjectWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + rect:(st_rect_t*)rect +{ + st_result_t iRet = st_mobile_object_tracker_set_target(_hTrackerHandle, + buffer, + pixelFormat, + width, + height, + stride, + rect); + return iRet; +} + +- (st_result_t)detectObjectWithBuffer:(unsigned char *)buffer + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + rect:(st_rect_t *)rect + score:(nonnull float *)result_score +{ + if (_bTracking) { + [self setObjectWithBuffer:buffer + pixelFormat:pixelFormat + width:width + height:height + stride:stride + rect:&_rect]; + _bTracking = NO; + } + EFFECTSTIMELOG(key) + st_result_t iRet = st_mobile_object_tracker_track(_hTrackerHandle, + buffer, + pixelFormat, + width, + height, + stride, + rect, + result_score); + EFFECTSTIMEPRINT(key, "st_mobile_object_tracker_track"); + return iRet; +} +#endif +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.h new file mode 100644 index 000000000..06fd3b528 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.h @@ -0,0 +1,76 @@ +// +// EffectsCommon.h +// Effects +// +// Created by sunjian on 2021/5/8. +// Copyright © 2021 sjuinan. All rights reserved. +// + +#import +#if __has_include("st_mobile_common.h") +#import "st_mobile_common.h" +#import "st_mobile_human_action.h" +#endif +#import "EffectMacro.h" + +@interface EffectsDetector : NSObject + +#if __has_include("st_mobile_common.h") + +/// 初始化对象 +/// @param type 类型 +- (instancetype)initWithType:(EffectsType)type; + +/// 添加model +/// @param modelPath 模型路径 +- (st_result_t)setModelPath:(NSString *)modelPath; +- (st_result_t)setModelPath:(NSString *)modelPath withFirstPhaseFinished:(void(^)(void))finishedCallback; + +- (st_result_t)setParam:(st_human_action_param_type)type andValue:(float)value; + +/// 人脸检测函数 +/// @param pixelBuffer 每帧图像数据 +/// @param detect_config 检测配置 +/// @param rotate 手机旋转方向 +/// @param detectResult 检测结果 +- (st_result_t)detectHumanActionWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + config:(unsigned long long)detect_config + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t *)detectResult; + +/// 人脸检测函数 +/// @param buffer 每帧图像数据 +/// @param bufferSize 图像数据大小 +/// @param detect_config 检测配置 +/// @param rotate 手机旋转方向 +/// @param pixelFormat 视频数据格式(YUV/RGBA/BGRA......) +/// @param width 图像宽度 +/// @param height 图像高度 +/// @param stride 图像的stride +/// @param detectResult 检测结果 +- (st_result_t)detectHumanActionWithBuffer:(unsigned char *)buffer + size:(int)bufferSize + config:(unsigned long long)detect_config + rotate:(st_rotate_type)rotate + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + detectResult:(st_mobile_human_action_t *)detectResult; + + + +///人脸检测 +- (st_mobile_human_action_t)detectHumanActionWithPixelBuffer:(CVPixelBufferRef)pixelBuffer; + +///检测脸型 +- (st_face_shape_t)detectFaceShape:(st_mobile_face_t)p_face; + +-(st_result_t)resetHumanAction; + +-(st_result_t)getMeshList:(st_mobile_face_mesh_list_t *)p_mesh; +-(st_result_t)getMeshInfo:(st_mobile_mesh_info_t *)mesh_info; + +#endif + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.m new file mode 100644 index 000000000..231763e8e --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsDetector.m @@ -0,0 +1,282 @@ +// +// EffectsCommon.m +// Effects +// +// Created by sunjian on 2021/5/8. +// Copyright © 2021 sjuinan. All rights reserved. +// + +#import +#import "EffectsDetector.h" +#if __has_include("st_mobile_common.h") +#import "st_mobile_human_action.h" +#endif +#import +#import "EFGlobalSingleton.h" + +@interface EffectsDetector () +{ +#if __has_include("st_mobile_common.h") + st_handle_t _hDetector; +#endif +} +@end + +@implementation EffectsDetector + +- (void)dealloc{ +#if __has_include("st_mobile_common.h") + if (_hDetector) { + st_mobile_human_action_destroy(_hDetector); + } +#endif +} + +- (instancetype)initWithType:(EffectsType)type{ + if ((self = [super init])) { + [self createHandlerWithType:type]; + } + return self; +} + + +- (void)createHandlerWithType:(EffectsType)type{ +#if __has_include("st_mobile_common.h") + if (!_hDetector) { + int config = ST_MOBILE_TRACKING_MULTI_THREAD; + switch (type) { + case EffectsTypePhoto: + config = ST_MOBILE_DETECT_MODE_IMAGE; + break; + + case EffectsTypeVideo: + config = ST_MOBILE_TRACKING_SINGLE_THREAD; + break; + + default: + break; + } + st_result_t ret = st_mobile_human_action_create(NULL, config, &_hDetector); + if (ret != ST_OK) { + NSLog(@"st_mobile_human_action_create error %d", ret); + } + + ret = st_mobile_human_action_setparam(_hDetector, ST_HUMAN_ACTION_PARAM_MESH_MODE, ST_MOBILE_MESH_PART_FACE|ST_MOBILE_MESH_PART_EYE|ST_MOBILE_MESH_PART_MOUTH|ST_MOBILE_MESH_PART_SKULL|ST_MOBILE_MESH_PART_EAR|ST_MOBILE_MESH_PART_NECK|ST_MOBILE_MESH_PART_EYEBROW); + + if (ret != ST_OK) { + NSLog(@"st_mobile_human_action_setparam error %d", ret); + } + + ret = st_mobile_human_action_setparam(_hDetector, ST_HUMAN_ACTION_PARAM_BACKGROUND_SEGMENT_USE_TEMPERATURE, 0.0); + if (ret != ST_OK) { + NSLog(@"st_mobile_human_action_setparam error %d", ret); + } + + ret = st_mobile_human_action_setparam(_hDetector, ST_HUMAN_ACTION_PARAM_HEAD_SEGMENT_INSTANCE, 1.0); + if (ret != ST_OK) { + NSLog(@"st_mobile_human_action_setparam error %d", ret); + } + + ret = st_mobile_human_action_setparam(_hDetector, ST_HUMAN_ACTION_PARAM_FACE_MESH_OUTPUT_FORMAT, ST_3D_WORLD_COORDINATE); + if (ret != ST_OK) { + NSLog(@"st_mobile_human_action_setparam error %d", ret); + } + } +#endif +} + +#if __has_include("st_mobile_common.h") +st_result_t addSubModel(st_handle_t handle, NSString* file) { + st_result_t iRet = st_mobile_human_action_add_sub_model(handle, file.UTF8String); + if (iRet != ST_OK) { + NSLog(@"st mobile human action add %@ model failed: %d", file, iRet); + } + return iRet; +} + + +- (st_result_t)setModelPath:(NSString *)modelPath{ + + st_result_t state = ST_OK; + + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:modelPath error:nil]; +// NSTimeInterval last = CFAbsoluteTimeGetCurrent(); + for(NSString *file in files) { +// if ([file containsString:@"M_SenseME_Face_"] || [file containsString:@"Occlusion"] || [file containsString:@"M_SenseME_3DMesh_Face2396pt"]) { + NSString *fullPath = [modelPath stringByAppendingPathComponent:file]; + + state = st_mobile_human_action_add_sub_model(_hDetector, fullPath.UTF8String); + if ([fullPath containsString:@"Skin"]) { // 皮肤分割 + [EFGlobalSingleton sharedInstance].efHasSegmentCapability = state != ST_E_NO_CAPABILITY; + } + if (state != ST_OK) { + NSLog(@"st mobile human action add %@ model failed: %d", fullPath, state); + } +// } + } +// NSLog(@"@mahaomeng cost %f", CFAbsoluteTimeGetCurrent()-last); + return state; +} + +- (st_result_t)setModelPath:(NSString *)modelPath withFirstPhaseFinished:(void(^)(void))finishedCallback { + st_result_t state = ST_OK; + + NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:modelPath error:nil]; +// NSTimeInterval last = CFAbsoluteTimeGetCurrent(); + for(NSString *file in files) { + if ([file containsString:@"M_SenseME_Face_"] || [file containsString:@"Occlusion"] || [file containsString:@"M_SenseME_3DMesh_Face2396pt"]) { + NSString *fullPath = [modelPath stringByAppendingPathComponent:file]; + + state = st_mobile_human_action_add_sub_model(_hDetector, fullPath.UTF8String); + if ([fullPath containsString:@"Skin"]) { // 皮肤分割 + [EFGlobalSingleton sharedInstance].efHasSegmentCapability = state != ST_E_NO_CAPABILITY; + } + if (state != ST_OK) { + NSLog(@"st mobile human action add %@ model failed: %d", fullPath, state); + } + } + } + if (finishedCallback) { + finishedCallback(); + } + for(NSString *file in files) { + if ([file containsString:@"M_SenseME_Face_"] || [file containsString:@"Occlusion"] || [file containsString:@"M_SenseME_3DMesh_Face2396pt"]) { + continue; + } + NSString *fullPath = [modelPath stringByAppendingPathComponent:file]; + + state = st_mobile_human_action_add_sub_model(_hDetector, fullPath.UTF8String); + if ([fullPath containsString:@"Skin"]) { // 皮肤分割 + [EFGlobalSingleton sharedInstance].efHasSegmentCapability = state != ST_E_NO_CAPABILITY; + } + if (state != ST_OK) { + NSLog(@"st mobile human action add %@ model failed: %d", fullPath, state); + } + } + + return state; +} + +-(st_result_t)setParam:(st_human_action_param_type)type andValue:(float)value { + st_result_t iRet = st_mobile_human_action_setparam(_hDetector, type, value); + if (iRet != ST_OK) { + NSLog(@"st_mobile_human_action_setparam error %d", iRet); + } + return iRet; +} + +- (st_result_t)detectHumanActionWithPixelbuffer:(CVPixelBufferRef)pixelBuffer + config:(unsigned long long)detectConfig + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t *)detectResult{ + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char* pixelData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + int pixelDataSize = (int)CVPixelBufferGetDataSize(pixelBuffer); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRow(pixelBuffer); + int iWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int iHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + st_result_t state = [self detectHumanActionWithBuffer:pixelData + size:pixelDataSize + config:detectConfig + rotate:rotate + pixelFormat:ST_PIX_FMT_BGRA8888 + width:iWidth + height:iHeight + stride:iBytesPerRow + detectResult:detectResult]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return state; +} + +- (st_result_t)detectHumanActionWithBuffer:(unsigned char *)buffer + size:(int)bufferSize + config:(unsigned long long)detect_config + rotate:(st_rotate_type)rotate + pixelFormat:(st_pixel_format)pixelFormat + width:(int)width + height:(int)height + stride:(int)stride + detectResult:(st_mobile_human_action_t *)detectResult{ + EFFECTSTIMELOG(key) + st_result_t iRet = ST_OK; + if (detect_config) { + iRet = st_mobile_human_action_detect(_hDetector, + buffer, + pixelFormat, + width, + height, + stride, + rotate, + detect_config, + detectResult); + } + EFFECTSTIMEPRINT(key, "st_mobile_human_action_detect"); + return iRet; +} + +- (st_mobile_human_action_t)detectHumanActionWithPixelBuffer:(CVPixelBufferRef)pixelBuffer { + + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char* pBGRAImageIn = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + int iWidth = (int)CVPixelBufferGetWidth(pixelBuffer); + int iHeight = (int)CVPixelBufferGetHeight(pixelBuffer); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRow(pixelBuffer); + st_mobile_human_action_t detectResult; + memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); + st_result_t iRet = ST_OK; + EFFECTSTIMELOG(key) + iRet = st_mobile_human_action_detect(_hDetector, + pBGRAImageIn, + ST_PIX_FMT_BGRA8888, + iWidth, + iHeight, + iBytesPerRow, + ST_CLOCKWISE_ROTATE_0, + ST_MOBILE_FACE_DETECT , + &detectResult); + EFFECTSTIMEPRINT(key, "st_mobile_human_action_detect"); + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return detectResult; +} + + +///检测脸型 +- (st_face_shape_t)detectFaceShape:(st_mobile_face_t)p_face { + + st_face_shape_t shape_t = ST_FACE_SHAPE_UNKNOWN; + st_result_t iRet = st_mobile_human_action_get_face_shape(_hDetector, &p_face, &shape_t); + if (iRet != ST_OK) { + NSLog(@"human action get face shape failed %d", iRet); + } + return shape_t; +} + +-(st_result_t)getMeshList:(st_mobile_face_mesh_list_t *)p_mesh { + st_result_t state; + st_mobile_mesh_info_t mesh_info; + state = st_mobile_human_action_get_mesh_info(_hDetector, ST_MOBILE_FACE_MESH, &mesh_info); + + if (state != ST_OK) { + NSLog(@"st_mobile_human_action_get_mesh_list failed"); + } + return state; +} + +-(st_result_t)getMeshInfo:(st_mobile_mesh_info_t *)mesh_info { + st_result_t state; + state = st_mobile_human_action_get_mesh_info(_hDetector, ST_MOBILE_FACE_MESH, mesh_info); + if (state != ST_OK) { + NSLog(@"st_mobile_human_action_get_mesh_info failed"); + } + return state; +} + +-(st_result_t)resetHumanAction { + if (_hDetector) { + return st_mobile_human_action_reset(_hDetector); + } + return -2; +} + +#endif +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.h new file mode 100644 index 000000000..b6a9dee35 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.h @@ -0,0 +1,24 @@ +// +// License.h +// Effects +// +// Created by sunjian on 2021/5/8. +// Copyright © 2021 sjuinan. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface EffectsLicense : NSObject + +/// 鉴权 +/// @param licensePath 授权文件路径 ++ (BOOL)authorizeWithLicensePath:(NSString *)licensePath; + +/// 鉴权 +/// @param dataLicense 授权文件数据 ++ (BOOL)authorizeWithLicenseData:(NSData *)dataLicense; +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.m new file mode 100644 index 000000000..e9794d3aa --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsLicense.m @@ -0,0 +1,76 @@ +// +// License.m +// Effects +// +// Created by sunjian on 2021/5/8. +// Copyright © 2021 sjuinan. All rights reserved. +// + +#import "EffectsLicense.h" +#if __has_include("st_mobile_common.h") +#import "st_mobile_common.h" +#import "st_mobile_license.h" +#endif + +@interface EffectsLicense () +@end + +@implementation EffectsLicense + +static NSString *strActiveCodeKey = @"EFFECTS_ACTIVE_CODE"; + ++ (NSString *)getActiveCodeWithData:(NSData *)dataLicense{ +#if __has_include("st_mobile_common.h") + st_result_t iRet = ST_E_FAIL; + char active_code[10240]; + int active_code_len = 10240; + iRet = st_mobile_generate_activecode_from_buffer([dataLicense bytes], + (int)[dataLicense length], + active_code, + &active_code_len); + if (iRet != ST_OK) { + return nil; + } + NSString * strActiveCode = [[NSString alloc] initWithUTF8String:active_code]; + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + if (strActiveCode.length) { + [userDefaults setObject:strActiveCode forKey:strActiveCodeKey]; + [userDefaults synchronize]; + } + return strActiveCode; +#else + return nil; +#endif +} + ++ (BOOL)authorizeWithLicensePath:(NSString *)licensePath{ + NSData * dataLicense = [NSData dataWithContentsOfFile:licensePath]; + return [self authorizeWithLicenseData:dataLicense]; +} + ++ (BOOL)authorizeWithLicenseData:(NSData *)dataLicense{ + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + NSString *strActiveCode = [userDefaults objectForKey:strActiveCodeKey]; + if (strActiveCode.length) { +#if __has_include("st_mobile_common.h") + st_result_t iRet = ST_E_FAIL; + iRet = st_mobile_check_activecode_from_buffer( + [dataLicense bytes], + (int)[dataLicense length], + strActiveCode.UTF8String, + (int)[strActiveCode length] + ); + + if (iRet == ST_OK) { + return YES; + } +#endif + } + strActiveCode = [self getActiveCodeWithData:dataLicense]; + if (strActiveCode) { + return YES; + } + return NO; +} + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.h new file mode 100644 index 000000000..edb1efaf3 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.h @@ -0,0 +1,260 @@ +// +// Effects.h +// Effects +// +// Created by sunjian on 2021/5/8. +// Copyright © 2021 sjuinan. All rights reserved. +// + +#import +#import +#if __has_include("st_mobile_common.h") +#import "EffectsDetector.h" +#import "st_mobile_common.h" +#import "st_mobile_effect.h" +#endif +#import "EffectMacro.h" + +@protocol EFEffectsProcessDelegate + +- (void)updateEffectsFacePoint:(CGPoint)point; +#if __has_include("st_mobile_common.h") +- (void)updateCommonObjectPosition:(st_rect_t)rect; +#endif +- (void)updateKeyPoinst:(NSArray *)keyPoints; + +@end + +@interface EffectsProcess : NSObject + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, assign) GLuint inputTexture; + +@property (nonatomic, assign) GLuint outputTexture; + +@property (nonatomic) CVPixelBufferRef outputPixelBuffer; + +@property (nonatomic, assign) uint64_t detectConfig; + +@property (nonatomic, assign) EFDetectConfigMode configMode; + +/// 鉴权 +/// @param licensePath 授权文件路径 ++ (BOOL)authorizeWithLicensePath:(NSString *)licensePath; + +/// 鉴权 +/// @param licenseData 授权文件数据 ++ (BOOL)authorizeWithLicenseData:(NSData *)licenseData; + +/// 初始化对象 +/// @param type 类型 +- (instancetype)initWithType:(EffectsType)type glContext:(EAGLContext *)glContext; + +#if __has_include("st_mobile_common.h") +/// 加载模型 +/// @param modelPath 模型文件路径(可将多个模型放在一个文件中,SDK内部遍历加载Model) +- (st_result_t)setModelPath:(NSString *)modelPath; +- (st_result_t)setModelPath:(NSString *)modelPath withFirstPhaseFinished:(void(^)(void))finishedCallback; + +/// 设置特效 +/// @param type 特效类型 +/// @param path 特效素材路径 +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type path:(NSString *)path; + +/// 设置特效 +/// @param type 特效类型 +/// @param model 特效素材model +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type model:(int)model; + + +/// 设置特效强度值 +/// @param type 特效类型 +/// @param value 强度值 +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type value:(float)value; + + +/// 设置风格强度 +/// @param packageId packageID +/// @param type 类型 +/// @param value 强度值 +- (st_result_t)setPackageId:(int)packageId groupType:(st_effect_beauty_group_t)type strength:(float)value; + +- (st_result_t)setTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType; + +- (st_result_t)getTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType; + +- (st_result_t)setBeautyParam:(st_effect_beauty_param_t)param andVal:(float)val; + +- (st_result_t)getBeautyParam:(st_effect_beauty_param_t)param andVal:(float *)val; + +- (st_result_t)get3dBeautyPartsSize:(int *)partSize; + +- (st_result_t)get3dBeautyParts:(st_effect_3D_beauty_part_info_t[])parts fromSize:(int)partSize; + +- (st_result_t)set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t *)parts andVal:(int)partSize; +- (st_result_t)f_set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t[])parts andVal:(int)partSize; + +- (st_result_t)disableOverlap:(BOOL)isDisableOverlap; +- (st_result_t)disableModuleReorder:(BOOL)isDisableModuleReorder; + +/// 设置贴纸 +/// @param stickerPath stickerPath 贴纸资源路径 +/// @param callback 回调Block +- (void)setStickerWithPath:(NSString *)stickerPath + callBack:(void(^)(st_result_t state, int stickerId, uint64_t action))callback; + +- (void)setStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback; + +/// 添加贴纸 +/// @param stickerPath stickerPath 贴纸资源路径 +/// @param callback 回调Block +- (void)addStickerWithPath:(NSString *)stickerPath + callBack:(void(^)(st_result_t state, int sticker, uint64_t action))callback; + +/// 重新播放贴纸 +/// @param packageId packageId +-(void)replayStickerWithPackage:(int)packageId; + +- (void)addStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback; + +-(void)changeStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback; + +/// 获取获取素材的贴纸信息 +/// @param package_id package_id +/// @param modules 贴纸信息 +-(st_result_t)getModulesInPackage:(int)package_id modules:(st_effect_module_info_t*)modules; + +/// 设置贴纸信息 +/// @param module_info 贴纸信息 +-(st_result_t)setModuleInfo:(st_effect_module_info_t *)module_info; + +/// 获取覆盖生效的美颜的信息 +/// @param callback 回调block +- (void)getOverLap:(void(^)(st_effect_beauty_info_t *beauty_info))callback; + +/// 获取覆盖生效的美颜的信息 +/// If there is st_effect_beauty_info_t, release it when used it; +- (st_effect_beauty_info_t *)getOverlapInfo:(int *)count; + +/// 移除贴纸 +/// @param stickerId 贴纸Id +- (st_result_t)removeSticker:(int)stickerId; + +/// 清空贴纸 +- (st_result_t)cleareStickers; + +- (st_result_t)detectWithPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + cameraPosition:(AVCaptureDevicePosition)position + humanAction:(st_mobile_human_action_t *)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult; + +-(st_result_t)resetHumanAction; + +-(st_result_t)setHumanActionParam:(st_human_action_param_type)type andValue:(float)value; +-(st_result_t)setEffectParam:(st_effect_param_t)param andValue:(float)value; + +- (st_result_t)renderPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + humanAction:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)img_out; + +-(st_result_t)detectAttribute:(unsigned char *)imageData pixelFormat:(st_pixel_format)pixel_format imageWidth:(int)image_width imageHeight:(int)image_height imageStride:(int)image_stride orientation:(st_rotate_type)orientation withGenderCallback:(void(^)(BOOL isMale))callback; + +-(st_result_t)detectAttribute:(unsigned char *)imageData pixelFormat:(st_pixel_format)pixel_format imageWidth:(int)image_width imageHeight:(int)image_height detectResult:(st_mobile_human_action_t)detectResult withGenderCallback:(void(^)(BOOL isMale))callback; + +/// 处理视频数据 +/// @param pixelBuffer 视频数据 +/// @param rotate 当前手机的旋转方向 +/// @param position 当前手机摄像头方向 +/// @param outTexture 目标纹理, 仅支持RGBA纹理 +/// @param fmt_out 输出图片的类型,支持NV21,BGR,BGRA,NV12,RGBA,YUV420P格式 +/// @param img_out 输出图像数据数组,需要用户分配内存,如果是null, 不输出buffer +- (st_result_t)processPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + cameraPosition:(AVCaptureDevicePosition)position + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)img_out; + +/// 处理图像数据 +/// @param data 图像数据 +/// @param dataSize 图像数据大小 +/// @param width 图像宽度 +/// @param height 图像高度 +/// @param stride 图像数据跨度 +/// @param rotate 手机旋转方向 +/// @param pixelFormat 图像格式 +/// @param outTexture 输出纹理,仅支持RGBA纹理 +/// @param fmt_out 输出图片的类型,支持NV21,BGR,BGRA,NV12,RGBA,YUV420P格式 +/// @param outData 输出数据 +- (st_result_t)processData:(unsigned char *)data + size:(int)dataSize + width:(int)width + height:(int)height + stride:(int)stride + rotate:(st_rotate_type)rotate + pixelFormat:(st_pixel_format)pixelFormat + inputTexture:(GLuint)inputTexture + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)outData; + + +/// 创建一个纹理 +/// @param width 纹理宽度 +/// @param height 纹理高度 +- (GLuint)createTextureWidth:(int)width height:(int)height; + +/// 创建OpenGL对象 +/// @param width 像素宽 +/// @param height 像素高 +/// @param texture 纹理 +/// @param pixelBuffer 像素缓存 +- (void)createGLObjectWith:(int)width + height:(int)height + texture:(GLuint *)texture + pixelBuffer:(CVPixelBufferRef *)pixelBuffer + cvTexture:(CVOpenGLESTextureRef *)cvTexture; + +- (id)createMetalTextureWithDevice:(id)metalDevice width:(int)width height:(int)height pixelBuffer:(CVPixelBufferRef)pixelBuffer; + +/// 删除一个纹理 +/// @param texture 纹理 +- (void)deleteTexture:(GLuint *)texture + pixelBuffer:(CVPixelBufferRef *)pixelBuffer + cvTexture:(CVOpenGLESTextureRef *)cvTexture;; + +/// 获取纹理 +/// @param pixelBuffer 输入pixelBuffer +- (GLuint)getTexutreWithPixelBuffer:(CVPixelBufferRef)pixelBuffer; + + + +/// 人脸检测 +/// @param pixelBuffer 像素缓存 +/// @ [out] 返回检测结果值 +- (st_mobile_human_action_t)detectHumanActionWithPixelBuffer:(CVPixelBufferRef)pixelBuffer; + + +/// 获取人脸形状 +/// @param p_face 检测到的人脸信息 +/// @out p_face_shape 人脸形状 +- (st_face_shape_t)detectFaceShape:(st_mobile_face_t)p_face; + +/// 设置通用物体位置 +/// @param rect 物体方框 +- (void)setObjectTrackRect:(st_rect_t)rect; + +#endif + +/// 是否鉴权成功 +- (BOOL)isAuthrized; + +-(void)getMeshList; + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.m new file mode 100644 index 000000000..fcd4ac08b --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/EffectsProcess.m @@ -0,0 +1,1264 @@ +// +// Effects.m +// Effects +// +// Created by sunjian on 2021/5/8. +// Copyright © 2021 sjuinan. All rights reserved. +// + +#import "EffectsProcess.h" +#import "EffectsLicense.h" +#import "EffectsDetector.h" +#import +#import +#import "Effects.h" +#import "EffectsAnimal.h" +#import "EffectsAttribute.h" +#import "EffectsCommonObject.h" + +#define ENABLE_ATTRIBUTE 0 + +@interface EffectsToken : NSObject ++ (instancetype)sharedInstance; +@property (nonatomic, assign) BOOL bAuthrize; +@end +@implementation EffectsToken ++ (instancetype)sharedInstance{ + static dispatch_once_t onceToken; + static id instance = nil; + dispatch_once(&onceToken, ^{ + instance = [[EffectsToken alloc] init]; + }); + return instance; +} +@end + +@interface EffectsProcess () +{ + CVOpenGLESTextureCacheRef _cvTextureCache; + uint64_t _effectsProcess; + float _result_score; + BOOL _bObject; + + float _scale; + float _margin; + + int _width, _height; + +} +@property (nonatomic, strong) EAGLContext *glContext; +@property (nonatomic, strong) EffectsDetector *detector; +@property (nonatomic, strong) EffectsAnimal *animalDetect; +@property (nonatomic, strong) EffectsAttribute *attriDetect; +@property (nonatomic, strong) EffectsCommonObject *commonObject; +@property (nonatomic, strong) Effects *effect; +@property (nonatomic) AVCaptureDevicePosition cameraPosition; +@property (nonatomic, strong) dispatch_queue_t renderQueue; +@end + + +@implementation EffectsProcess + +- (void)dealloc{ + [self setCurrentEAGLContext:self.glContext]; + if (_inputTexture) { + glDeleteTextures(1, &_inputTexture); + } + + if (_cvTextureCache) { + CVOpenGLESTextureCacheFlush(_cvTextureCache, 0); + CFRelease(_cvTextureCache); + _cvTextureCache = NULL; + } +} + + +- (instancetype)initWithType:(EffectsType)type + glContext:(EAGLContext *)glContext{ + if (![EffectsToken sharedInstance].bAuthrize) { + NSLog(@"please authorize the license first!!!"); + return nil; + } + if (!glContext) { + return nil; + } + if ((self = [super init])){ + self.glContext = glContext; + [self setCurrentEAGLContext:self.glContext]; + CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, self.glContext, NULL, &_cvTextureCache); +#if __has_include("st_mobile_common.h") + self.detector = [[EffectsDetector alloc] initWithType:type]; +#endif + self.animalDetect = [[EffectsAnimal alloc] initWithType:type]; + self.attriDetect = [[EffectsAttribute alloc] init]; + self.commonObject = [[EffectsCommonObject alloc] init]; + self.effect = [[Effects alloc] initWithType:type context:self.glContext]; + + } + return self; +} + +/// 鉴权 +/// @param licensePath 授权文件路径 ++ (BOOL)authorizeWithLicensePath:(NSString *)licensePath{ + if ([EffectsLicense authorizeWithLicensePath:licensePath]) { + [EffectsToken sharedInstance].bAuthrize = YES; + }else{ + [EffectsToken sharedInstance].bAuthrize = NO; + } + return [EffectsToken sharedInstance].bAuthrize; +} + +/// 鉴权 +/// @param licenseData 授权文件数据 ++ (BOOL)authorizeWithLicenseData:(NSData *)licenseData{ + if ([EffectsLicense authorizeWithLicenseData:licenseData]) { + [EffectsToken sharedInstance].bAuthrize = YES; + }else{ + [EffectsToken sharedInstance].bAuthrize = NO; + } + return [EffectsToken sharedInstance].bAuthrize; +} + +#if __has_include("st_mobile_common.h") +- (st_result_t)setModelPath:(NSString *)modelPath{ + st_result_t state = [self.detector setModelPath:modelPath]; + return state; +} + +- (st_result_t)setModelPath:(NSString *)modelPath withFirstPhaseFinished:(void(^)(void))finishedCallback { + st_result_t state = [self.detector setModelPath:modelPath withFirstPhaseFinished:finishedCallback]; + return state; +} + +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type path:(NSString *)path{ + return [self.effect setEffectType:type path:path]; +} + +- (st_result_t)setPackageId:(int)packageId groupType:(st_effect_beauty_group_t)type strength:(float)value{ + return [self.effect setPackageId:packageId groupType:type strength:value]; +} + +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type model:(int)model{ + return [self.effect setEffectType:type model:model]; +} + +- (st_result_t)setEffectType:(st_effect_beauty_type_t)type value:(float)value{ + return [self.effect setEffectType:type value:value]; +} + +- (st_result_t)setTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType { + return [self.effect setTryon:tryonInfo andTryonType:tryonType]; +} + +- (st_result_t)getTryon:(st_effect_tryon_info_t *)tryonInfo andTryonType:(st_effect_beauty_type_t)tryonType { + return [self.effect getTryon:tryonInfo andTryonType:tryonType]; +} + +- (st_result_t)setBeautyParam:(st_effect_beauty_param_t)param andVal:(float)val { + return [self.effect setBeautyParam:param andVal:val]; +} + +- (st_result_t)getBeautyParam:(st_effect_beauty_param_t)param andVal:(float *)val { + return [self.effect getBeautyParam:param andVal:val]; +} + +- (st_result_t)get3dBeautyPartsSize:(int *)partSize { + return [self.effect get3dBeautyPartsSize:partSize]; +} + +- (st_result_t)get3dBeautyParts:(st_effect_3D_beauty_part_info_t[])parts fromSize:(int)partSize { + return [self.effect get3dBeautyParts:parts fromSize:partSize]; +} + +- (st_result_t)set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t *)parts andVal:(int)partSize { + return [self.effect set3dBeautyPartsStrength:parts andVal:partSize]; +} + +- (st_result_t)f_set3dBeautyPartsStrength:(st_effect_3D_beauty_part_info_t[])parts andVal:(int)partSize { + return [self.effect f_set3dBeautyPartsStrength:parts andVal:partSize]; +} + +- (st_result_t)disableOverlap:(BOOL)isDisableOverlap { + return [self.effect disableOverlap:isDisableOverlap];; +} + +- (st_result_t)disableModuleReorder:(BOOL)isDisableModuleReorder { + return [self.effect disableModuleReorder:isDisableModuleReorder]; +} + +- (void)setStickerWithPath:(NSString *)stickerPath + callBack:(void(^)(st_result_t state, int stickerId, uint64_t action))callback{ + [self.effect setStickerWithPath:stickerPath callBack:callback]; +} + +- (void)setStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback { + [self.effect setStickerWithPath:stickerPath callBackCustomEventIncluded:callback]; +} + +- (st_result_t)removeSticker:(int)stickerId{ + return [self.effect removeSticker:stickerId]; +} + +- (void)addStickerWithPath:(NSString *)stickerPath + callBack:(void(^)(st_result_t state, int sticker, uint64_t action))callback{ + [self.effect addStickerWithPath:stickerPath callBack:callback]; +} + +-(void)replayStickerWithPackage:(int)packageId { + [self.effect replayStickerWithPackage:packageId]; +} + +- (void)addStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback { + [self.effect addStickerWithPath:stickerPath callBackCustomEventIncluded:callback]; +} + +-(void)changeStickerWithPath:(NSString *)stickerPath callBackCustomEventIncluded:(void(^)(st_result_t state, int stickerId, uint64_t action, uint64_t customEvent))callback { + [self.effect changeStickerWithPath:stickerPath callBackCustomEventIncluded:callback]; +} + +-(st_result_t)getModulesInPackage:(int)package_id modules:(st_effect_module_info_t*)modules { + return [self.effect getModulesInPackage:package_id modules:modules]; +} + +-(st_result_t)setModuleInfo:(st_effect_module_info_t *)module_info { + return [self.effect setModuleInfo:module_info]; +} + +- (void)getOverLap:(void(^)(st_effect_beauty_info_t *beauty_info))callback{ + [self.effect getOverLap:callback]; +} + +- (st_effect_beauty_info_t *)getOverlapInfo:(int *)count;{ + return [self.effect getOverlapInfo:count]; +} + +- (st_result_t)cleareStickers{ + return [self.effect cleareStickers]; +} + +- (st_result_t)processPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + cameraPosition:(AVCaptureDevicePosition)position + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)img_out{ + if(![EffectsToken sharedInstance].bAuthrize) return ST_E_NO_CAPABILITY; + if (!self.detector) return ST_E_FAIL; + self.cameraPosition = position; + int plane = (int)CVPixelBufferGetPlaneCount(pixelBuffer); + if (plane > 0) { + return [self processYUVPixelBuffer:pixelBuffer + rotate:rotate + outTexture:outTexture + outPixelFormat:fmt_out + outBuffer:img_out]; + }else{ + return [self processRGBAPixelBuffer:pixelBuffer + rotate:rotate + outTexture:outTexture + outPixelFormat:fmt_out + outBuffer:img_out]; + } +} + +- (st_result_t)detectWithPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + cameraPosition:(AVCaptureDevicePosition)position + humanAction:(st_mobile_human_action_t *)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult { + if(![EffectsToken sharedInstance].bAuthrize) return ST_E_NO_CAPABILITY; + if (!self.detector) return ST_E_FAIL; + self.cameraPosition = position; + OSType pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer); + if (pixelFormat != kCVPixelFormatType_32BGRA) { + return [self detectYUVPixelBuffer:pixelBuffer + rotate:rotate + humanAction:detectResult + animalResult:animalResult]; + }else{ + return [self detectRGBPixelBuffer:pixelBuffer + rotate:rotate + humanAction:detectResult + animalResult:animalResult]; + } +} + +-(st_result_t)resetHumanAction { + return [self.detector resetHumanAction] || [self.animalDetect resetAnimalFaceTracker]; +} + +-(st_result_t)setHumanActionParam:(st_human_action_param_type)type andValue:(float)value { + return [self.detector setParam:type andValue:value]; +} + +-(st_result_t)setEffectParam:(st_effect_param_t)param andValue:(float)value { + return [self.effect setParam:param andValue:value]; +} + +- (st_result_t)detectYUVPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + humanAction:(st_mobile_human_action_t *)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult { + uint64_t config = [self getDetectConfig]; + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char *yData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); + int yWidth = (int)CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); + int yHeight = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + BOOL needPadding = NO; + if (iBytesPerRow != yWidth) needPadding = YES; + unsigned char *uvData = NULL, *detectData = NULL; + int uvHeight = 0, uvBytesPerRow = 0; + if (needPadding) { + uvData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); + uvBytesPerRow = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); + uvHeight = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); + } + if (needPadding) { + [self solvePaddingImage:yData width:yWidth height:yHeight bytesPerRow:&iBytesPerRow]; + [self solvePaddingImage:uvData width:yWidth height:uvHeight bytesPerRow:&uvBytesPerRow]; + detectData = (unsigned char *)malloc(yWidth * yHeight * 3 / 2); + memcpy(detectData, yData, yWidth * yHeight); + memcpy(detectData+yWidth*yHeight, uvData, yWidth * yHeight / 2); + } + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + st_result_t ret = [self.detector detectHumanActionWithBuffer:needPadding?detectData:yData + size:(yWidth * yHeight) + config:config + rotate:rotate + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + detectResult:detectResult]; + + //detect animal + uint64_t animalConfig = [_effect getAnimalDetectConfig]; + if (animalConfig) { + ret = [self.animalDetect detectAnimalWithBuffer:needPadding?detectData:yData + rotate:rotate + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + config:(st_mobile_animal_type)animalConfig + detectResult:animalResult]; + } + + //focus center + CGPoint point = CGPointMake(0.5, 0.5); + if ((*detectResult).face_count && self.delegate) { + st_pointf_t facePoint = (*detectResult).p_faces[0].face106.points_array[46]; + point.x = facePoint.x/yWidth; point.y = facePoint.y/yHeight; + } + if (self.delegate && [self.delegate respondsToSelector:@selector(updateEffectsFacePoint:)]) { + [self.delegate updateEffectsFacePoint:point]; + } + + //attribute + if ((*detectResult).face_count) { +#if ENABLE_ATTRIBUTE + st_mobile_106_t *faces = (st_mobile_106_t *)malloc(sizeof(st_mobile_106_t) * (*detectResult).face_count); + memset(faces, 0, sizeof(st_mobile_106_t)*(*detectResult).face_count); + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithBuffer:needPadding?detectData:yData + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + faces:faces + attrArray:pAttrArray]; + free(faces); + NSLog(@"attribute_count %d", pAttrArray->attribute_count); +#endif + } + + //common object tracking + if (_bObject) { + st_rect_t rect; + float score; + ret = [self.commonObject detectObjectWithBuffer:needPadding?detectData:yData + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + rect:&rect + score:&score]; + if (ret == ST_OK) { + if (self.delegate && + [self.delegate respondsToSelector:@selector(updateCommonObjectPosition:)]) { + [self.delegate updateCommonObjectPosition:rect]; + } + } + } + if (detectData) free(detectData); + return ST_OK; +} + +- (st_result_t)detectRGBPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + humanAction:(st_mobile_human_action_t *)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult { + uint64_t config = [self getDetectConfig]; + //detect human action + st_result_t ret = [self.detector detectHumanActionWithPixelbuffer:pixelBuffer + config:config + rotate:rotate + detectResult:detectResult]; + + //detect animal + uint64_t animalConfig = [_effect getAnimalDetectConfig]; + if (animalConfig) { + ret = [self.animalDetect detectAnimalWithPixelbuffer:pixelBuffer + rotate:rotate + config:(st_mobile_animal_type)animalConfig + detectResult:animalResult]; + } + + //get face center point + CGPoint point = CGPointMake(0.5, 0.5); + if ((*detectResult).face_count) { + st_pointf_t facePoint = (*detectResult).p_faces[0].face106.points_array[46]; + int w = (int)CVPixelBufferGetWidth(pixelBuffer); + int h = (int)CVPixelBufferGetHeight(pixelBuffer); + point.x = facePoint.x/w; point.y = facePoint.y/h; + } + if (self.delegate && [self.delegate respondsToSelector:@selector(updateEffectsFacePoint:)]) { + [self.delegate updateEffectsFacePoint:point]; + } + //attribute + if ((*detectResult).face_count) { +#if ENABLE_ATTRIBUTE + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithPixelbuffer:pixelBuffer + detectResult:*detectResult + attrArray:pAttrArray]; + NSLog(@"attribute_count %d", pAttrArray->attribute_count); +#endif + } + //common object tracking + if (_bObject) { + st_rect_t rect; + float score; + ret = [self.commonObject detectObjectWithPixelbuffer:pixelBuffer + rect:&rect + score:&score]; + if (ret == ST_OK) { + if (self.delegate && + [self.delegate respondsToSelector:@selector(updateCommonObjectPosition:)]) { + [self.delegate updateCommonObjectPosition:rect]; + } + } + } + + return ST_OK; +} + +-(st_result_t)detectAttribute:(unsigned char *)imageData pixelFormat:(st_pixel_format)pixel_format imageWidth:(int)image_width imageHeight:(int)image_height imageStride:(int)image_stride orientation:(st_rotate_type)orientation withGenderCallback:(void(^)(BOOL isMale))callback { + unsigned long long config = [self getDetectConfig]; + st_mobile_human_action_t detectResult; + st_result_t ret = ST_OK; + ret = [self.detector detectHumanActionWithBuffer:imageData size:0 config:config rotate:ST_CLOCKWISE_ROTATE_0 pixelFormat:pixel_format width:image_width height:image_height stride:image_stride detectResult:&detectResult]; + if (ret != ST_OK) { + NSLog(@"%s - %d", __func__, __LINE__); + return ret; + } + if (detectResult.face_count == 0) return ST_E_INVALIDARG; + st_mobile_106_t *faces = &detectResult.p_faces[0].face106; + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithBuffer:imageData pixelFormat:pixel_format width:image_width height:image_height stride:image_width *4 faces:faces attrArray:pAttrArray withGenderCallback:callback]; + if (ret != ST_OK) { + NSLog(@"%s - %d", __func__, __LINE__); + return ret; + } + return ret; +} + +-(st_result_t)detectAttribute:(unsigned char *)imageData pixelFormat:(st_pixel_format)pixel_format imageWidth:(int)image_width imageHeight:(int)image_height detectResult:(st_mobile_human_action_t)detectResult withGenderCallback:(void(^)(BOOL isMale))callback { + st_result_t ret = ST_OK; + if (detectResult.face_count == 0) return ST_E_INVALIDARG; + st_mobile_106_t *faces = &detectResult.p_faces[0].face106; + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithBuffer:imageData pixelFormat:pixel_format width:image_width height:image_height stride:image_width *4 faces:faces attrArray:pAttrArray withGenderCallback:callback]; + if (ret != ST_OK) { + NSLog(@"%s - %d", __func__, __LINE__); + return ret; + } + return ret; +} + +//iRet = st_mobile_human_action_detect(_hDetector, +// pBGRAImageIn, +// ST_PIX_FMT_BGRA8888, +// iWidth, +// iHeight, +// iBytesPerRow, +// ST_CLOCKWISE_ROTATE_0, +// ST_MOBILE_FACE_DETECT , +// &detectResult); + +//- (st_result_t)detectAttributeWithPixelbuffer:(CVPixelBufferRef)pixelBuffer +// detectResult:(st_mobile_human_action_t)detectResult +// attrArray:(st_mobile_attributes_t *)pAttrArray withGenderCallback:(void(^)(BOOL isMale))callback { + +//st_mobile_human_action_detect( +// st_handle_t handle, +// const unsigned char *image, +// st_pixel_format pixel_format, +// int image_width, +// int image_height, +// int image_stride, +// st_rotate_type orientation, +// unsigned long long detect_config, +// st_mobile_human_action_t *p_human_action +//); + +- (st_result_t)renderPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + humanAction:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)img_out{ + if(![EffectsToken sharedInstance].bAuthrize) return ST_E_NO_CAPABILITY; + if (!self.detector) return ST_E_FAIL; + OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer); + if (format != kCVPixelFormatType_32BGRA) { + return [self renderYUVPixelBuffer:pixelBuffer + rotate:rotate + humanAction:detectResult + animalResult:animalResult + outTexture:outTexture + outPixelFormat:fmt_out + outData:img_out]; + }else{ + return [self renderRGBPixelBuffer:pixelBuffer + rotate:rotate + humanAction:detectResult + animalResult:animalResult + outTexture:outTexture + outPixelFormat:fmt_out + outData:img_out]; + } +} + +- (st_result_t)renderYUVPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + humanAction:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)img_out{ + [self setCurrentEAGLContext:self.glContext]; + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char *yData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); + int yWidth = (int)CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); + int yHeight = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); + unsigned char *uvData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); + + if (!_inputTexture) { + _inputTexture = [self createTextureWidth:yWidth height:yHeight]; + }else{ + if (_width != yWidth || _height != yHeight) { + _width = yWidth; _height = yHeight; + glDeleteTextures(1, &_inputTexture); + _inputTexture = [self createTextureWidth:yWidth height:yHeight]; + } + } + + int size = yWidth * yHeight * 3 / 2; + unsigned char *fullData = (unsigned char *)malloc(size); + memset(fullData, 0, size); + memcpy(fullData, yData, yWidth * yHeight); + memcpy(fullData+yWidth*yHeight, uvData, yWidth * yHeight / 2); + + [self.effect convertYUVBuffer:fullData + rgba:_inputTexture + size:CGSizeMake(yWidth, yHeight)]; + //render + [self processInputTexture:_inputTexture + inputData:fullData + inputFormat:ST_PIX_FMT_NV12 + outputTexture:outTexture + width:yWidth + height:yHeight + stride:yWidth + rotate:rotate + detectResult:detectResult + animalResult:animalResult + outPixelFormat:fmt_out + outBuffer:img_out]; + + if (fullData) free(fullData); + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + glFlush(); + return ST_OK; +} + + +- (st_result_t)renderRGBPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + humanAction:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)img_out{ + //render + [self setCurrentEAGLContext:self.glContext]; + GLuint originTexture = 0; + CVOpenGLESTextureRef originCVTexture = NULL; + BOOL bSuccess = [self getTextureWithPixelBuffer:pixelBuffer + texture:&originTexture + cvTexture:&originCVTexture + withCache:_cvTextureCache]; + if (originCVTexture) { + CFRelease(originCVTexture); + originCVTexture = NULL; + } + if (!bSuccess) { + NSLog(@"get origin textrue error"); + return 0; + } + int width = (int)CVPixelBufferGetWidth(pixelBuffer); + int height = (int)CVPixelBufferGetHeight(pixelBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char *inputData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + + self.inputTexture = originTexture; + GLuint dstText = [self processInputTexture:originTexture + inputData:inputData + inputFormat:ST_PIX_FMT_BGRA8888 + outputTexture:outTexture + width:width + height:height + stride:width * 4 + rotate:rotate + detectResult:detectResult + animalResult:animalResult + outPixelFormat:fmt_out + outBuffer:img_out]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + self.outputTexture = dstText; + return ST_OK; +} + +- (st_result_t)processData:(unsigned char *)data + size:(int)dataSize + width:(int)width + height:(int)height + stride:(int)stride + rotate:(st_rotate_type)rotate + pixelFormat:(st_pixel_format)pixelFormat + inputTexture:(GLuint)inputTexture + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outData:(unsigned char *)outData{ + if(![EffectsToken sharedInstance].bAuthrize) return ST_E_NO_CAPABILITY; + if (!self.detector) return ST_E_FAIL; + if (!glIsTexture(outTexture) || !glIsTexture(inputTexture)) return ST_E_INVALIDARG; + EFFECTSTIMELOG(total_cost); + uint64_t config = [self getDetectConfig]; + st_mobile_human_action_t detectResult; + memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); + st_result_t ret = [self.detector detectHumanActionWithBuffer:data + size:dataSize + config:config + rotate:rotate + pixelFormat:pixelFormat + width:width + height:height + stride:stride + detectResult:&detectResult]; + if (ret != ST_OK) { + NSLog(@"detect human action error %d", ret); + return ret; + } + //detect animal + st_mobile_animal_result_t animalResult; + memset(&animalResult, 0, sizeof(st_mobile_animal_result_t)); + uint64_t animalConfig = [_effect getAnimalDetectConfig]; + if (animalConfig) { + ret = [self.animalDetect detectAnimalWithBuffer:data + rotate:rotate + pixelFormat:pixelFormat + width:width + height:height + stride:width * 4 + config:(st_mobile_animal_type)animalConfig//ST_MOBILE_DOG_DETECT + detectResult:&animalResult]; + } + + //attribute + if (detectResult.face_count) { +#if ENABLE_ATTRIBUTE + st_mobile_106_t *faces = (st_mobile_106_t *)malloc(sizeof(st_mobile_106_t) * detectResult.face_count); + memset(faces, 0, sizeof(st_mobile_106_t)*detectResult.face_count); + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithBuffer:data + pixelFormat:pixelFormat + width:width + height:height + stride:width * 4 + faces:faces + attrArray:pAttrArray]; + free(faces); + NSLog(@"attribute_count %d", pAttrArray->attribute_count); +#endif + } + + [self setCurrentEAGLContext:self.glContext]; + [self processInputTexture:inputTexture + inputData:data + inputFormat:ST_PIX_FMT_RGBA8888 + outputTexture:outTexture + width:width + height:height + stride:width * 4 + rotate:rotate + detectResult:detectResult + animalResult:&animalResult + outPixelFormat:fmt_out + outBuffer:outData]; + EFFECTSTIMEPRINT(total_cost, "total_cost"); + return ST_OK; +} +#endif + +- (GLuint)createTextureWidth:(int)width height:(int)height{ + [self setCurrentEAGLContext:self.glContext]; + return createTextrue(width, height, NULL); +} + +- (GLuint)getTexutreWithPixelBuffer:(CVPixelBufferRef)pixelBuffer{ + int width = (int)CVPixelBufferGetWidth(pixelBuffer); + int height = (int)CVPixelBufferGetHeight(pixelBuffer); + CVOpenGLESTextureRef cvTextrue = nil; + CVReturn cvRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + _cvTextureCache, + + pixelBuffer, + NULL, + GL_TEXTURE_2D, + GL_RGBA, + width, + height, + GL_BGRA, + GL_UNSIGNED_BYTE, + 0, + &cvTextrue); + if (!cvTextrue || kCVReturnSuccess != cvRet) { + NSLog(@"CVOpenGLESTextureCacheCreateTextureFromImage error %d", cvRet); + return NO; + } + GLuint texture = CVOpenGLESTextureGetName(cvTextrue); + glBindTexture(GL_TEXTURE_2D , texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + CFRelease(cvTextrue); + return texture; +} + + +- (BOOL)getTextureWithPixelBuffer:(CVPixelBufferRef)pixelBuffer + texture:(GLuint*)texture + cvTexture:(CVOpenGLESTextureRef*)cvTexture + withCache:(CVOpenGLESTextureCacheRef)cache{ + int width = (int)CVPixelBufferGetWidth(pixelBuffer); + int height = (int)CVPixelBufferGetHeight(pixelBuffer); + CVReturn cvRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + cache, + pixelBuffer, + NULL, + GL_TEXTURE_2D, + GL_RGBA, + width, + height, + GL_BGRA, + GL_UNSIGNED_BYTE, + 0, + cvTexture); + if (!*cvTexture || kCVReturnSuccess != cvRet) { + NSLog(@"CVOpenGLESTextureCacheCreateTextureFromImage error %d", cvRet); + return NO; + } + *texture = CVOpenGLESTextureGetName(*cvTexture); + glBindTexture(GL_TEXTURE_2D , *texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + return YES; +} + +#if __has_include("st_mobile_common.h") +- (st_mobile_human_action_t)detectHumanActionWithPixelBuffer:(CVPixelBufferRef)pixelBuffer { + + return [self.detector detectHumanActionWithPixelBuffer:pixelBuffer]; +} + +- (st_face_shape_t)detectFaceShape:(st_mobile_face_t)p_face { + return [self.detector detectFaceShape:p_face]; +} + +- (void)setObjectTrackRect:(st_rect_t)rect{ + _bObject = rect.top | rect.left | rect.right | rect.bottom; + [self.commonObject setObjectRect:rect]; +} +#endif + +- (BOOL)isAuthrized{ + return [EffectsToken sharedInstance].bAuthrize; +} + + +- (void)createGLObjectWith:(int)width + height:(int)height + texture:(GLuint *)texture + pixelBuffer:(CVPixelBufferRef *)pixelBuffer + cvTexture:(CVOpenGLESTextureRef *)cvTexture{ + [self setCurrentEAGLContext:self.glContext]; + [self createTexture:texture + pixelBuffer:pixelBuffer + cvTexture:cvTexture + width:width + height:height + withCache:_cvTextureCache]; +} + +- (void)deleteTexture:(GLuint *)texture + pixelBuffer:(CVPixelBufferRef *)pixelBuffer + cvTexture:(CVOpenGLESTextureRef *)cvTexture{ + [self setCurrentEAGLContext:self.glContext]; + if (*texture) glDeleteTextures(1, texture); + if (*pixelBuffer) CVPixelBufferRelease(*pixelBuffer); + if (*cvTexture) CFRelease(*cvTexture); +} +#pragma mark - 3D mesh +#if __has_include("st_mobile_common.h") +-(void)getMeshList { + st_mobile_mesh_info_t mesh_info; + st_result_t state = [self.detector getMeshInfo:&mesh_info]; + if (state != ST_OK) { + NSLog(@"detect human action error %d", state); + return; + } + + state = [self.effect setFaceMeshList:*(mesh_info.p_mesh)]; + if (state != ST_OK) { + NSLog(@"effect set face mesh list error %d", state); + } +} +#endif + +#pragma mark - Private +- (void)setCurrentEAGLContext:(EAGLContext *)context{ + if (![[EAGLContext currentContext] isEqual:self.glContext]) { + [EAGLContext setCurrentContext:self.glContext]; + } +} +#if __has_include("st_mobile_common.h") +- (st_result_t)processYUVPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out{ + st_mobile_human_action_t detectResult; + memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); + uint64_t config = [self getDetectConfig]; + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char *yData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); + int yWidth = (int)CVPixelBufferGetWidthOfPlane(pixelBuffer, 0); + int yHeight = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 0); + int iBytesPerRow = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); + BOOL needPadding = NO; + if (iBytesPerRow != yWidth) needPadding = YES; + unsigned char *uvData = NULL, *detectData = NULL; + int uvHeight = 0, uvBytesPerRow = 0; + if (needPadding) { + uvData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); + uvBytesPerRow = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); + uvHeight = (int)CVPixelBufferGetHeightOfPlane(pixelBuffer, 1); + } + if (needPadding) { + [self solvePaddingImage:yData width:yWidth height:yHeight bytesPerRow:&iBytesPerRow]; + [self solvePaddingImage:uvData width:yWidth height:uvHeight bytesPerRow:&uvBytesPerRow]; + detectData = (unsigned char *)malloc(yWidth * yHeight * 3 / 2); + memcpy(detectData, yData, yWidth * yHeight); + memcpy(detectData+yWidth*yHeight, uvData, yWidth * yHeight / 2); + } + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + EFFECTSTIMELOG(total_cost); + st_result_t ret = [self.detector detectHumanActionWithBuffer:needPadding?detectData:yData + size:(yWidth * yHeight) + config:config + rotate:rotate + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + detectResult:&detectResult]; + if (ret != ST_OK) { + NSLog(@"detect human action error %d", ret); + return ret; + } + + //detect animal + st_mobile_animal_result_t animalResult; + memset(&animalResult, 0, sizeof(st_mobile_animal_result_t)); + uint64_t animalConfig = [_effect getAnimalDetectConfig]; + if (animalConfig) { + ret = [self.animalDetect detectAnimalWithBuffer:needPadding?detectData:yData + rotate:rotate + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + config:(st_mobile_animal_type)animalConfig//ST_MOBILE_DOG_DETECT + detectResult:&animalResult]; + } + + //render + [self setCurrentEAGLContext:self.glContext]; + + if (!_inputTexture) { + _inputTexture = [self createTextureWidth:yWidth height:yHeight]; + }else{ + if (_width != yWidth || _height != yHeight) { + _width = yWidth; _height = yHeight; + glDeleteTextures(1, &_inputTexture); + _inputTexture = [self createTextureWidth:yWidth height:yHeight]; + } + } + + [self.effect convertYUVBuffer:detectData + rgba:_inputTexture + size:CGSizeMake(yWidth, yHeight)]; + + //render + [self processInputTexture:_inputTexture + inputData:detectData + inputFormat:ST_PIX_FMT_NV12 + outputTexture:outTexture + width:yWidth + height:yHeight + stride:yWidth + rotate:rotate + detectResult:detectResult + animalResult:&animalResult + outPixelFormat:fmt_out + outBuffer:img_out]; + + if (detectData) free(detectData); + EFFECTSTIMEPRINT(total_cost, "total_cost"); + //focus center + CGPoint point = CGPointMake(0.5, 0.5); + if (detectResult.face_count && self.delegate) { + st_pointf_t facePoint = detectResult.p_faces[0].face106.points_array[46]; + point.x = facePoint.x/yWidth; point.y = facePoint.y/yHeight; + } + if (self.delegate && [self.delegate respondsToSelector:@selector(updateEffectsFacePoint:)]) { + [self.delegate updateEffectsFacePoint:point]; + } + + //attribute + if (detectResult.face_count) { +#if ENABLE_ATTRIBUTE + st_mobile_106_t *faces = (st_mobile_106_t *)malloc(sizeof(st_mobile_106_t) * detectResult.face_count); + memset(faces, 0, sizeof(st_mobile_106_t)*detectResult.face_count); + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithBuffer:needPadding?detectData:yData + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + faces:faces + attrArray:pAttrArray]; + free(faces); + NSLog(@"attribute_count %d", pAttrArray->attribute_count); +#endif + } + + //common object tracking + if (_bObject) { + st_rect_t rect; + float score; + ret = [self.commonObject detectObjectWithBuffer:needPadding?detectData:yData + pixelFormat:ST_PIX_FMT_NV12 + width:yWidth + height:yHeight + stride:iBytesPerRow + rect:&rect + score:&score]; + if (ret == ST_OK) { + if (self.delegate && + [self.delegate respondsToSelector:@selector(updateCommonObjectPosition:)]) { + [self.delegate updateCommonObjectPosition:rect]; + } + } + } + return ST_OK; +} + +- (GLuint)processRGBAPixelBuffer:(CVPixelBufferRef)pixelBuffer + rotate:(st_rotate_type)rotate + outTexture:(GLuint)outTexture + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out{ + uint64_t config = [self getDetectConfig]; + + EFFECTSTIMELOG(total_cost); + + //detect human action + st_mobile_human_action_t detectResult; + memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); + st_result_t ret = [self.detector detectHumanActionWithPixelbuffer:pixelBuffer + config:config + rotate:rotate + detectResult:&detectResult]; + if (ret != ST_OK) { + NSLog(@"detect human action error %d", ret); + return 0; + } + + //detect animal + st_mobile_animal_result_t animalResult; + memset(&animalResult, 0, sizeof(st_mobile_animal_result_t)); + uint64_t animalConfig = [_effect getAnimalDetectConfig]; + if (animalConfig) { + ret = [self.animalDetect detectAnimalWithPixelbuffer:pixelBuffer + rotate:rotate + config:(st_mobile_animal_type)animalConfig//ST_MOBILE_DOG_DETECT + detectResult:&animalResult]; + } + + //render + [self setCurrentEAGLContext:self.glContext]; + GLuint originTexture = 0; + CVOpenGLESTextureRef originCVTexture = NULL; + BOOL bSuccess = [self getTextureWithPixelBuffer:pixelBuffer + texture:&originTexture + cvTexture:&originCVTexture + withCache:_cvTextureCache]; + if (originCVTexture) { + CFRelease(originCVTexture); + originCVTexture = NULL; + } + if (!bSuccess) { + NSLog(@"get origin textrue error"); + return 0; + } + int width = (int)CVPixelBufferGetWidth(pixelBuffer); + int height = (int)CVPixelBufferGetHeight(pixelBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + unsigned char *inputData = (unsigned char*)CVPixelBufferGetBaseAddress(pixelBuffer); + self.inputTexture = originTexture; + [self processInputTexture:originTexture + inputData:inputData + inputFormat:ST_PIX_FMT_BGRA8888 + outputTexture:outTexture + width:width + height:height + stride:width * 4 + rotate:rotate + detectResult:detectResult + animalResult:&animalResult + outPixelFormat:fmt_out + outBuffer:img_out]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + EFFECTSTIMEPRINT(total_cost, "total_cost"); + //get face center point + CGPoint point = CGPointMake(0.5, 0.5); + if (detectResult.face_count) { + st_pointf_t facePoint = detectResult.p_faces[0].face106.points_array[46]; + int w = (int)CVPixelBufferGetWidth(pixelBuffer); + int h = (int)CVPixelBufferGetHeight(pixelBuffer); + point.x = facePoint.x/w; point.y = facePoint.y/h; + } + if (self.delegate && [self.delegate respondsToSelector:@selector(updateEffectsFacePoint:)]) { + [self.delegate updateEffectsFacePoint:point]; + } + //attribute + if (detectResult.face_count) { +#if ENABLE_ATTRIBUTE + st_mobile_attributes_t *pAttrArray = NULL; + ret = [self.attriDetect detectAttributeWithPixelbuffer:pixelBuffer + detectResult:detectResult + attrArray:pAttrArray]; + NSLog(@"attribute_count %d", pAttrArray->attribute_count); +#endif + } + //common object tracking + if (_bObject) { + st_rect_t rect; + float score; + ret = [self.commonObject detectObjectWithPixelbuffer:pixelBuffer + rect:&rect + score:&score]; + if (ret == ST_OK) { + if (self.delegate && + [self.delegate respondsToSelector:@selector(updateCommonObjectPosition:)]) { + [self.delegate updateCommonObjectPosition:rect]; + } + } + } + return ret; +} + +- (GLuint)processInputTexture:(GLuint)originTexture + inputData:(unsigned char *)inputData + inputFormat:(st_pixel_format)inputFormat + outputTexture:(GLuint)outputTexture + width:(int)width + height:(int)heigh + stride:(int)stride + rotate:(st_rotate_type)rotate + detectResult:(st_mobile_human_action_t)detectResult + animalResult:(st_mobile_animal_result_t *)animalResult + outPixelFormat:(st_pixel_format)fmt_out + outBuffer:(unsigned char *)img_out{ + //render texture to outTexture + st_mobile_human_action_t beautyOutDecResult; + memset(&beautyOutDecResult, 0, sizeof(st_mobile_human_action_t)); + st_mobile_human_action_copy(&detectResult, &beautyOutDecResult); + if (self.effect) { + self.effect.cameraPosition = self.cameraPosition; + [self.effect processTexture:originTexture + inputData:inputData + inputFormat:inputFormat + outputTexture:outputTexture + width:width + height:heigh + stride:stride + rotate:rotate + detectResult:detectResult + animalResult:animalResult + outDetectResult:beautyOutDecResult + withCache:_cvTextureCache + outPixelFormat:fmt_out + outBuffer:img_out]; + } + st_mobile_human_action_delete(&beautyOutDecResult); + return outputTexture; +} +#endif + +- (void)solvePaddingImage:(Byte *)pImage width:(int)iWidth height:(int)iHeight bytesPerRow:(int *)pBytesPerRow +{ + //pBytesPerRow 每行字节数 + int iBytesPerPixel = *pBytesPerRow / iWidth; + int iBytesPerRowCopied = iWidth * iBytesPerPixel; + int iCopiedImageSize = sizeof(Byte) * iWidth * iBytesPerPixel * iHeight; + + Byte *pCopiedImage = (Byte *)malloc(iCopiedImageSize); + memset(pCopiedImage, 0, iCopiedImageSize); + + for (int i = 0; i < iHeight; i ++) { + memcpy(pCopiedImage + i * iBytesPerRowCopied, + pImage + i * *pBytesPerRow, + iBytesPerRowCopied); + } + + memcpy(pImage, pCopiedImage, iCopiedImageSize); + *pBytesPerRow = iBytesPerRowCopied; + free(pCopiedImage); +} +#if __has_include("st_mobile_common.h") +- (uint64_t)getDetectConfig{ + if (self.configMode == EFDetectConfigModeItsMe) { + return [self getDetectConfigWithMode:EFDetectConfigModeItsMe]; + } + return [self.effect getDetectConfig] | (self.detectConfig?self.detectConfig:0); +} + +- (uint64_t)getDetectConfigWithMode:(EFDetectConfigMode)configMode { + return [self.effect getDetectConfigWithMode:configMode] | (self.detectConfig?self.detectConfig:0); +} +#endif +- (GLuint)createaTextureWithData:(unsigned char *)data + width:(int)width + height:(int)height{ + GLuint texture = createTextrue(width, height, NULL); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, data); + return texture; +} + + + +- (BOOL)createTexture:(GLuint *)texture + pixelBuffer:(CVPixelBufferRef *)pixelBuffer + cvTexture:(CVOpenGLESTextureRef *)cvTexture + width:(int)width + height:(int)height + withCache:(nonnull CVOpenGLESTextureCacheRef)cache{ + CFDictionaryRef empty = CFDictionaryCreate(kCFAllocatorDefault, + NULL, + NULL, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFMutableDictionaryRef attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, + 1, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty); + CVReturn cvRet = CVPixelBufferCreate(kCFAllocatorDefault, + width, + height, + kCVPixelFormatType_32BGRA, + attrs, + pixelBuffer); + if (kCVReturnSuccess != cvRet) { + NSLog(@"CVPixelBufferCreate %d", cvRet); + return NO; + } + cvRet = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, + cache, + *pixelBuffer, + NULL, + GL_TEXTURE_2D, + GL_RGBA, + width, + height, + GL_BGRA, + GL_UNSIGNED_BYTE, + 0, + cvTexture); + CFRelease(attrs); + CFRelease(empty); + if (kCVReturnSuccess != cvRet) { + NSLog(@"CVOpenGLESTextureCacheCreateTextureFromImage %d", cvRet); + return NO; + } + *texture = CVOpenGLESTextureGetName(*cvTexture); + glBindTexture(CVOpenGLESTextureGetTarget(*cvTexture), *texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glBindTexture(GL_TEXTURE_2D, 0); + return YES; +} + + +#pragma mark - C Function +GLuint createTextrue(int width, int height, unsigned char *data){ + GLuint texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); + glBindTexture(GL_TEXTURE_2D, 0); + return texture; +} + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h new file mode 100644 index 000000000..80da6c616 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h @@ -0,0 +1,32 @@ +// +// VideoProcessingManager.h +// BeautifyExample +// +// Created by zhaoyongqiang on 2022/6/28. +// Copyright © 2022 Agora. All rights reserved. +// + +#import +#import "EffectsProcess.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface VideoProcessingManager : NSObject + +@property (nonatomic, strong) EffectsProcess *effectsProcess; + +- (void)setEffectType: (uint32_t)type value:(float)value; + +- (CVPixelBufferRef)videoProcessHandler:(CVPixelBufferRef)pixelBuffer; + +- (void)setStickerWithPath: (NSString *)stickerPath callBack:(void (^)(int))callback; + +- (void)addStylePath: (NSString *)stylePath groupId: (int)groudId strength: (CGFloat)strength callBack:(void (^)(int))callback; + +- (void)removeStickerId: (int)stickerId; + +- (void)cleareStickers; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m new file mode 100644 index 000000000..8d0a81aa3 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m @@ -0,0 +1,215 @@ +// +// VideoProcessingManager.m +// BeautifyExample +// +// Created by zhaoyongqiang on 2022/6/28. +// Copyright © 2022 Agora. All rights reserved. +// + +#import "VideoProcessingManager.h" +#import "EFMotionManager.h" +#import "BundleUtil.h" +#import + +@interface VideoProcessingManager () +{ + @public + GLuint _outTexture; + CVPixelBufferRef _outputPixelBuffer; + CVOpenGLESTextureRef _outputCVTexture; + BOOL _isFirstLaunch; +} + +@property (nonatomic, strong, readwrite) EAGLContext *glContext; +@property (nonatomic) UIDeviceOrientation deviceOrientation; +@property (nonatomic) dispatch_queue_t renderQueue; +///贴纸id +@property (nonatomic, assign) int stickerId; +@property (nonatomic, copy) NSString *stickerPath; +@property (nonatomic, assign) int filterId; + +@end + +@implementation VideoProcessingManager + +- (instancetype)init { + if (self = [super init]) { + self.renderQueue = dispatch_queue_create("com.render.queue", DISPATCH_QUEUE_SERIAL); + //effects + dispatch_async(self.renderQueue, ^{ +#if __has_include("st_mobile_common.h") + self.glContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; + self.effectsProcess = [[EffectsProcess alloc] initWithType:EffectsTypeVideo glContext:self.glContext]; + NSBundle *bundle = [BundleUtil bundleWithBundleName:@"SenseLib" podName:@"senseLib"]; + [self.effectsProcess setModelPath:[bundle pathForResource:@"model" ofType:@"bundle"]]; + [EAGLContext setCurrentContext:self.glContext]; +// self.effectsProcess.detectConfig = ST_MOBILE_FACE_DETECT; +#endif + }); + } + return self; +} + +- (void)setEffectType: (uint32_t)type value:(float)value { +#if __has_include("st_mobile_common.h") + [self.effectsProcess setEffectType:type value:value]; +#endif +} + +- (void)setStickerWithPath: (NSString *)stickerPath callBack:(void (^)(int))callback { + NSString *path = [[NSBundle mainBundle] pathForResource:stickerPath ofType:nil]; +#if __has_include("st_mobile_common.h") + [self.effectsProcess addStickerWithPath:path callBack:^(st_result_t state, int sticker, uint64_t action) { + if (callback) { + callback(sticker); + } + }]; +#endif +} + +- (void)addStylePath: (NSString *)stylePath groupId: (int)groudId strength: (CGFloat)strength callBack:(void (^)(int))callback { +#if __has_include("st_mobile_common.h") + if (self.stickerId && [stylePath isEqualToString:self.stickerPath]) { + if (groudId == 0) { + [self.effectsProcess setPackageId:self.stickerId groupType:EFFECT_BEAUTY_GROUP_MAKEUP strength:strength]; + } else { + [self.effectsProcess setPackageId:self.stickerId groupType:EFFECT_BEAUTY_GROUP_FILTER strength:strength]; + } + if (callback) { + callback(self.stickerId); + } + return; + } + if (self.stickerId) { + [self removeStickerId:self.stickerId]; + } + NSString *path = [[NSBundle mainBundle] pathForResource:stylePath ofType:nil]; + __weak VideoProcessingManager *weakself = self; + [self.effectsProcess addStickerWithPath:path callBack:^(st_result_t state, int sticker, uint64_t action) { + weakself.stickerId = sticker; + weakself.stickerPath = path; + if (groudId == 0) { + [weakself.effectsProcess setPackageId:sticker groupType:EFFECT_BEAUTY_GROUP_MAKEUP strength:strength]; + } else { + [weakself.effectsProcess setPackageId:sticker groupType:EFFECT_BEAUTY_GROUP_FILTER strength:strength]; + } + if (callback) { + callback(sticker); + } + }]; +#endif +} + +- (void)removeStickerId:(int)stickerId { +#if __has_include("st_mobile_common.h") + [self.effectsProcess removeSticker:stickerId]; +#endif +} + +- (void)cleareStickers { +#if __has_include("st_mobile_common.h") + [self.effectsProcess cleareStickers]; +#endif +} + +- (CVPixelBufferRef)videoProcessHandler:(CVPixelBufferRef)pixelBuffer { + if (!pixelBuffer) return pixelBuffer; + + if (!self.effectsProcess) { + return pixelBuffer; + } + // 设置 OpenGL 环境 , 需要与初始化 SDK 时一致 + if ([EAGLContext currentContext] != self.glContext) { + [EAGLContext setCurrentContext:self.glContext]; + } + + CVPixelBufferLockBaseAddress(pixelBuffer, 0); + int width = (int)CVPixelBufferGetWidth(pixelBuffer); + int heigh = (int)CVPixelBufferGetHeight(pixelBuffer); +#if __has_include("st_mobile_common.h") + if (_outTexture) { + int _cacheW = (int)CVPixelBufferGetWidth(_outputPixelBuffer); + int _cacheH = (int)CVPixelBufferGetHeight(_outputPixelBuffer); + if (_cacheH != heigh || _cacheW != width) { + GLuint testTexture = 0; //TODO: shengtao + [self.effectsProcess deleteTexture:&testTexture pixelBuffer:&_outputPixelBuffer cvTexture:&_outputCVTexture]; + _outTexture = 0; + _outputPixelBuffer = NULL; + _outputCVTexture = NULL; + } + } else { + [self.effectsProcess createGLObjectWith:width + height:heigh + texture:&_outTexture + pixelBuffer:&_outputPixelBuffer + cvTexture:&_outputCVTexture]; + } + st_mobile_human_action_t detectResult; + memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); + st_result_t ret = [self.effectsProcess detectWithPixelBuffer:pixelBuffer + rotate:[self getRotateType] + cameraPosition:AVCaptureDevicePositionFront + humanAction:&detectResult + animalResult:nil]; + if (ret != ST_OK) { + NSLog(@"人脸检测失败"); + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return pixelBuffer; + } + [self.effectsProcess renderPixelBuffer:pixelBuffer + rotate:[self getRotateType] + humanAction:detectResult + animalResult:nil + outTexture:self->_outTexture + outPixelFormat:ST_PIX_FMT_BGRA8888 + outData:nil]; + +#endif + + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + + return self->_outputPixelBuffer; +} + +- (void)getDeviceOrientation:(CMAccelerometerData *)accelerometerData { + if (accelerometerData.acceleration.x >= 0.75) { + _deviceOrientation = UIDeviceOrientationLandscapeRight; + } else if (accelerometerData.acceleration.x <= -0.75) { + _deviceOrientation = UIDeviceOrientationLandscapeLeft; + } else if (accelerometerData.acceleration.y <= -0.75) { + _deviceOrientation = UIDeviceOrientationPortrait; + } else if (accelerometerData.acceleration.y >= 0.75) { + _deviceOrientation = UIDeviceOrientationPortraitUpsideDown; + } else { + _deviceOrientation = UIDeviceOrientationPortrait; + } +} + +#if __has_include("st_mobile_common.h") +- (st_rotate_type)getRotateType{ + BOOL isFrontCamera = YES;//self.camera.devicePosition == AVCaptureDevicePositionFront; + BOOL isVideoMirrored = YES;//self.camera.videoConnection.isVideoMirrored; + + [self getDeviceOrientation:[EFMotionManager sharedInstance].motionManager.accelerometerData]; + + switch (_deviceOrientation) { + + case UIDeviceOrientationPortrait: + return ST_CLOCKWISE_ROTATE_0; + + case UIDeviceOrientationPortraitUpsideDown: + return ST_CLOCKWISE_ROTATE_180; + + case UIDeviceOrientationLandscapeLeft: + return ((isFrontCamera && isVideoMirrored) || (!isFrontCamera && !isVideoMirrored)) ? ST_CLOCKWISE_ROTATE_270 : ST_CLOCKWISE_ROTATE_90; + + case UIDeviceOrientationLandscapeRight: + return ((isFrontCamera && isVideoMirrored) || (!isFrontCamera && !isVideoMirrored)) ? ST_CLOCKWISE_ROTATE_90 : ST_CLOCKWISE_ROTATE_270; + + default: + return ST_CLOCKWISE_ROTATE_0; + } +} +#endif + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.h new file mode 100644 index 000000000..12f65b113 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.h @@ -0,0 +1,15 @@ +// +// FUBeautify.h +// APIExample +// +// Created by zhaoyongqiang on 2022/10/21. +// Copyright © 2022 Agora Corp. All rights reserved. +// + +#import + +@interface SenseBeautifyVC : UIViewController + +@property (nonatomic, strong) NSDictionary *configs; + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.m new file mode 100644 index 000000000..cadd3c6a1 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/SenseBeautifyVC.m @@ -0,0 +1,200 @@ +// +// FUBeautify.m +// APIExample +// +// Created by zhaoyongqiang on 2022/10/21. +// Copyright © 2022 Agora Corp. All rights reserved. +// + +#import "SenseBeautifyVC.h" +#import +#import "APIExample-Swift.h" +#import "BeautyAPI.h" +#import "SenseBeautyRender.h" + +@interface SenseBeautifyVC () + +@property (weak, nonatomic) IBOutlet UILabel *tipsLabel; +@property (weak, nonatomic) IBOutlet UIView *container; +@property (weak, nonatomic) IBOutlet UIView *localVideo; +@property (weak, nonatomic) IBOutlet UIView *remoteVideo; + +@property (nonatomic, strong) AgoraRtcEngineKit *rtcEngineKit; +@property (nonatomic, strong) BeautyAPI *beautyAPI; +@property (nonatomic, strong) SenseBeautyRender *senseRender; +@property (nonatomic, assign) BOOL didCleanup; + +@end + +@implementation SenseBeautifyVC +- (BeautyAPI *)beautyAPI { + if (_beautyAPI == nil) { + _beautyAPI = [[BeautyAPI alloc] init]; + [_beautyAPI enable:YES]; + } + return _beautyAPI; +} + +- (SenseBeautyRender *)senseRender { + if (_senseRender == nil) { + _senseRender = [[SenseBeautyRender alloc] init]; + } + return _senseRender; +} + +- (void)viewDidLoad { + [super viewDidLoad]; +#if __has_include("st_mobile_common.h") + [self setupSenseArService]; +#else + [self.tipsLabel setHidden:NO]; + [self.container setHidden:YES]; +#endif +} + +- (void)setupSenseArService { + if (self.senseRender.isSuccessLicense) { + [self initSDK]; + } else { + [self.tipsLabel setHidden:NO]; + [self.container setHidden:YES]; + [self.tipsLabel setText:NSLocalizedString(@"license authorization failed, please check whether the license file is correct", nil)]; + [self performSelector:@selector(setupSenseArService) withObject:nil afterDelay:1]; + } +} + +- (void) initSDK { +#if __has_include("st_mobile_common.h") + [self.tipsLabel setHidden:YES]; + [self.container setHidden:NO]; +#else + [self.tipsLabel setHidden:NO]; + [self.container setHidden:YES]; +#endif + + AgoraRtcEngineConfig *rtcConfig = [[AgoraRtcEngineConfig alloc] init]; + rtcConfig.appId = KeyCenter.AppId; + rtcConfig.channelProfile = AgoraChannelProfileLiveBroadcasting; + self.rtcEngineKit = [AgoraRtcEngineKit sharedEngineWithConfig:rtcConfig delegate:self]; + + [self.rtcEngineKit enableVideo]; + [self.rtcEngineKit enableAudio]; + + AgoraVideoEncoderConfiguration *encodeConfig = [[AgoraVideoEncoderConfiguration alloc] initWithSize:CGSizeMake(480, 640) + frameRate:(AgoraVideoFrameRateFps15) + bitrate:AgoraVideoBitrateStandard + orientationMode:(AgoraVideoOutputOrientationModeFixedPortrait) + mirrorMode:(AgoraVideoMirrorModeDisabled)]; + [self.rtcEngineKit setVideoEncoderConfiguration:encodeConfig]; + + BeautyConfig *config = [[BeautyConfig alloc] init]; + config.rtcEngine = self.rtcEngineKit; + config.captureMode = CaptureModeAgora; + config.beautyRender = self.senseRender; + [self.beautyAPI initialize:config]; + + [self.beautyAPI setupLocalVideo:self.localVideo renderMode:AgoraVideoRenderModeHidden]; + [self.rtcEngineKit startPreview]; + + // set custom capturer as video source + AgoraRtcChannelMediaOptions *option = [[AgoraRtcChannelMediaOptions alloc] init]; + option.clientRoleType = AgoraClientRoleBroadcaster; + option.channelProfile = AgoraChannelProfileLiveBroadcasting; + option.publishMicrophoneTrack = YES; + option.publishCameraTrack = YES; + option.autoSubscribeAudio = YES; + option.autoSubscribeVideo = YES; + __weak typeof(self) weakSelf = self; + [[NetworkManager shared] generateTokenWithChannelName:self.title uid:0 success:^(NSString * _Nullable token) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf == nil || strongSelf.didCleanup) { + return; + } + [strongSelf.rtcEngineKit joinChannelByToken:token + channelId:strongSelf.title + uid: 0 + mediaOptions:option + joinSuccess:^(NSString * _Nonnull channel, NSUInteger uid, NSInteger elapsed) { + NSLog(@"join channel success uid: %lu", uid); + }]; + }]; +} + +- (IBAction)onTapSwitchCameraButton:(id)sender { + [self.beautyAPI switchCamera]; +} +- (IBAction)onTapBeautyButton:(UIButton *)sender { + [sender setSelected:!sender.isSelected]; + if (sender.isSelected) { + [self.beautyAPI setBeautyPreset:(BeautyPresetModeDefault)]; + } else { + [self.beautyAPI.beautyRender reset]; + } +} +- (IBAction)onTapMakeupButton:(UIButton *)sender { + [sender setSelected:!sender.isSelected]; + [self.beautyAPI.beautyRender setMakeup:sender.isSelected]; +} +- (IBAction)onTapStickerButton:(UIButton *)sender { + [sender setSelected:!sender.isSelected]; + [self.beautyAPI.beautyRender setSticker:sender.isSelected]; +} + + +#pragma mark - RtcEngineDelegate +- (void)rtcEngine:(AgoraRtcEngineKit *)engine didJoinedOfUid:(NSUInteger)uid elapsed:(NSInteger)elapsed { + dispatch_async(dispatch_get_main_queue(), ^{ + AgoraRtcVideoCanvas *videoCanvas = [AgoraRtcVideoCanvas new]; + videoCanvas.uid = uid; + videoCanvas.view = self.remoteVideo; + videoCanvas.renderMode = AgoraVideoRenderModeHidden; + videoCanvas.mirrorMode = AgoraVideoMirrorModeDisabled; + [self.rtcEngineKit setupRemoteVideo:videoCanvas]; + [self.remoteVideo setHidden:NO]; + }); +} + +- (void)rtcEngine:(AgoraRtcEngineKit *)engine didOfflineOfUid:(NSUInteger)uid reason:(AgoraUserOfflineReason)reason { + dispatch_async(dispatch_get_main_queue(), ^{ + AgoraRtcVideoCanvas *videoCanvas = [AgoraRtcVideoCanvas new]; + videoCanvas.uid = uid; + videoCanvas.view = nil; + [self.rtcEngineKit setupRemoteVideo:videoCanvas]; + [self.remoteVideo setHidden:YES]; + }); +} + +- (void)willMoveToParentViewController:(UIViewController *)parent { + [super willMoveToParentViewController:parent]; + if (parent == nil) { + [self cleanup]; + } +} + +- (void)cleanup { + if (self.didCleanup) { + return; + } + self.didCleanup = YES; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(setupSenseArService) object:nil]; + if (_beautyAPI != nil) { + [_beautyAPI destroy]; + _beautyAPI = nil; + } else if (_senseRender != nil) { + [_senseRender destroy]; + } + _senseRender = nil; + if (_rtcEngineKit != nil) { + [_rtcEngineKit stopPreview]; + [_rtcEngineKit leaveChannel:nil]; + [AgoraRtcEngineKit destroy]; + _rtcEngineKit = nil; + } +} + +- (void)dealloc { + [self cleanup]; +} + + +@end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/zh-Hans.lproj/SenseBeautify.strings b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/zh-Hans.lproj/SenseBeautify.strings new file mode 100644 index 000000000..50f1e28d4 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/zh-Hans.lproj/SenseBeautify.strings @@ -0,0 +1,36 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "Iy0-Dq-h5x"; */ +"Iy0-Dq-h5x.title" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "VpM-9W-auG"; */ +"VpM-9W-auG.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "kf0-3f-UI5"; */ +"kf0-3f-UI5.normalTitle" = "Button"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "视频实时通话"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "wHl-zh-dFe"; */ +"wHl-zh-dFe.normalTitle" = "Button"; + +"j6s-Pm-fSS.text" = "您还没有开启商汤美颜功能, 请按如下步骤操作: +1: 在SenseBeautify->Manager->替换SENSEME.lic +2: 替换license绑定的Bundle identifier +3: 打开Podfile中 pod 'senseLib' 注释 +4: 在iOS->APIExample目录下创建SenseLib文件夹 +5: 在iOS->APIExample->SenseLib目录下添加remoteSourcesLib文件夹 +6: 在iOS->APIExample->SenseLib目录下添加st_mobile_sdk文件夹 +7: 执行pod install +8: 重新运行项目查看效果"; + +"CrL-Yf-Cev.normalTitle" = "美颜"; +"3hp-ZM-MMW.normalTitle" = "美妆"; +"UdR-D4-uNu.normalTitle" = "贴纸"; +"K3f-4k-VQ1.normalTitle" = "滤镜"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift index 6f401b351..798408906 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift @@ -33,10 +33,14 @@ class ThirdBeautifyEntry: BaseViewController { let actionSheetVC = UIAlertController(title: "Third Beautify".localized, message: nil, preferredStyle: .actionSheet) + let sense = UIAlertAction(title: "Sense Beautify".localized, style: .default) { _ in + self.jumpHandler(type: .sense) + } let fu = UIAlertAction(title: "FU Beautify".localized, style: .default) { _ in self.jumpHandler(type: .fu) } let cancel = UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil) + actionSheetVC.addAction(sense) actionSheetVC.addAction(fu) actionSheetVC.addAction(cancel) // present(actionSheetVC, animated: true, completion: nil) diff --git a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings index 375d1fdc7..499183206 100644 --- a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings @@ -142,6 +142,7 @@ "Open Rear Camera" = "打开后摄像头"; "Close Rear Camera" = "关闭后摄像头"; "Third Beautify" = "第三方美颜"; +"Sense Beautify" = "商汤美颜"; "FU Beautify" = "相芯美颜"; "license authorization failed, please check whether the license file is correct" = "license授权失败, 请检查license文件是否正确"; "Quick input APPID and Token methods" = "快捷输入APPID和Token方法"; diff --git a/iOS/APIExample/Podfile b/iOS/APIExample/Podfile index 102f745d3..16fc000f6 100644 --- a/iOS/APIExample/Podfile +++ b/iOS/APIExample/Podfile @@ -4,8 +4,8 @@ #source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' def common_pods - pod 'AgoraRtcEngine_iOS', '4.7.0' - # pod 'sdk', :path => 'sdk.podspec' + # pod 'AgoraRtcEngine_iOS', '4.7.0' + pod 'sdk', :path => 'sdk.podspec' end target 'APIExample' do @@ -22,7 +22,8 @@ target 'APIExample' do # pod 'SwiftLint', '~> 0.53.0' pod 'SnapKit', '~> 5.7.0' - # pod 'fuLib', :path => 'fu.podspec' + pod 'SenseLib', :path => 'sense.podspec' + pod 'fuLib', :path => 'fu.podspec' end target 'Agora-ScreenShare-Extension' do diff --git a/iOS/APIExample/cloud_build.sh b/iOS/APIExample/cloud_build.sh index 99fd8a2fe..831965118 100755 --- a/iOS/APIExample/cloud_build.sh +++ b/iOS/APIExample/cloud_build.sh @@ -17,6 +17,11 @@ unzip -q AgoraBeautyMaterial.bundle.zip -d APIExample/Resources || exit 1 rm -f AgoraBeautyMaterial.bundle.zip test -f APIExample/Resources/AgoraBeautyMaterial.bundle/beauty_material_functional/config.json || exit 1 +echo "start download sense resource : $sense_lib" +curl -L -O "$sense_lib" +unzip -o vender_sense_iOS.zip +rm -f vender_sense_iOS.zip + echo "start download fu resource : $fu_lib" curl -L -O "$fu_lib" unzip -o vender_fu_iOS.zip @@ -26,6 +31,7 @@ rm -f vender_fu_iOS.zip sed -i -e "s#\# pod 'ijkplayer'# pod 'ijkplayer'#g" Podfile # Enable third-party beauty filters +sed -i -e "s#\# pod 'SenseLib'# pod 'SenseLib'#g" Podfile sed -i -e "s#\# pod 'fuLib'# pod 'fuLib'#g" Podfile echo "work space: $WORKSPACE" From 4bd0ba6c2e82bebab337b5975e1463aadaeedec3 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Mon, 10 Aug 2026 14:57:14 +0800 Subject: [PATCH 26/49] fix(ios): download beauty resources from Artifactory --- iOS/APIExample/cloud_build.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/iOS/APIExample/cloud_build.sh b/iOS/APIExample/cloud_build.sh index 831965118..5067a3cf3 100755 --- a/iOS/APIExample/cloud_build.sh +++ b/iOS/APIExample/cloud_build.sh @@ -17,15 +17,19 @@ unzip -q AgoraBeautyMaterial.bundle.zip -d APIExample/Resources || exit 1 rm -f AgoraBeautyMaterial.bundle.zip test -f APIExample/Resources/AgoraBeautyMaterial.bundle/beauty_material_functional/config.json || exit 1 -echo "start download sense resource : $sense_lib" -curl -L -O "$sense_lib" -unzip -o vender_sense_iOS.zip +echo "start download sense resource from Artifactory" +curl --fail --location -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -o vender_sense_iOS.zip "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/vender_sense_iOS.zip" || exit 1 +rm -rf SenseLib +unzip -q vender_sense_iOS.zip || exit 1 rm -f vender_sense_iOS.zip +test -f SenseLib/SENSEME.lic || exit 1 -echo "start download fu resource : $fu_lib" -curl -L -O "$fu_lib" -unzip -o vender_fu_iOS.zip +echo "start download fu resource from Artifactory" +curl --fail --location -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -o vender_fu_iOS.zip "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/vender_fu_iOS.zip" || exit 1 +rm -rf FULib +unzip -q vender_fu_iOS.zip || exit 1 rm -f vender_fu_iOS.zip +test -f FULib/authpack.h || exit 1 # Enable third-party player configuration sed -i -e "s#\# pod 'ijkplayer'# pod 'ijkplayer'#g" Podfile From a81b10e474d553cbca42fcbea81268eef02a5bdc Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 11 Aug 2026 15:01:29 +0800 Subject: [PATCH 27/49] feat(android): update agora beauty --- .../examples/advanced/AgoraBeauty.java | 130 ++++++++---------- 1 file changed, 54 insertions(+), 76 deletions(-) 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 09ebfcaea..922c6c0f1 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 @@ -509,89 +509,67 @@ private void updateMakeupOptionsByStyle() { if (!makeUp.isChecked()) { return; } - sbMakeupFilterStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getMakeupFilterStrength() * 10)); - - int facialStyleValue = AgoraBeautySDK.getBeautyConfig().getFacialStyle(); - int facialPosition; - if (facialStyleValue == 2) { - facialPosition = 1; - } else if (facialStyleValue == 4) { - facialPosition = 2; - } else if (facialStyleValue == 5) { - facialPosition = 3; - } else if (facialStyleValue == 6) { - facialPosition = 4; - } else { - facialPosition = 0; - } - spinnerFacialStyle.setSelection(facialPosition); - sbFacialStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getFacialStrength() * 10)); - - spinnerWocanStyle.setSelection(AgoraBeautySDK.getBeautyConfig().getWocanStyle()); - sbWocanStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getFacialStrength() * 10)); - - spinnerBrowStyle.setSelection(AgoraBeautySDK.getBeautyConfig().getBrowStyle()); - spinnerBrowColor.setSelection(AgoraBeautySDK.getBeautyConfig().getBrowColor()); - sbBrowStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBrowStrength() * 10)); - - int lashStyle = AgoraBeautySDK.getBeautyConfig().getLashStyle(); - int lashPosition = 0; - if (lashStyle == 3) { - lashPosition = 1; - } else if (lashStyle == 5) { - lashPosition = 2; - } - spinnerBrowStyle.setSelection(lashPosition); - spinnerBrowColor.setSelection(AgoraBeautySDK.getBeautyConfig().getBrowColor()); - sbBrowStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBrowStrength() * 10)); - - int shadowStyle = AgoraBeautySDK.getBeautyConfig().getShadowStyle(); - int shadowPosition = 0; - if (shadowStyle == 1) { - shadowPosition = 1; - } else if (lashStyle == 6) { - shadowPosition = 2; - } - spinnerShadowStyle.setSelection(shadowPosition); - sbBrowStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getShadowStrength() * 10)); - - spinnerPupilStyle.setSelection(AgoraBeautySDK.getBeautyConfig().getPupilStyle()); - sbPupilStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getPupilStrength() * 10)); - - int blushStyle = AgoraBeautySDK.getBeautyConfig().getBlushStyle(); - int blushPosition = 0; - if (blushStyle == 1) { - blushPosition = 1; - } else if (lashStyle == 2) { - blushPosition = 2; - } else if (lashStyle == 4) { - blushPosition = 3; - } else if (lashStyle == 8) { - blushPosition = 4; + isProgrammaticChange = true; + try { + sbMakeupFilterStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getMakeupFilterStrength() * 10)); + + setSpinnerSelection(spinnerFacialStyle, findStylePosition( + AgoraBeautySDK.getBeautyConfig().getFacialStyle(), 0, 2, 3, 5, 6)); + sbFacialStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getFacialStrength() * 10)); + + setSpinnerSelection(spinnerWocanStyle, AgoraBeautySDK.getBeautyConfig().getWocanStyle()); + sbWocanStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getWocanStrength() * 10)); + + setSpinnerSelection(spinnerBrowStyle, AgoraBeautySDK.getBeautyConfig().getBrowStyle()); + setSpinnerSelection(spinnerBrowColor, AgoraBeautySDK.getBeautyConfig().getBrowColor()); + sbBrowStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBrowStrength() * 10)); + + setSpinnerSelection(spinnerLashStyle, findStylePosition( + AgoraBeautySDK.getBeautyConfig().getLashStyle(), 0, 3, 5)); + setSpinnerSelection(spinnerLashColor, AgoraBeautySDK.getBeautyConfig().getLashColor()); + sbLashStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getLashStrength() * 10)); + + setSpinnerSelection(spinnerShadowStyle, findStylePosition( + AgoraBeautySDK.getBeautyConfig().getShadowStyle(), 0, 1, 6)); + sbShadowStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getShadowStrength() * 10)); + + setSpinnerSelection(spinnerPupilStyle, AgoraBeautySDK.getBeautyConfig().getPupilStyle()); + sbPupilStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getPupilStrength() * 10)); + + setSpinnerSelection(spinnerBlushStyle, findStylePosition( + AgoraBeautySDK.getBeautyConfig().getBlushStyle(), 0, 1, 2, 4, 9)); + setSpinnerSelection(spinnerBlushColor, AgoraBeautySDK.getBeautyConfig().getBlushColor()); + sbBlushStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBlushStrength() * 10)); + + setSpinnerSelection(spinnerLipStyle, findStylePosition( + AgoraBeautySDK.getBeautyConfig().getLipStyle(), 0, 1, 2, 3, 6)); + setSpinnerSelection(spinnerLipColor, AgoraBeautySDK.getBeautyConfig().getLipColor()); + sbLipStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getLipStrength() * 10)); + } finally { + isProgrammaticChange = false; } - spinnerBlushStyle.setSelection(blushPosition); - spinnerBlushColor.setSelection(AgoraBeautySDK.getBeautyConfig().getBlushColor()); - sbBlushStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBlushStrength() * 10)); - - int lipStyle = AgoraBeautySDK.getBeautyConfig().getLipStyle(); - int lipPosition = 0; - if (lipStyle == 1) { - lipPosition = 1; - } else if (lipStyle == 2) { - lipPosition = 2; - } else if (lipStyle == 3) { - lipPosition = 3; - } else if (lipStyle == 6) { - lipPosition = 4; + } + + private void setSpinnerSelection(Spinner spinner, int position) { + int itemCount = spinner.getAdapter() == null ? 0 : spinner.getAdapter().getCount(); + spinner.setSelection(position >= 0 && position < itemCount ? position : 0); + } + + private int findStylePosition(int styleValue, int... supportedValues) { + for (int position = 0; position < supportedValues.length; position++) { + if (supportedValues[position] == styleValue) { + return position; + } } - spinnerLipStyle.setSelection(lipPosition); - spinnerLipColor.setSelection(AgoraBeautySDK.getBeautyConfig().getLipColor()); - sbLipStrength.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getLipStrength() * 10)); + return 0; } @SuppressLint("NonConstantResourceId") @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (isProgrammaticChange) { + return; + } switch (parent.getId()) { case R.id.spinner_shape_beautify_style: if (!shapeBeauty.isChecked()) { From e2700565c7d937558d39b37b111ddbea06a525af Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 12 Aug 2026 20:53:51 +0800 Subject: [PATCH 28/49] fix(windows): correct audio mixing resource path --- .../APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp index 1b3136b01..f4379a758 100755 --- a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp @@ -516,7 +516,7 @@ void CAudioMixingEventHandler::onAudioMixingStateChanged(AUDIO_MIXING_STATE_TYPE void CAgoraAudioMixingDlg::OnBnClickedButtonMixingStart() { - CString audioUrl = GetExePath() + _T("\\ID_MUSIC_01.m4a"); + CString audioUrl = GetExePath() + _T("\\res\\ID_MUSIC_01.m4a"); int ret = m_rtcEngine->startAudioMixing(cs2utf8(audioUrl).c_str(), false, -1, 0); CString strInfo; From 00fb6b13a35ef45cf4331e7237ecf8e34b83bf9a Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 13 Aug 2026 22:25:59 +0800 Subject: [PATCH 29/49] fix(windows): correct packaged resource paths --- .../PushExternalVideoYUV.cpp | 18 ++++++++++---- .../TransparentBg/TransparentBgDialog.cpp | 2 +- .../LiveBroadcasting/CLiveBroadcastingDlg.cpp | 2 +- windows/APIExample/cloud_build.bat | 24 +++++++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp b/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp index 55016324d..b409fa90a 100755 --- a/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp +++ b/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp @@ -113,26 +113,34 @@ bool PushExternalVideoYUV::InitAgora() m_videoFrame.type = agora::media::base::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; m_imgBuffer = new BYTE[YUVReader::VIDEO_FRAME_SIZE]{ 0 }; - m_imgBuffer16 = new uint16_t[WIDHT_HDR * HEIGHT_HDR * 1.5]{ 0 }; - CString videoUrl = GetExePath() + _T("\\hdr_1280_720.yuv"); + CString videoUrl = GetExePath() + _T("\\res\\hdr_1280_720.yuv"); std::string tmp = cs2utf8(videoUrl); std::ifstream file(tmp, std::ios::binary); if (!file) { AfxMessageBox(_T("open hdr file failed")); + UnInitAgora(); + return false; } file.seekg(0, std::ios::end); std::streamsize fileSize = file.tellg(); file.seekg(0, std::ios::beg); - if (fileSize % sizeof(uint16_t) != 0) { - AfxMessageBox(_T(" hdr file format is wrong")); + const std::streamsize expectedFileSize = + static_cast(WIDHT_HDR) * HEIGHT_HDR * 3 * sizeof(uint16_t) / 2; + if (fileSize != expectedFileSize) { + AfxMessageBox(_T("hdr file size is wrong")); + UnInitAgora(); + return false; } std::vector fileBuffer(fileSize); if (!file.read(fileBuffer.data(), fileSize)) { AfxMessageBox(_T("read hdr file failed")); + UnInitAgora(); + return false; } file.close(); + m_imgBuffer16 = new uint16_t[WIDHT_HDR * HEIGHT_HDR * 3 / 2]{ 0 }; uint16_t* uint16Buffer = reinterpret_cast(fileBuffer.data()); - memcpy_s(m_imgBuffer16, fileSize, uint16Buffer, fileSize); + memcpy_s(m_imgBuffer16, expectedFileSize, uint16Buffer, fileSize); m_rtcEngine->startPreview(VIDEO_SOURCE_CUSTOM); return true; diff --git a/windows/APIExample/APIExample/Advanced/TransparentBg/TransparentBgDialog.cpp b/windows/APIExample/APIExample/Advanced/TransparentBg/TransparentBgDialog.cpp index f0988e077..3ce49b00b 100644 --- a/windows/APIExample/APIExample/Advanced/TransparentBg/TransparentBgDialog.cpp +++ b/windows/APIExample/APIExample/Advanced/TransparentBg/TransparentBgDialog.cpp @@ -256,7 +256,7 @@ void CTransparentBgDlg::StartPlay() m_mediaPlayer->registerVideoFrameObserver(this); m_mediaPlayer->registerPlayerSourceObserver(this); MediaSource mediaSource; - CString videoUrl = GetExePath() + _T("\\yuvj_full_range_alpha_1280_540_left.mp4"); + CString videoUrl = GetExePath() + _T("\\res\\yuvj_full_range_alpha_1280_540_left.mp4"); std::string tmp = cs2utf8(videoUrl); mediaSource.url = tmp.c_str(); CString pathInfo; diff --git a/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp b/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp index bdbad998b..b1f073421 100755 --- a/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp +++ b/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp @@ -1251,7 +1251,7 @@ void CLiveBroadcastingDlg::OnBnClickedCheckVideoImage() if (!m_rtcEngine || !m_initialize) return; ImageTrackOptions options; - CString strPath = GetExePath() + _T("\\bg_blue.png"); + CString strPath = GetExePath() + _T("\\res\\bg_blue.png"); m_imgPng = cs2utf8(strPath); options.imageUrl = m_imgPng.c_str(); options.fps = 15; diff --git a/windows/APIExample/cloud_build.bat b/windows/APIExample/cloud_build.bat index 4b059fef4..c4279ea84 100644 --- a/windows/APIExample/cloud_build.bat +++ b/windows/APIExample/cloud_build.bat @@ -51,9 +51,33 @@ REM Copy executables and DLLs from Release xcopy /Y /S Release\*.exe Output\ >nul 2>&1 xcopy /Y /S Release\*.dll Output\ >nul 2>&1 +REM Copy runtime images that are loaded from the executable directory +xcopy /Y APIExample\Advanced\LocalVideoTranscoding\agora.png Output\ >nul 2>&1 +xcopy /Y APIExample\Advanced\LocalVideoTranscoding\agora.jpg Output\ >nul 2>&1 + REM Copy resources xcopy /Y /S /I APIExample\res Output\res >nul 2>&1 xcopy /Y /S /I Release\beauty_agora Output\beauty_agora >nul 2>&1 +if not exist Output\agora.png ( + echo Local video transcoding PNG is missing from the output package! + exit /b 1 +) +if not exist Output\agora.jpg ( + echo Local video transcoding JPG is missing from the output package! + exit /b 1 +) +if not exist Output\res\hdr_1280_720.yuv ( + echo Custom video capture resource is missing from the output package! + exit /b 1 +) +if not exist Output\res\yuvj_full_range_alpha_1280_540_left.mp4 ( + echo Transparent background resource is missing from the output package! + exit /b 1 +) +if not exist Output\res\bg_blue.png ( + echo Live broadcasting image is missing from the output package! + exit /b 1 +) if not exist Output\beauty_agora\beauty_material_functional\config.json ( echo Agora beauty material is missing from the output package! exit /b 1 From ee77cc85a7599e4f683f4c42c08aa959b5763488 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Fri, 14 Aug 2026 11:19:25 +0800 Subject: [PATCH 30/49] fix(windows): package custom video sample --- .../APIExample/APIExample/APIExample.vcxproj | 15 +++----- .../PushExternalVideoYUV.cpp | 5 ++- windows/APIExample/APIExample/YUVReader.cpp | 37 +++++++++++++++---- windows/APIExample/APIExample/YUVReader.h | 8 ++-- windows/APIExample/cloud_build.bat | 5 +++ 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/windows/APIExample/APIExample/APIExample.vcxproj b/windows/APIExample/APIExample/APIExample.vcxproj index a76c495fd..561100f0a 100644 --- a/windows/APIExample/APIExample/APIExample.vcxproj +++ b/windows/APIExample/APIExample/APIExample.vcxproj @@ -123,8 +123,7 @@ if exist en.ini (copy en.ini $(SolutionDir)$(Configuration)) copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.png $(SolutionDir)$(Configuration) copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.jpg $(SolutionDir)$(Configuration) if exist sample.yuv (copy sample.yuv $(SolutionDir)$(Configuration)) -if exist $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 (copy $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 $(SolutionDir)$(Configuration)) -if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIExample\res\hdr_1280_720.yuv $(SolutionDir)$(Configuration)) +xcopy /Y /S /I "$(SolutionDir)APIExample\res" "$(SolutionDir)$(Configuration)\res" @@ -171,9 +170,8 @@ if exist zh-cn.ini (copy zh-cn.ini $(SolutionDir)$(Platform)\$(Configuration)) if exist en.ini (copy en.ini $(SolutionDir)$(Platform)\$(Configuration)) copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.png $(SolutionDir)$(Platform)\$(Configuration) copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.jpg $(SolutionDir)$(Platform)\$(Configuration) -if exist sample.yuv (copy sample.yuv $(SolutionDir)$(Platform)$(Configuration)) -if exist $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 (copy $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 $(SolutionDir)$(Configuration)) -if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIExample\res\hdr_1280_720.yuv $(SolutionDir)$(Configuration)) +if exist sample.yuv (copy sample.yuv $(SolutionDir)$(Platform)\$(Configuration)) +xcopy /Y /S /I "$(SolutionDir)APIExample\res" "$(SolutionDir)$(Platform)\$(Configuration)\res" @@ -221,8 +219,7 @@ copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.png $(Solutio copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.jpg $(SolutionDir)$(Configuration) if exist sample.yuv (copy sample.yuv $(SolutionDir)$(Configuration)) -if exist $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 (copy $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 $(SolutionDir)$(Configuration)) -if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIExample\res\hdr_1280_720.yuv $(SolutionDir)$(Configuration)) +xcopy /Y /S /I "$(SolutionDir)APIExample\res" "$(SolutionDir)$(Configuration)\res" @@ -268,11 +265,11 @@ if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIEx if exist $(SolutionDir)ThirdParty\libFFmpeg (copy $(SolutionDir)ThirdParty\libFFmpeg\x64\*.dll $(SolutionDir)$(Platform)\$(Configuration)) if exist zh-cn.ini (copy zh-cn.ini $(SolutionDir)$(Platform)\$(Configuration)) if exist en.ini (copy en.ini $(SolutionDir)$(Platform)\$(Configuration)) +copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.png $(SolutionDir)$(Platform)\$(Configuration) copy $(SolutionDir)APIExample\Advanced\LocalVideoTranscoding\agora.jpg $(SolutionDir)$(Platform)\$(Configuration) if exist sample.yuv (copy sample.yuv $(SolutionDir)$(Platform)\$(Configuration)) -if exist $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 (copy $(SolutionDir)APIExample\res\yuvj_full_range_alpha_1280_540_left.mp4 $(SolutionDir)$(Configuration)) -if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIExample\res\hdr_1280_720.yuv $(SolutionDir)$(Configuration)) +xcopy /Y /S /I "$(SolutionDir)APIExample\res" "$(SolutionDir)$(Platform)\$(Configuration)\res" PerMonitorHighDPIAware diff --git a/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp b/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp index b409fa90a..ee28d059f 100755 --- a/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp +++ b/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp @@ -275,7 +275,10 @@ void PushExternalVideoYUV::OnClickedButtonJoinchannel() strInfo.Format(_T("join channel %s, use ChannelMediaOptions"), getCurrentTime()); m_btnJoinChannel.EnableWindow(FALSE); - yuvReader.start(std::bind(&PushExternalVideoYUV::OnYUVRead, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + if (!yuvReader.start(std::bind(&PushExternalVideoYUV::OnYUVRead, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4))) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("open sample.yuv failed")); + AfxMessageBox(_T("open sample.yuv failed")); + } } } else { diff --git a/windows/APIExample/APIExample/YUVReader.cpp b/windows/APIExample/APIExample/YUVReader.cpp index 846ba109a..36455f77d 100755 --- a/windows/APIExample/APIExample/YUVReader.cpp +++ b/windows/APIExample/APIExample/YUVReader.cpp @@ -3,16 +3,37 @@ YUVReader::YUVReader() { - + char modulePath[MAX_PATH] = { 0 }; + DWORD pathLength = GetModuleFileNameA(nullptr, modulePath, MAX_PATH); + if (pathLength > 0 && pathLength < MAX_PATH) { + filePath.assign(modulePath, pathLength); + size_t separator = filePath.find_last_of("\\/"); + if (separator != std::string::npos) { + filePath.erase(separator + 1); + } + filePath += "sample.yuv"; + } + else { + filePath = "sample.yuv"; + } } -void YUVReader::start(IYUVCallback callback) +bool YUVReader::start(IYUVCallback callback) { - if (thread == nullptr) { - isReading = true; - this->callback = callback; - thread = new std::thread(std::bind(&YUVReader::run, this)); + if (thread != nullptr) { + return true; } + + std::ifstream fileStream(filePath, std::ios::binary); + if (!fileStream) { + return false; + } + fileStream.close(); + + isReading = true; + this->callback = callback; + thread = new std::thread(std::bind(&YUVReader::run, this)); + return true; } void YUVReader::stop() @@ -20,6 +41,7 @@ void YUVReader::stop() isReading = false; if (thread != nullptr) { thread->join(); + delete thread; thread = nullptr; this->callback = nullptr; } @@ -27,7 +49,7 @@ void YUVReader::stop() void YUVReader::run() { - std::ifstream fileStream(FILE_PATH); + std::ifstream fileStream(filePath, std::ios::binary); if (!fileStream) { // failure @@ -56,4 +78,3 @@ void YUVReader::run() fileStream.close(); } - diff --git a/windows/APIExample/APIExample/YUVReader.h b/windows/APIExample/APIExample/YUVReader.h index d85882b6e..8d5148d5d 100755 --- a/windows/APIExample/APIExample/YUVReader.h +++ b/windows/APIExample/APIExample/YUVReader.h @@ -1,9 +1,11 @@ #pragma once #include #include +#include #include #include #include +#include typedef std::function IYUVCallback; @@ -12,8 +14,6 @@ class YUVReader { public: - - const char* FILE_PATH = "sample.yuv"; const static int VIDEO_WIDTH = 320; const static int VIDEO_HEIGHT = 180; const static int VIDEO_FRAME_SIZE = VIDEO_WIDTH * VIDEO_HEIGHT * 3 / 2; @@ -23,7 +23,7 @@ class YUVReader YUVReader(); - void start(IYUVCallback callback); + bool start(IYUVCallback callback); void stop(); @@ -33,8 +33,8 @@ class YUVReader IYUVCallback callback = nullptr; std::thread* thread = nullptr; bool isReading = false; + std::string filePath; unsigned char buffer[VIDEO_FRAME_SIZE]{0}; void run(); }; - diff --git a/windows/APIExample/cloud_build.bat b/windows/APIExample/cloud_build.bat index c4279ea84..4428052cf 100644 --- a/windows/APIExample/cloud_build.bat +++ b/windows/APIExample/cloud_build.bat @@ -50,6 +50,7 @@ mkdir Output REM Copy executables and DLLs from Release xcopy /Y /S Release\*.exe Output\ >nul 2>&1 xcopy /Y /S Release\*.dll Output\ >nul 2>&1 +xcopy /Y Release\sample.yuv Output\ >nul 2>&1 REM Copy runtime images that are loaded from the executable directory xcopy /Y APIExample\Advanced\LocalVideoTranscoding\agora.png Output\ >nul 2>&1 @@ -66,6 +67,10 @@ if not exist Output\agora.jpg ( echo Local video transcoding JPG is missing from the output package! exit /b 1 ) +if not exist Output\sample.yuv ( + echo Custom video capture sample is missing from the output package! + exit /b 1 +) if not exist Output\res\hdr_1280_720.yuv ( echo Custom video capture resource is missing from the output package! exit /b 1 From 3597cd3b909d4a3fdf43157643abdcf0acdf3cf8 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 20 Aug 2026 14:47:52 +0800 Subject: [PATCH 31/49] chore: standardize agent assets and harden release checks --- .../api-example-release-iteration/SKILL.md | 2 +- .github/workflows/ai-engineering-checks.yml | 64 ++++ AGENTS.md | 13 +- .../.agents/skills/upsert-case/SKILL.md | 15 + .../ai-engineering/case-maintenance-matrix.md | 27 +- docs/ai-engineering/knowledge-index.md | 6 +- docs/ai-engineering/release-known-issues.md | 1 + .../templates/release-dry-run-template.md | 2 +- .../ai-engineering/tools/lint_agent_assets.py | 333 ++++++++++++++++++ .../tools/lint_agent_assets_test.py | 332 +++++++++++++++++ .../tools/prepare_case_execution.py | 13 +- .../tools/prepare_case_execution_test.py | 45 +++ .../tools/validate_acceptance_manifest.py | 2 - .../validate_acceptance_manifest_test.py | 13 +- .../skills/query-cases/SKILL.md | 0 .../skills/review-case/SKILL.md | 0 .../skills/upsert-case/SKILL.md | 0 iOS/APIExample-Audio/AGENTS.md | 6 +- .../skills/query-cases/SKILL.md | 0 .../skills/review-case/SKILL.md | 0 .../skills/upsert-case/SKILL.md | 0 iOS/APIExample-OC/AGENTS.md | 6 +- .../skills/query-cases/SKILL.md | 0 .../skills/review-case/SKILL.md | 0 .../skills/upsert-case/SKILL.md | 0 iOS/APIExample-SwiftUI/AGENTS.md | 6 +- .../skills/query-cases/SKILL.md | 0 .../skills/review-case/SKILL.md | 0 .../skills/upsert-case/SKILL.md | 0 iOS/APIExample/AGENTS.md | 6 +- .../skills/review-case/SKILL.md | 0 .../references/correct-lifecycle.swift | 0 .../references/correct-thread-safety.swift | 0 .../references/incorrect-lifecycle.swift | 0 .../references/incorrect-thread-safety.swift | 0 .../skills/upsert-case/SKILL.md | 0 .../references/example-template.swift | 0 .../references/lifecycle-pattern.swift | 0 .../references/thread-safety-pattern.swift | 0 macOS/AGENTS.md | 6 +- macOS/ARCHITECTURE.md | 2 +- .../skills/review-case/SKILL.md | 0 .../references/correct-lifecycle.cpp | 0 .../references/correct-thread-safety.cpp | 0 .../references/incorrect-lifecycle.cpp | 0 .../references/incorrect-thread-safety.cpp | 0 .../skills/upsert-case/SKILL.md | 0 .../references/event-handler-pattern.cpp | 0 .../references/example-template.cpp | 0 .../upsert-case/references/example-template.h | 0 .../references/lifecycle-pattern.cpp | 0 .../references/message-map-pattern.cpp | 0 windows/AGENTS.md | 6 +- windows/ARCHITECTURE.md | 2 +- 54 files changed, 872 insertions(+), 36 deletions(-) rename {.agent => .agents}/skills/api-example-release-iteration/SKILL.md (95%) create mode 100644 .github/workflows/ai-engineering-checks.yml create mode 100644 docs/ai-engineering/tools/lint_agent_assets.py create mode 100644 docs/ai-engineering/tools/lint_agent_assets_test.py rename iOS/APIExample-Audio/{.agent => .agents}/skills/query-cases/SKILL.md (100%) rename iOS/APIExample-Audio/{.agent => .agents}/skills/review-case/SKILL.md (100%) rename iOS/APIExample-Audio/{.agent => .agents}/skills/upsert-case/SKILL.md (100%) rename iOS/APIExample-OC/{.agent => .agents}/skills/query-cases/SKILL.md (100%) rename iOS/APIExample-OC/{.agent => .agents}/skills/review-case/SKILL.md (100%) rename iOS/APIExample-OC/{.agent => .agents}/skills/upsert-case/SKILL.md (100%) rename iOS/APIExample-SwiftUI/{.agent => .agents}/skills/query-cases/SKILL.md (100%) rename iOS/APIExample-SwiftUI/{.agent => .agents}/skills/review-case/SKILL.md (100%) rename iOS/APIExample-SwiftUI/{.agent => .agents}/skills/upsert-case/SKILL.md (100%) rename iOS/APIExample/{.agent => .agents}/skills/query-cases/SKILL.md (100%) rename iOS/APIExample/{.agent => .agents}/skills/review-case/SKILL.md (100%) rename iOS/APIExample/{.agent => .agents}/skills/upsert-case/SKILL.md (100%) rename macOS/{.agent => .agents}/skills/review-case/SKILL.md (100%) rename macOS/{.agent => .agents}/skills/review-case/references/correct-lifecycle.swift (100%) rename macOS/{.agent => .agents}/skills/review-case/references/correct-thread-safety.swift (100%) rename macOS/{.agent => .agents}/skills/review-case/references/incorrect-lifecycle.swift (100%) rename macOS/{.agent => .agents}/skills/review-case/references/incorrect-thread-safety.swift (100%) rename macOS/{.agent => .agents}/skills/upsert-case/SKILL.md (100%) rename macOS/{.agent => .agents}/skills/upsert-case/references/example-template.swift (100%) rename macOS/{.agent => .agents}/skills/upsert-case/references/lifecycle-pattern.swift (100%) rename macOS/{.agent => .agents}/skills/upsert-case/references/thread-safety-pattern.swift (100%) rename windows/{.agent => .agents}/skills/review-case/SKILL.md (100%) rename windows/{.agent => .agents}/skills/review-case/references/correct-lifecycle.cpp (100%) rename windows/{.agent => .agents}/skills/review-case/references/correct-thread-safety.cpp (100%) rename windows/{.agent => .agents}/skills/review-case/references/incorrect-lifecycle.cpp (100%) rename windows/{.agent => .agents}/skills/review-case/references/incorrect-thread-safety.cpp (100%) rename windows/{.agent => .agents}/skills/upsert-case/SKILL.md (100%) rename windows/{.agent => .agents}/skills/upsert-case/references/event-handler-pattern.cpp (100%) rename windows/{.agent => .agents}/skills/upsert-case/references/example-template.cpp (100%) rename windows/{.agent => .agents}/skills/upsert-case/references/example-template.h (100%) rename windows/{.agent => .agents}/skills/upsert-case/references/lifecycle-pattern.cpp (100%) rename windows/{.agent => .agents}/skills/upsert-case/references/message-map-pattern.cpp (100%) diff --git a/.agent/skills/api-example-release-iteration/SKILL.md b/.agents/skills/api-example-release-iteration/SKILL.md similarity index 95% rename from .agent/skills/api-example-release-iteration/SKILL.md rename to .agents/skills/api-example-release-iteration/SKILL.md index 9213cbd36..6c469dfde 100644 --- a/.agent/skills/api-example-release-iteration/SKILL.md +++ b/.agents/skills/api-example-release-iteration/SKILL.md @@ -21,7 +21,7 @@ Android, iOS, macOS, and Windows are required by default. Contract may choose th ## Source Route -Read root `AGENTS.md`, the repository profile, this knowledge index and gate, then each platform/project `AGENTS.md`, `ARCHITECTURE.md`, and available `query-cases`, `upsert-case`, and `review-case` skills. Project skills may use either `.agent/skills/` or `.agents/skills/`. Read `release-known-issues.md` only for release, SDK version, CI, signing, packaging, license, or third-party asset work. +Read root `AGENTS.md`, the repository profile, this knowledge index and gate, then `/AGENTS.md`, `/AGENTS.md`, `/ARCHITECTURE.md`, and the available `query-cases`, `upsert-case`, and `review-case` skills. Every project keeps its skills under `.agents/skills/`. Read `docs/ai-engineering/release-known-issues.md` only for release, SDK version, CI, signing, packaging, license, or third-party asset work. ## Agent Topology diff --git a/.github/workflows/ai-engineering-checks.yml b/.github/workflows/ai-engineering-checks.yml new file mode 100644 index 000000000..e28dfdfb2 --- /dev/null +++ b/.github/workflows/ai-engineering-checks.yml @@ -0,0 +1,64 @@ +name: AI Engineering Checks + +# Guards the AI engineering assets (AGENTS.md routing, SKILL.md bundles) and the +# orchestration tooling under docs/ai-engineering/tools/. These checks are file-only: +# they never build a platform sample, so they stay fast enough to run on every PR. + +on: + push: + branches: + - main + - master + - 'dev/**' + paths: + - '**/AGENTS.md' + - '**/ARCHITECTURE.md' + - '**/SKILL.md' + - '**/.agents/**' + - 'docs/ai-engineering/**' + - '.github/workflows/ai-engineering-checks.yml' + pull_request: + paths: + - '**/AGENTS.md' + - '**/ARCHITECTURE.md' + - '**/SKILL.md' + - '**/.agents/**' + - 'docs/ai-engineering/**' + - '.github/workflows/ai-engineering-checks.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ai-engineering-checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + name: Lint assets and run tooling tests + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + PYTHONDONTWRITEBYTECODE: '1' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + # The tooling uses str.removeprefix, so 3.9 is the floor. Pin 3.11 for stability. + python-version: '3.11' + + - name: Lint AI engineering assets + run: python3 docs/ai-engineering/tools/lint_agent_assets.py + + - name: Run AI engineering tooling tests + run: | + python3 -m unittest \ + docs/ai-engineering/tools/lint_agent_assets_test.py \ + docs/ai-engineering/tools/validate_acceptance_manifest_test.py \ + docs/ai-engineering/tools/generate_case_backlog_test.py \ + docs/ai-engineering/tools/prepare_case_execution_test.py \ + docs/ai-engineering/tools/orchestrate_case_execution_test.py diff --git a/AGENTS.md b/AGENTS.md index 3249c06d4..51d0ce421 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ Each platform directory contains its own `AGENTS.md` with platform-specific rule ## AI Engineering Workflow -For AI-assisted version iteration, product-to-implementation routing, multi-agent acceptance, or automatic API example generation, use the repository workflow in `.agent/skills/api-example-release-iteration/SKILL.md`. +For AI-assisted version iteration, product-to-implementation routing, multi-agent acceptance, or automatic API example generation, use the repository workflow in `.agents/skills/api-example-release-iteration/SKILL.md`. Supporting documents: - `docs/ai-engineering/knowledge-index.md` - repository knowledge map for agents. @@ -64,4 +64,13 @@ 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. +Never commit a real App ID, App Certificate, or token. The mechanism differs by platform, and only Android keeps credentials in a git-ignored file: + +| Platform | Where the App ID goes | Tracked by git? | +|----------|----------------------|-----------------| +| Android | `local.properties` at each project root, key `AGORA_APP_ID`, read by `app/build.gradle`(`.kts`) into `BuildConfig.AGORA_APP_ID` | No — `local.properties` is git-ignored | +| iOS | `/Common/KeyCenter.swift`, or `KeyCenter.h` / `KeyCenter.m` for `APIExample-OC` | **Yes** | +| macOS | `APIExample/Common/KeyCenter.swift` | **Yes** | +| Windows | `APIExample/APIExample/CConfig.h` and `CConfig.cpp` | **Yes** | + +The iOS, macOS, and Windows credential files are tracked placeholder files, not git-ignored. Editing one to build locally leaves a real credential in a tracked file, so never stage it. The safety net is the gitleaks pre-commit hook (see Git Hooks above), not `.gitignore` — do not rely on git ignoring these paths. diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md index 7048e6920..a81c131d3 100644 --- a/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -1,3 +1,18 @@ +--- +name: upsert-case +description: > + Add a new audio API example case or modify an existing one in the APIExample-Audio + Android demo — updates the Fragment source, XML layout, string resources, nav_graph + registration, and ARCHITECTURE.md together. Use when: adding a new Agora voice API + demo screen, modifying an existing audio case's implementation or registration, or + updating an audio case's strings, layout, nav entry, or architecture docs. This + project uses voice-sdk only and must never call video APIs. + Keywords: add case, modify case, update case, new fragment, nav_graph, @Example, + BaseFragment, APIExample-Audio, audio case, voice API, demo case. +--- + +# Upsert Case — APIExample-Audio + ## What this skill is for Use this skill to add or update a case in `APIExample-Audio/`. It owns the full change closure: case source, XML layout, strings, `nav_graph.xml`, and `ARCHITECTURE.md`, while preserving the voice-SDK-only constraint. diff --git a/docs/ai-engineering/case-maintenance-matrix.md b/docs/ai-engineering/case-maintenance-matrix.md index 9019b4474..1f1e0cdac 100644 --- a/docs/ai-engineering/case-maintenance-matrix.md +++ b/docs/ai-engineering/case-maintenance-matrix.md @@ -1,6 +1,6 @@ # API Examples Case Maintenance Matrix -This matrix is the planning artifact for cross-platform API Example parity work. It does not replace platform `AGENTS.md`, project `ARCHITECTURE.md`, or project `.agent/skills/*` / `.agents/skills/*`; every row must be verified against the target project before implementation. +This matrix is the planning artifact for cross-platform API Example parity work. It does not replace platform `AGENTS.md`, project `ARCHITECTURE.md`, or project `.agents/skills/*`; every row must be verified against the target project before implementation. ## How To Use @@ -49,7 +49,9 @@ If runtime/device behavior is required but unavailable, use `PARTIAL(...)` and n ## Pilot Matrix -Seeded from the overseas `dev/4.6.4` project `ARCHITECTURE.md` indexes and source tree. The inventory audit resolved 191 `DONE` candidates against their target project roots; paths are relative to the projects listed in Platform Units. +Seeded from the project `ARCHITECTURE.md` indexes and source tree, then re-verified against `dev/4.7.0` (`ee77cc85`). Paths are relative to the projects listed in Platform Units. + +Inventory scope: rows were derived from the `Android/APIExample/` case index and its cross-platform counterparts. Cases that exist only outside Android full are **not inventoried yet** — including `ARKit`, `FaceCapture`, `MutliCamera`/`MultiCameraSourece`, `RawMediaData`, `CustomVideoSourceMediaIO`, `VideoChat`, and the Windows-only dialogs (`AudioProfile`, `AudioVolume`, `RegionConn`, `ReportInCall`, `RTMPinject`, `MultiVideoSource`). Their absence from this matrix is a documented inventory gap, not a coverage claim. | Feature | SDK Family | Key APIs | Android full | Android audio | Android Compose | iOS UIKit | iOS SwiftUI | iOS Objective-C | iOS audio | macOS | Windows | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | @@ -62,7 +64,7 @@ Seeded from the overseas `dev/4.6.4` project `ARCHITECTURE.md` indexes and sourc | Data stream messaging | Full RTC | `createDataStream`, `sendStreamMessage` | `DONE(advanced/SendDataStream.java)` | `MISSING` | `DONE(samples/SendDataStream.kt)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | Windows, Android audio, and iOS audio have no data stream case. | | Custom audio source | Full RTC or audio SDK | `createCustomAudioTrack`, `setExternalAudioSource`, `pushExternalAudioFrame` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(samples/CustomAudioSource.kt)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomPCMAudioSource/)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioCapture/)` | Platform APIs differ; compare demonstrated behavior, not only method names. | | Custom audio render | Full RTC or audio SDK | `setExternalAudioSink`, pull playback audio frame APIs | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(samples/CustomAudioRender.kt)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `MISSING` | Windows has no custom audio render case. | -| Custom video source | Full RTC | `setExternalVideoSource`, `pushExternalVideoFrame`, custom video track APIs | `DONE(advanced/PushExternalVideoYUV.java)` | `N/A(audio SDK)` | `DONE(samples/CustomVideoSource.kt)` | `DONE(Advanced/CustomVideoSourcePush/)` | `MISSING` | `DONE(Advanced/CustomVideoSourcePush/)` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoCapture/)` | SwiftUI has CustomVideoRender but no source-push case. Multi-source variants are separate rows. | +| Custom video source | Full RTC | `setExternalVideoSource`, `pushExternalVideoFrame`, custom video track APIs | `DONE(advanced/PushExternalVideoYUV.java)` | `N/A(audio SDK)` | `DONE(samples/CustomVideoSource.kt)` | `DONE(Advanced/CustomVideoSourcePush/)` | `MISSING` | `DONE(Advanced/CustomVideoSourcePush/)` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoCapture/)` | SwiftUI has neither a source-push nor a CustomVideoRender case (re-verified on `dev/4.7.0`). Multi-source variants are separate rows. | | Raw audio data | Full RTC or audio SDK | `registerAudioFrameObserver`, audio frame delegate APIs | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(samples/OriginAudioData.kt)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/OriginalAudio/)` | Check callback threading in every platform review. | | Raw video data | Full RTC | `registerVideoFrameObserver`, video frame delegate APIs | `DONE(advanced/ProcessRawData.java)` | `N/A(audio SDK)` | `DONE(samples/OriginVideoData.kt)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `N/A(audio SDK)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/OriginalVideo/)` | Raw media combined samples are tracked separately. | | Screen sharing | Full RTC | `startScreenCapture`, `updateScreenCaptureParameters`, `stopScreenCapture` | `DONE(advanced/ScreenSharing.java)` | `N/A(audio SDK)` | `DONE(samples/ScreenSharing.kt)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `N/A(audio SDK)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | iOS requires ReplayKit extension; Android requires foreground service. | @@ -81,6 +83,17 @@ Seeded from the overseas `dev/4.6.4` project `ARCHITECTURE.md` indexes and sourc | Content inspect | Full RTC | `enableContentInspect` | `DONE(advanced/ContentInspect.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `N/A(audio SDK)` | `DONE(Advanced/ContentInspect/)` | `MISSING` | Android Compose and Windows have no content inspect case. | | Channel media relay | Full RTC | `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay`, pause/resume APIs | `DONE(advanced/HostAcrossChannel.java)` | `N/A(audio SDK)` | `DONE(samples/HostAcrossChannel.kt)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/MediaChannelRelay/)` | `N/A(audio SDK)` | `DONE(Advanced/ChannelMediaRelay/)` | `DONE(Advanced/CrossChannel/)` | Full coverage across full-RTC projects. | | Picture in picture | Full RTC | `enterPictureInPictureMode` or `AVPictureInPictureController` | `DONE(advanced/PictureInPicture.java)` | `N/A(audio SDK)` | `DONE(samples/PictureInPicture.kt)` | `DONE(Advanced/PictureInPicture/)` | `DONE(Advanced/PictureInPicture/)` | `DONE(Advanced/PictureInPicture/)` | `N/A(audio SDK)` | `MISSING` | `MISSING` | macOS and Windows have no PiP case. | +| Voice beautifier and effects | Full RTC or audio SDK | `setVoiceBeautifierPreset`, `setAudioEffectPreset`, `setVoiceConversionPreset` | `DONE(advanced/VoiceEffects.java)` | `DONE(advanced/VoiceEffects.java)` | `DONE(samples/VoiceEffects.kt)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/BeautyAudio/)` | Full coverage, verified by `setVoiceBeautifierPreset`. Windows bundles it with video beauty in `BeautyAudio`. | +| Audio waveform | Full RTC or audio SDK | `enableAudioVolumeIndication`, audio frame observer waveform rendering | `DONE(audio/AudioWaveform.java)` | `DONE(audio/AudioWaveform.java)` | `MISSING` | `DONE(Advanced/AudioWaveform/)` | `DONE(Advanced/AudioWaveform/)` | `MISSING` | `MISSING` | `MISSING` | `MISSING` | Windows `AudioVolume` adjusts signal volume and is not a waveform case. | +| Third-party player audio routing | Full RTC or audio SDK | third-party player playback combined with the SDK audio route | `DONE(audio/AudioRouterPlayer.java)` | `MISSING` | `MISSING` | `DONE(Advanced/AudioRouterPlayer/)` | `MISSING` | `MISSING` | `MISSING` | `MISSING` | `MISSING` | Android ships Exo/Ijk/Native variants. Confirm the third-party player dependency is available before porting. | +| Agora beauty (video effect object) | Full RTC | `createVideoEffectObject`, `setVideoEffectStringParam`, `setVideoEffectFloatParam` | `DONE(advanced/AgoraBeauty.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/AgoraBeauty/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `DONE(Advanced/AgoraBeauty/)` | `DONE(Advanced/Beauty2.0/)` | Distinct from the built-in `setBeautyEffectOptions` video-enhancement case. Verified by `createVideoEffectObject`. | +| Simulcast | Full RTC | `setSimulcastConfig` | `DONE(advanced/Simulcast.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | `N/A(audio SDK)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | Android Compose is the only full-RTC gap. Verified by `setSimulcastConfig`. | +| Multiple custom video tracks | Full RTC | `createCustomVideoTrack`, `joinChannelEx`, `pushExternalVideoFrameById` | `DONE(advanced/MultiVideoSourceTracks.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `DONE(Advanced/MultiVideoSourceTracks/)` | Case names differ per platform; correspondence verified by `createCustomVideoTrack`. | +| Custom remote video render | Full RTC | remote render path via `registerVideoFrameObserver` or `setVideoFrameDelegate` | `DONE(advanced/CustomRemoteVideoRender.java)` | `N/A(audio SDK)` | `DONE(samples/CustomVideoRender.kt)` | `DONE(Advanced/CustomVideoRender/)` | `MISSING` | `DONE(Advanced/CustomVideoRender/)` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoRender/)` | `MISSING` | SwiftUI has no CustomVideoRender case. Windows `OriginalVideo` is raw video data, tracked in its own row. | +| Transparent background rendering | Full RTC | alpha mask and alpha stitch mode with encoded alpha | `DONE(advanced/TransparentRendering.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/TransparentRender/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/TransparentBg/)` | macOS uses alpha options only inside `VideoProcess`/`AgoraBeauty`, not in a transparent-rendering case. | +| Quick channel switch | Full RTC | rapid `joinChannel` / `leaveChannel` switching across channels | `DONE(advanced/VideoQuickSwitch.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/QuickSwitchChannel/)` | `DONE(Advanced/QuickSwitchChannel/)` | `MISSING` | `N/A(audio SDK)` | `DONE(Advanced/QuickSwitchChannel/)` | `MISSING` | Correspondence is by scenario and case name; this feature has no single distinctive API to verify against. | +| URL live stream player | Full RTC | RTE player `openWithUrl`, `startWithUrl` | `DONE(advanced/UrlLiveStream.java)` | `N/A(audio SDK)` | `MISSING` | `UNKNOWN` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `UNKNOWN` | `UNKNOWN` | iOS UIKit, macOS, and Windows ship an `RtePlayer` case that was not confirmed to demonstrate the same scenario. Run `query-cases` before treating these as gaps. | +| KTV copyright music | None (docs link) | none — the case only opens the Agora documentation page | `DONE(advanced/KtvCopyrightMusic.java)` | `N/A(docs-link case)` | `N/A(docs-link case)` | `DONE(Advanced/KtvCopyrightMusic/)` | `DONE(Advanced/KtvCopyrightMusic/)` | `N/A(docs-link case)` | `N/A(docs-link case)` | `N/A(docs-link case)` | `N/A(docs-link case)` | All three implementations only open a docs URL (Android `WebView`, iOS/SwiftUI hand off to the browser). No SDK surface to port, so absences are not gaps. | ## Confirmed Gaps @@ -101,6 +114,14 @@ These gaps are derived from the pilot matrix above and should be prioritized whe | Content inspect | Android Compose, Windows | Low — moderation feature gap | | Picture in picture | macOS, Windows | Low — platform feature gap | | Custom video source (push) | iOS SwiftUI | Medium — SwiftUI variant gap | +| Audio waveform | Android Compose, iOS Objective-C, iOS audio, macOS, Windows | Medium — audio-visualisation gap on five units | +| Simulcast | Android Compose | Medium — only full-RTC gap for a shipped 4.x feature | +| Custom remote video render | iOS SwiftUI, Windows | Medium — full-RTC variant gap | +| Multiple custom video tracks | Android Compose, iOS SwiftUI, iOS Objective-C | Medium — multi-track gap | +| Transparent background rendering | Android Compose, iOS SwiftUI, iOS Objective-C, macOS | Low — niche rendering feature | +| Quick channel switch | Android Compose, iOS Objective-C, Windows | Low — switching-performance demo gap | +| Agora beauty (video effect object) | Android Compose, iOS SwiftUI, iOS Objective-C | Low — beauty v2 gap | +| Third-party player audio routing | Android audio, Android Compose, iOS SwiftUI, iOS Objective-C, iOS audio, macOS, Windows | Low — depends on third-party player dependencies | ## Maintenance Rules diff --git a/docs/ai-engineering/knowledge-index.md b/docs/ai-engineering/knowledge-index.md index 9f393d275..825440fc3 100644 --- a/docs/ai-engineering/knowledge-index.md +++ b/docs/ai-engineering/knowledge-index.md @@ -13,7 +13,7 @@ Use sources in this order: 5. Project `ARCHITECTURE.md` for case index, registration rules, and canonical file layout. 6. `docs/ai-engineering/repository-profile.json` for repository-specific SDK package names and version sources. 7. `docs/ai-engineering/case-maintenance-matrix.md` for cross-platform parity planning and known gaps. -8. Existing project `.agent/skills/*/SKILL.md` or `.agents/skills/*/SKILL.md` for case query, creation, and review procedures. +8. Existing project `.agents/skills/*/SKILL.md` for case query, creation, and review procedures. 9. `docs/ai-engineering/release-known-issues.md` for release packaging and pipeline risks. 10. Repository hooks and platform build scripts for final verification. @@ -62,7 +62,7 @@ If the product request does not name a platform, default to Android, iOS, macOS, | `macOS/` | `upsert-case`, `review-case` | | `windows/` | `upsert-case`, `review-case` | -The repository-level orchestration skill is `.agent/skills/api-example-release-iteration/SKILL.md`. +The repository-level orchestration skill is `.agents/skills/api-example-release-iteration/SKILL.md`. ## Repository Profile @@ -152,7 +152,7 @@ Where to write it: - Cross-platform routing or implementation traps: `docs/ai-engineering/knowledge-index.md`. - Release, signing, CI, packaging, license, or SDK-version risks: `docs/ai-engineering/release-known-issues.md`. - Case coverage state: `docs/ai-engineering/case-maintenance-matrix.md`. -- Project-specific implementation traps: the target project `ARCHITECTURE.md`, `.agent/skills/*/SKILL.md`, or `.agents/skills/*/SKILL.md`. +- Project-specific implementation traps: the target project `ARCHITECTURE.md` or `.agents/skills/*/SKILL.md`. Do not duplicate the same rule in every file. Put the durable rule at the lowest scope that future agents must read. diff --git a/docs/ai-engineering/release-known-issues.md b/docs/ai-engineering/release-known-issues.md index 2fc609d5c..69f662935 100644 --- a/docs/ai-engineering/release-known-issues.md +++ b/docs/ai-engineering/release-known-issues.md @@ -7,6 +7,7 @@ This file records repository-specific risks that can affect API Example source a | Area | Risk | Repository Gate | | --- | --- | --- | | SDK versions | Android, iOS, macOS, and Windows dependency declarations can drift across release branches. | Validate every source declared in `repository-profile.json` against the target SDK version. | +| iOS full demo SDK source | `iOS/APIExample/Podfile` keeps `pod 'AgoraRtcEngine_iOS'` commented out and builds from the local `sdk.podspec`, which vendors `libs/*.xcframework` and declares an unrelated `spec.version`. No RTC semver is readable from the repository, so `sdk-version-ios` reports `BLOCKED` with `no active SDK version declaration`. A commented-out pod line is never accepted as evidence. | Before release, restore the pinned `pod 'AgoraRtcEngine_iOS', ''` line, or record an explicit waiver stating the SDK was supplied out-of-band and state how its version was confirmed. | | Android extensions | Extension samples can retain headers or native assets from an older SDK. | Review extension `include` files and matching assets whenever the SDK dependency changes. | | Third-party beauty | Sample licenses and bundled assets can expire or become incompatible. | Record the current license or asset limitation when the related example changes. | | iOS/macOS signing | Local verification may not cover external package-signing configuration. | Treat signing and packaging as an external handoff; do not report repository acceptance as package completion. | diff --git a/docs/ai-engineering/templates/release-dry-run-template.md b/docs/ai-engineering/templates/release-dry-run-template.md index 0439c82cc..73de74f61 100644 --- a/docs/ai-engineering/templates/release-dry-run-template.md +++ b/docs/ai-engineering/templates/release-dry-run-template.md @@ -18,7 +18,7 @@ Do not fill this template with stale values from an older run. Re-check the live ## Sources Used - `AGENTS.md` -- `.agent/skills/api-example-release-iteration/SKILL.md` +- `.agents/skills/api-example-release-iteration/SKILL.md` - `docs/ai-engineering/knowledge-index.md` - `docs/ai-engineering/repository-profile.json` - `docs/ai-engineering/release-iteration-gate.md` diff --git a/docs/ai-engineering/tools/lint_agent_assets.py b/docs/ai-engineering/tools/lint_agent_assets.py new file mode 100644 index 000000000..26da6b03a --- /dev/null +++ b/docs/ai-engineering/tools/lint_agent_assets.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +"""Lint the repository AI engineering assets: AGENTS.md, SKILL.md, and skill bundles. + +Each rule guards a drift class that has actually broken these assets before: + +- `skills-directory-name` a project puts its skills in the legacy `.agent/skills/` +- `skill-frontmatter` a SKILL.md has no YAML frontmatter, so it cannot be selected +- `skill-name` `name` is missing, malformed, or disagrees with its directory +- `skill-description` `description` is missing or exceeds the specification limit +- `asset-reference` an AGENTS.md or SKILL.md points at a file that does not exist + +Exit code is 0 when every rule passes and 1 when any finding is reported. +""" + +import argparse +import json +import os +import re +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] + +SKILLS_DIR_NAME = ".agents" +LEGACY_SKILLS_DIR_NAMES = (".agent",) + +# Directories that never hold committed agent assets. Pruning them keeps the walk +# fast on a repository that also contains platform build output. +SKIP_DIR_NAMES = { + ".cxx", + ".git", + ".gradle", + ".idea", + "Pods", + "__pycache__", + "build", + "libs", + "node_modules", + "scope", + "sdk", + "xcuserdata", +} + +ASSET_FILE_NAMES = ("AGENTS.md", "SKILL.md") + +FRONTMATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) +FRONTMATTER_KEY_RE = re.compile(r"^([A-Za-z0-9_-]+):\s*(.*)$") +FOLD_INDICATORS = {">", "|", ">-", "|-", ">+", "|+"} + +# Agent Skills specification: 1-64 chars, lowercase alphanumerics and single hyphens, +# no leading or trailing hyphen. +SKILL_NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +MAX_SKILL_NAME_LENGTH = 64 +MAX_SKILL_DESCRIPTION_LENGTH = 1024 + +INLINE_CODE_RE = re.compile(r"`([^`\n]+)`") +PLACEHOLDER_CHARS = set("<>*?|$()[]") +# Only references that identify an agent asset are resolved. Project source paths are +# written relative to a source root that varies per platform, so checking them here +# would report noise instead of drift. +ASSET_REFERENCE_MARKERS = ( + f"{SKILLS_DIR_NAME}/skills/", + "references/", + "docs/ai-engineering/", +) +ASSET_REFERENCE_SUFFIXES = ("AGENTS.md", "ARCHITECTURE.md", "SKILL.md") + + +class Finding: + """A single rule violation, reported with the location that must be edited.""" + + def __init__(self, rule, path, line, message): + self.rule = rule + self.path = path + self.line = line + self.message = message + + def as_dict(self): + return { + "rule": self.rule, + "path": self.path, + "line": self.line, + "message": self.message, + } + + def format(self): + location = f"{self.path}:{self.line}" if self.line else self.path + return f"{location}: [{self.rule}] {self.message}" + + +def relative_path(path, root): + try: + return Path(path).resolve().relative_to(Path(root).resolve()).as_posix() + except ValueError: + return Path(path).as_posix() + + +def walk_directories(root): + """Yield (dirpath, dirnames) with build and vendor directories pruned in place.""" + for dirpath, dirnames, _ in os.walk(root): + dirnames[:] = sorted(name for name in dirnames if name not in SKIP_DIR_NAMES) + yield dirpath, dirnames + + +def find_asset_files(root, names=ASSET_FILE_NAMES): + found = [] + for dirpath, _ in walk_directories(root): + for name in names: + candidate = Path(dirpath) / name + if candidate.is_file(): + found.append(candidate) + return sorted(found) + + +def parse_frontmatter(text): + """Return the frontmatter fields, or None when the block is absent. + + Only the subset of YAML used by these skills is supported: top-level scalars and + folded block scalars introduced with `>` or `|`. This avoids a PyYAML dependency + that the repository does not otherwise carry. + """ + match = FRONTMATTER_RE.match(text) + if not match: + return None + fields = {} + current_key = None + for raw_line in match.group(1).splitlines(): + if not raw_line.strip(): + continue + indented = raw_line[:1].isspace() + key_match = FRONTMATTER_KEY_RE.match(raw_line.strip()) + if key_match and not indented: + current_key = key_match.group(1) + value = key_match.group(2).strip() + fields[current_key] = "" if value in FOLD_INDICATORS else value + elif current_key is not None and indented: + continuation = raw_line.strip() + existing = fields.get(current_key, "") + fields[current_key] = f"{existing} {continuation}".strip() + return fields + + +def check_skill_directory_names(root): + findings = [] + for dirpath, dirnames in walk_directories(root): + for legacy_name in LEGACY_SKILLS_DIR_NAMES: + if legacy_name not in dirnames: + continue + legacy_dir = Path(dirpath) / legacy_name + if not (legacy_dir / "skills").is_dir(): + continue + findings.append( + Finding( + "skills-directory-name", + relative_path(legacy_dir, root), + 0, + f"skills must live in `{SKILLS_DIR_NAME}/skills/`; " + f"rename this legacy `{legacy_name}/skills/` directory", + ) + ) + return findings + + +def check_skill_metadata(skill_path, root): + findings = [] + rel = relative_path(skill_path, root) + text = skill_path.read_text(encoding="utf-8", errors="replace") + fields = parse_frontmatter(text) + if fields is None: + findings.append( + Finding( + "skill-frontmatter", + rel, + 1, + "missing YAML frontmatter; the file must open with a `---` block " + "declaring `name` and `description`", + ) + ) + return findings + + expected_name = skill_path.parent.name + name = fields.get("name", "").strip() + if not name: + findings.append(Finding("skill-name", rel, 1, "frontmatter is missing `name`")) + else: + if name != expected_name: + findings.append( + Finding( + "skill-name", + rel, + 1, + f"`name: {name}` must match the skill directory name `{expected_name}`", + ) + ) + if len(name) > MAX_SKILL_NAME_LENGTH: + findings.append( + Finding( + "skill-name", + rel, + 1, + f"`name` is {len(name)} characters; the limit is {MAX_SKILL_NAME_LENGTH}", + ) + ) + if not SKILL_NAME_RE.match(name): + findings.append( + Finding( + "skill-name", + rel, + 1, + f"`name: {name}` must use lowercase alphanumerics separated by " + "single hyphens, with no leading or trailing hyphen", + ) + ) + + description = fields.get("description", "").strip() + if not description: + findings.append( + Finding("skill-description", rel, 1, "frontmatter is missing `description`") + ) + elif len(description) > MAX_SKILL_DESCRIPTION_LENGTH: + findings.append( + Finding( + "skill-description", + rel, + 1, + f"`description` is {len(description)} characters; " + f"the limit is {MAX_SKILL_DESCRIPTION_LENGTH}", + ) + ) + return findings + + +def is_asset_reference(ref): + if not ref or ref.startswith(("http://", "https://", "#")): + return False + if any(char in ref for char in PLACEHOLDER_CHARS) or " " in ref: + return False + if ref.endswith(ASSET_REFERENCE_SUFFIXES): + return True + return any(marker in ref for marker in ASSET_REFERENCE_MARKERS) + + +def resolve_reference(ref, source_path, root): + """Resolve a reference against the source directory, then each ancestor. + + Asset docs write sibling paths (`ARCHITECTURE.md`), project-relative paths + (`APIExample/.agents/skills/...`), and repository-relative paths + (`docs/ai-engineering/knowledge-index.md`). Walking up from the source directory + accepts all three without accepting a path that exists nowhere. + """ + root = Path(root).resolve() + candidates = [root / ref] + current = Path(source_path).resolve().parent + while True: + candidates.append(current / ref) + if current == root or root not in current.parents: + break + current = current.parent + return any(candidate.exists() for candidate in candidates) + + +def check_asset_references(asset_path, root): + findings = [] + rel = relative_path(asset_path, root) + seen = set() + for line_number, line in enumerate( + asset_path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1 + ): + for match in INLINE_CODE_RE.finditer(line): + ref = match.group(1).strip().rstrip(",.;:") + if not is_asset_reference(ref): + continue + if ref in seen: + continue + seen.add(ref) + if not resolve_reference(ref, asset_path, root): + findings.append( + Finding( + "asset-reference", + rel, + line_number, + f"referenced path does not exist: {ref}", + ) + ) + return findings + + +def lint(root=REPO_ROOT): + root = Path(root) + findings = list(check_skill_directory_names(root)) + asset_files = find_asset_files(root) + for asset_path in asset_files: + if asset_path.name == "SKILL.md": + findings.extend(check_skill_metadata(asset_path, root)) + findings.extend(check_asset_references(asset_path, root)) + findings.sort(key=lambda finding: (finding.path, finding.line, finding.rule)) + return { + "checked_files": [relative_path(path, root) for path in asset_files], + "findings": [finding.as_dict() for finding in findings], + }, findings + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--root", default=str(REPO_ROOT), help="Repository root to lint") + parser.add_argument("--json", action="store_true", help="Emit findings as JSON") + args = parser.parse_args(argv) + + try: + payload, findings = lint(Path(args.root)) + except OSError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + if args.json: + print(json.dumps(payload, indent=2, ensure_ascii=False)) + else: + for finding in findings: + print(finding.format(), file=sys.stderr) + checked = len(payload["checked_files"]) + if findings: + print( + f"\n{len(findings)} finding(s) across {checked} agent asset file(s)", + file=sys.stderr, + ) + else: + print(f"OK: {checked} agent asset file(s) passed all rules") + return 1 if findings else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/ai-engineering/tools/lint_agent_assets_test.py b/docs/ai-engineering/tools/lint_agent_assets_test.py new file mode 100644 index 000000000..9fc260431 --- /dev/null +++ b/docs/ai-engineering/tools/lint_agent_assets_test.py @@ -0,0 +1,332 @@ +import json +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +LINTER = REPO_ROOT / "docs/ai-engineering/tools/lint_agent_assets.py" + +sys.path.insert(0, str(LINTER.parent)) +from lint_agent_assets import lint, parse_frontmatter + + +class LintAgentAssetsTest(unittest.TestCase): + def write(self, root, rel_path, text): + path = Path(root) / rel_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(textwrap.dedent(text).lstrip("\n"), encoding="utf-8") + return path + + def write_skill(self, root, project, skill, name=None, description="Does a thing. Use when asked.", body="Body.\n"): + name = skill if name is None else name + path = Path(root) / f"{project}/.agents/skills/{skill}/SKILL.md" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + f"---\nname: {name}\ndescription: {description}\n---\n\n{body}", + encoding="utf-8", + ) + return path + + def write_clean_tree(self, root): + """A minimal tree that must satisfy every rule.""" + self.write(root, "AGENTS.md", "# Root\n\nSee `Android/AGENTS.md`.\n") + self.write(root, "Android/AGENTS.md", "# Android\n\nSee `APIExample/AGENTS.md`.\n") + self.write( + root, + "Android/APIExample/AGENTS.md", + """ + # APIExample + + | Skill | Path | + |-------|------| + | upsert-case | `.agents/skills/upsert-case/` | + + See `ARCHITECTURE.md`. + """, + ) + self.write(root, "Android/APIExample/ARCHITECTURE.md", "# Architecture\n") + self.write_skill(root, "Android/APIExample", "upsert-case") + + def rules(self, findings): + return sorted(finding.rule for finding in findings) + + def messages(self, findings): + return "\n".join(finding.format() for finding in findings) + + def test_clean_tree_reports_no_findings(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + + payload, findings = lint(tmpdir) + + self.assertEqual(findings, [], self.messages(findings)) + self.assertIn("Android/APIExample/.agents/skills/upsert-case/SKILL.md", payload["checked_files"]) + self.assertEqual(payload["findings"], []) + + def test_flags_legacy_agent_skills_directory(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "iOS/APIExample/.agent/skills/review-case/SKILL.md", + """ + --- + name: review-case + description: Reviews a case. Use when reviewing. + --- + + Body. + """, + ) + + _, findings = lint(tmpdir) + + self.assertIn("skills-directory-name", self.rules(findings)) + self.assertIn("iOS/APIExample/.agent", self.messages(findings)) + + def test_ignores_dot_agent_directory_without_skills(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + (Path(tmpdir) / "iOS/.agent/notes").mkdir(parents=True) + + _, findings = lint(tmpdir) + + self.assertEqual(findings, [], self.messages(findings)) + + def test_flags_skill_without_frontmatter(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "Android/APIExample/.agents/skills/query-cases/SKILL.md", + "## What this skill is for\n\nBody.\n", + ) + + _, findings = lint(tmpdir) + + self.assertEqual(self.rules(findings), ["skill-frontmatter"]) + + def test_flags_name_that_disagrees_with_directory(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write_skill(tmpdir, "Android/APIExample", "query-cases", name="query-case") + + _, findings = lint(tmpdir) + + self.assertEqual(self.rules(findings), ["skill-name"]) + self.assertIn("must match the skill directory name `query-cases`", self.messages(findings)) + + def test_flags_malformed_name_charset(self): + for bad_name in ["Upsert_Case", "-upsert", "upsert-", "upsert--case"]: + with self.subTest(name=bad_name), tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + f"Android/APIExample/.agents/skills/{bad_name}/SKILL.md", + f""" + --- + name: {bad_name} + description: Does a thing. Use when asked. + --- + + Body. + """, + ) + + _, findings = lint(tmpdir) + + self.assertIn("skill-name", self.rules(findings), self.messages(findings)) + + def test_flags_missing_and_oversized_description(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "Android/APIExample/.agents/skills/query-cases/SKILL.md", + """ + --- + name: query-cases + --- + + Body. + """, + ) + + _, findings = lint(tmpdir) + + self.assertEqual(self.rules(findings), ["skill-description"]) + + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write_skill(tmpdir, "Android/APIExample", "query-cases", description="x" * 1025) + + _, findings = lint(tmpdir) + + self.assertEqual(self.rules(findings), ["skill-description"]) + self.assertIn("the limit is 1024", self.messages(findings)) + + def test_accepts_description_at_the_limit(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write_skill(tmpdir, "Android/APIExample", "query-cases", description="x" * 1024) + + _, findings = lint(tmpdir) + + self.assertEqual(findings, [], self.messages(findings)) + + def test_flags_agents_md_pointing_at_missing_skill_directory(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "Android/APIExample/AGENTS.md", + """ + # APIExample + + | review-case | `.agents/skills/review-case/` | + + See `ARCHITECTURE.md`. + """, + ) + + _, findings = lint(tmpdir) + + self.assertEqual(self.rules(findings), ["asset-reference"]) + self.assertIn(".agents/skills/review-case/", self.messages(findings)) + + def test_flags_skill_pointing_at_missing_bundled_reference(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write_skill( + tmpdir, + "Android/APIExample", + "upsert-case", + body="Use `APIExample/.agents/skills/upsert-case/references/fragment-template.java`.\n", + ) + + _, findings = lint(tmpdir) + + self.assertEqual(self.rules(findings), ["asset-reference"]) + self.assertIn("fragment-template.java", self.messages(findings)) + + def test_resolves_project_relative_reference_via_ancestor_walk(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write_skill( + tmpdir, + "Android/APIExample", + "upsert-case", + body="Use `APIExample/.agents/skills/upsert-case/references/fragment-template.java`.\n", + ) + self.write( + tmpdir, + "Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java", + "class Template {}\n", + ) + + _, findings = lint(tmpdir) + + self.assertEqual(findings, [], self.messages(findings)) + + def test_skips_placeholder_and_url_references(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write_skill( + tmpdir, + "Android/APIExample", + "upsert-case", + body=( + "Read `/AGENTS.md` and `/ARCHITECTURE.md`.\n" + "Glob `.agents/skills/*/SKILL.md`.\n" + "See https://agentskills.io/specification for the spec.\n" + ), + ) + + _, findings = lint(tmpdir) + + self.assertEqual(findings, [], self.messages(findings)) + + def test_prunes_build_and_vendor_directories(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "Android/APIExample/build/.agents/skills/stale/SKILL.md", + "no frontmatter here\n", + ) + self.write(tmpdir, "Pods/AGENTS.md", "Points at `missing/AGENTS.md`.\n") + + payload, findings = lint(tmpdir) + + self.assertEqual(findings, [], self.messages(findings)) + self.assertFalse([path for path in payload["checked_files"] if "build/" in path]) + self.assertFalse([path for path in payload["checked_files"] if path.startswith("Pods/")]) + + def test_parses_folded_block_scalar_description(self): + fields = parse_frontmatter( + textwrap.dedent( + """ + --- + name: upsert-case + description: > + Add a new case or modify an existing one. + Use when: adding a demo screen. + --- + + Body. + """ + ).lstrip("\n") + ) + + self.assertEqual(fields["name"], "upsert-case") + self.assertEqual( + fields["description"], + "Add a new case or modify an existing one. Use when: adding a demo screen.", + ) + + def test_returns_none_without_frontmatter(self): + self.assertIsNone(parse_frontmatter("## Heading\n\nBody.\n")) + + def test_cli_reports_findings_and_exits_nonzero(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "Android/APIExample/.agents/skills/query-cases/SKILL.md", + "## No frontmatter\n", + ) + + result = subprocess.run( + [sys.executable, str(LINTER), "--root", tmpdir, "--json"], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + self.assertEqual(result.returncode, 1, result.stderr) + payload = json.loads(result.stdout) + self.assertEqual( + [finding["rule"] for finding in payload["findings"]], ["skill-frontmatter"] + ) + + def test_cli_passes_on_the_live_repository(self): + result = subprocess.run( + [sys.executable, str(LINTER)], + cwd=REPO_ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/ai-engineering/tools/prepare_case_execution.py b/docs/ai-engineering/tools/prepare_case_execution.py index 8d0223036..c62e47524 100644 --- a/docs/ai-engineering/tools/prepare_case_execution.py +++ b/docs/ai-engineering/tools/prepare_case_execution.py @@ -210,8 +210,13 @@ def sdk_version_pattern(source): if kind == "gradle-property": return rf"(?m)^\s*{re.escape(source['key'])}\s*=\s*{SEMVER_CAPTURE}\s*$" if kind == "cocoapods": + # Anchor to the start of a line and reject Ruby comments so a commented-out + # `# pod 'Pkg', 'x.y.z'` line can never be accepted as a live declaration. package = re.escape(source["package"]) - return rf"pod\s+['\"]{package}['\"]\s*,\s*['\"]{SEMVER_CAPTURE}['\"]" + return ( + rf"(?m)^(?!\s*#)\s*pod\s+['\"]{package}['\"]" + rf"\s*,\s*['\"]{SEMVER_CAPTURE}['\"]" + ) if kind == "archive-name": return re.escape(source["prefix"]) + SEMVER_CAPTURE + re.escape(source["suffix"]) raise ValueError(f"unsupported SDK version source kind: {kind}") @@ -240,7 +245,11 @@ def collect_sdk_version_checks( continue pattern = sdk_version_pattern(source) matches = sorted(set(re.findall(pattern, path.read_text(encoding="utf-8")))) - if len(matches) != 1: + if not matches: + actual_versions[path_text] = "" + problems.append(f"no active SDK version declaration in {path_text}") + continue + if len(matches) > 1: actual_versions[path_text] = "" problems.append(f"expected one SDK version in {path_text}, found {len(matches)}") continue diff --git a/docs/ai-engineering/tools/prepare_case_execution_test.py b/docs/ai-engineering/tools/prepare_case_execution_test.py index ea50dcd24..a4195fa1c 100644 --- a/docs/ai-engineering/tools/prepare_case_execution_test.py +++ b/docs/ai-engineering/tools/prepare_case_execution_test.py @@ -206,6 +206,51 @@ def test_collects_live_sdk_version_evidence(self): self.assertEqual(windows["result"], "BLOCKED") self.assertIn("4.6.2", windows["reason"]) + def test_rejects_commented_out_cocoapods_declaration(self): + for comment_line in [ + " # pod 'AgoraRtcEngine_iOS', '4.6.4'", + "#pod 'AgoraRtcEngine_iOS', '4.6.4'", + "\t# pod 'AgoraRtcEngine_iOS', '4.6.4'", + ]: + with self.subTest(comment_line=comment_line), tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + profile_path = self.write_repository_profile(root, "Agora") + (root / "iOS/Podfile").write_text( + f"def common_pods\n{comment_line}\n" + " pod 'sdk', :path => 'sdk.podspec'\nend\n", + encoding="utf-8", + ) + + checks = collect_sdk_version_checks( + self.target_sdk_versions(), + repo_root=root, + profile_path=profile_path, + ) + ios = next(check for check in checks if check["name"] == "sdk-version-ios") + + self.assertEqual(ios["result"], "BLOCKED") + self.assertEqual(ios["actual_versions"]["iOS/Podfile"], "") + self.assertIn("no active SDK version declaration in iOS/Podfile", ios["reason"]) + + def test_accepts_cocoapods_declaration_with_trailing_comment(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + profile_path = self.write_repository_profile(root, "Agora") + (root / "iOS/Podfile").write_text( + " pod 'AgoraRtcEngine_iOS', '4.6.4' # pinned for this release\n", + encoding="utf-8", + ) + + checks = collect_sdk_version_checks( + self.target_sdk_versions(), + repo_root=root, + profile_path=profile_path, + ) + ios = next(check for check in checks if check["name"] == "sdk-version-ios") + + self.assertEqual(ios["result"], "PASS") + self.assertEqual(ios["actual_versions"]["iOS/Podfile"], "4.6.4") + def test_collects_staggered_platform_sdk_versions(self): with tempfile.TemporaryDirectory() as tmpdir: root = Path(tmpdir) diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest.py b/docs/ai-engineering/tools/validate_acceptance_manifest.py index 215fec666..72c455dbf 100644 --- a/docs/ai-engineering/tools/validate_acceptance_manifest.py +++ b/docs/ai-engineering/tools/validate_acceptance_manifest.py @@ -1090,8 +1090,6 @@ def is_durable_knowledge_path(path_text): return ( normalized in DURABLE_KNOWLEDGE_PATHS or normalized.endswith("/ARCHITECTURE.md") - or normalized.startswith(".agent/skills/") - or "/.agent/skills/" in normalized or normalized.startswith(".agents/skills/") or "/.agents/skills/" in normalized ) diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py index b805d592f..5c5f671d5 100644 --- a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py +++ b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py @@ -671,14 +671,23 @@ def test_knowledge_update_requires_a_durable_file_change(self): self.assert_error_contains(manifest, "knowledge_updates require at least one durable knowledge") - def test_durable_knowledge_accepts_both_skill_directory_conventions(self): - self.assertTrue(is_durable_knowledge_path(".agent/skills/review-case/SKILL.md")) + def test_durable_knowledge_accepts_repository_and_project_skill_paths(self): + self.assertTrue(is_durable_knowledge_path(".agents/skills/review-case/SKILL.md")) self.assertTrue( is_durable_knowledge_path( "Android/APIExample/.agents/skills/review-case/SKILL.md" ) ) + def test_durable_knowledge_rejects_legacy_agent_skill_directory(self): + # The repository standardized on `.agents/skills/`; the legacy `.agent/skills/` + # spelling must not silently pass or the naming split can reappear. + self.assertFalse( + is_durable_knowledge_path( + "Android/APIExample/.agent/skills/review-case/SKILL.md" + ) + ) + def test_evidence_files_are_rehashed_from_manifest_directory(self): manifest = base_manifest() with tempfile.TemporaryDirectory() as tmpdir: diff --git a/iOS/APIExample-Audio/.agent/skills/query-cases/SKILL.md b/iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md similarity index 100% rename from iOS/APIExample-Audio/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md diff --git a/iOS/APIExample-Audio/.agent/skills/review-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md similarity index 100% rename from iOS/APIExample-Audio/.agent/skills/review-case/SKILL.md rename to iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md diff --git a/iOS/APIExample-Audio/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md similarity index 100% rename from iOS/APIExample-Audio/.agent/skills/upsert-case/SKILL.md rename to iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md diff --git a/iOS/APIExample-Audio/AGENTS.md b/iOS/APIExample-Audio/AGENTS.md index f42c6f8a8..413d19fb6 100644 --- a/iOS/APIExample-Audio/AGENTS.md +++ b/iOS/APIExample-Audio/AGENTS.md @@ -23,9 +23,9 @@ To obtain an App ID, see [README.md](README.md#obtain-an-app-id). | Task | Skill | When to use | |------|-------|-------------| -| Add or modify a case | `.agent/skills/upsert-case/` | Need to create a new audio API demo or update an existing one | -| Code review | `.agent/skills/review-case/` | Review case code for lifecycle, thread safety, and audio-only convention compliance | -| Find an existing case | `.agent/skills/query-cases/` | Locate which file demonstrates a specific API or feature | +| Add or modify a case | `.agents/skills/upsert-case/` | Need to create a new audio API demo or update an existing one | +| Code review | `.agents/skills/review-case/` | Review case code for lifecycle, thread safety, and audio-only convention compliance | +| Find an existing case | `.agents/skills/query-cases/` | Locate which file demonstrates a specific API or feature | ## Further Reading diff --git a/iOS/APIExample-OC/.agent/skills/query-cases/SKILL.md b/iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md similarity index 100% rename from iOS/APIExample-OC/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md diff --git a/iOS/APIExample-OC/.agent/skills/review-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md similarity index 100% rename from iOS/APIExample-OC/.agent/skills/review-case/SKILL.md rename to iOS/APIExample-OC/.agents/skills/review-case/SKILL.md diff --git a/iOS/APIExample-OC/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md similarity index 100% rename from iOS/APIExample-OC/.agent/skills/upsert-case/SKILL.md rename to iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md diff --git a/iOS/APIExample-OC/AGENTS.md b/iOS/APIExample-OC/AGENTS.md index 5bf171d06..b61ef6107 100644 --- a/iOS/APIExample-OC/AGENTS.md +++ b/iOS/APIExample-OC/AGENTS.md @@ -28,9 +28,9 @@ To obtain an App ID, see [README.md](README.md#obtain-an-app-id). | Task | Skill | When to use | |------|-------|-------------| -| Add or modify a case | `.agent/skills/upsert-case/` | Need to create a new API demo or update an existing one | -| Code review | `.agent/skills/review-case/` | Review case code for lifecycle, thread safety, and OC convention compliance | -| Find an existing case | `.agent/skills/query-cases/` | Locate which file demonstrates a specific API or feature | +| Add or modify a case | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | +| Code review | `.agents/skills/review-case/` | Review case code for lifecycle, thread safety, and OC convention compliance | +| Find an existing case | `.agents/skills/query-cases/` | Locate which file demonstrates a specific API or feature | ## Further Reading diff --git a/iOS/APIExample-SwiftUI/.agent/skills/query-cases/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md similarity index 100% rename from iOS/APIExample-SwiftUI/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md diff --git a/iOS/APIExample-SwiftUI/.agent/skills/review-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md similarity index 100% rename from iOS/APIExample-SwiftUI/.agent/skills/review-case/SKILL.md rename to iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md diff --git a/iOS/APIExample-SwiftUI/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md similarity index 100% rename from iOS/APIExample-SwiftUI/.agent/skills/upsert-case/SKILL.md rename to iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md diff --git a/iOS/APIExample-SwiftUI/AGENTS.md b/iOS/APIExample-SwiftUI/AGENTS.md index 1ca46420c..a4a466a8e 100644 --- a/iOS/APIExample-SwiftUI/AGENTS.md +++ b/iOS/APIExample-SwiftUI/AGENTS.md @@ -23,9 +23,9 @@ To obtain an App ID, see [README.md](README.md#obtain-an-app-id). | Task | Skill | When to use | |------|-------|-------------| -| Add or modify a case | `.agent/skills/upsert-case/` | Need to create a new API demo or update an existing one | -| Code review | `.agent/skills/review-case/` | Review case code for lifecycle, thread safety, and SwiftUI convention compliance | -| Find an existing case | `.agent/skills/query-cases/` | Locate which file demonstrates a specific API or feature | +| Add or modify a case | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | +| Code review | `.agents/skills/review-case/` | Review case code for lifecycle, thread safety, and SwiftUI convention compliance | +| Find an existing case | `.agents/skills/query-cases/` | Locate which file demonstrates a specific API or feature | ## Further Reading diff --git a/iOS/APIExample/.agent/skills/query-cases/SKILL.md b/iOS/APIExample/.agents/skills/query-cases/SKILL.md similarity index 100% rename from iOS/APIExample/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample/.agents/skills/query-cases/SKILL.md diff --git a/iOS/APIExample/.agent/skills/review-case/SKILL.md b/iOS/APIExample/.agents/skills/review-case/SKILL.md similarity index 100% rename from iOS/APIExample/.agent/skills/review-case/SKILL.md rename to iOS/APIExample/.agents/skills/review-case/SKILL.md diff --git a/iOS/APIExample/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample/.agents/skills/upsert-case/SKILL.md similarity index 100% rename from iOS/APIExample/.agent/skills/upsert-case/SKILL.md rename to iOS/APIExample/.agents/skills/upsert-case/SKILL.md diff --git a/iOS/APIExample/AGENTS.md b/iOS/APIExample/AGENTS.md index 2401e937b..efa93e311 100644 --- a/iOS/APIExample/AGENTS.md +++ b/iOS/APIExample/AGENTS.md @@ -23,9 +23,9 @@ To obtain an App ID, see [README.md](README.md#obtain-an-app-id). | Task | Skill | When to use | |------|-------|-------------| -| Add or modify a case | `.agent/skills/upsert-case/` | Need to create a new API demo or update an existing one | -| Code review | `.agent/skills/review-case/` | Review case code for lifecycle, thread safety, and convention compliance | -| Find an existing case | `.agent/skills/query-cases/` | Locate which file demonstrates a specific API or feature | +| Add or modify a case | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | +| Code review | `.agents/skills/review-case/` | Review case code for lifecycle, thread safety, and convention compliance | +| Find an existing case | `.agents/skills/query-cases/` | Locate which file demonstrates a specific API or feature | ## Further Reading diff --git a/macOS/.agent/skills/review-case/SKILL.md b/macOS/.agents/skills/review-case/SKILL.md similarity index 100% rename from macOS/.agent/skills/review-case/SKILL.md rename to macOS/.agents/skills/review-case/SKILL.md diff --git a/macOS/.agent/skills/review-case/references/correct-lifecycle.swift b/macOS/.agents/skills/review-case/references/correct-lifecycle.swift similarity index 100% rename from macOS/.agent/skills/review-case/references/correct-lifecycle.swift rename to macOS/.agents/skills/review-case/references/correct-lifecycle.swift diff --git a/macOS/.agent/skills/review-case/references/correct-thread-safety.swift b/macOS/.agents/skills/review-case/references/correct-thread-safety.swift similarity index 100% rename from macOS/.agent/skills/review-case/references/correct-thread-safety.swift rename to macOS/.agents/skills/review-case/references/correct-thread-safety.swift diff --git a/macOS/.agent/skills/review-case/references/incorrect-lifecycle.swift b/macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift similarity index 100% rename from macOS/.agent/skills/review-case/references/incorrect-lifecycle.swift rename to macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift diff --git a/macOS/.agent/skills/review-case/references/incorrect-thread-safety.swift b/macOS/.agents/skills/review-case/references/incorrect-thread-safety.swift similarity index 100% rename from macOS/.agent/skills/review-case/references/incorrect-thread-safety.swift rename to macOS/.agents/skills/review-case/references/incorrect-thread-safety.swift diff --git a/macOS/.agent/skills/upsert-case/SKILL.md b/macOS/.agents/skills/upsert-case/SKILL.md similarity index 100% rename from macOS/.agent/skills/upsert-case/SKILL.md rename to macOS/.agents/skills/upsert-case/SKILL.md diff --git a/macOS/.agent/skills/upsert-case/references/example-template.swift b/macOS/.agents/skills/upsert-case/references/example-template.swift similarity index 100% rename from macOS/.agent/skills/upsert-case/references/example-template.swift rename to macOS/.agents/skills/upsert-case/references/example-template.swift diff --git a/macOS/.agent/skills/upsert-case/references/lifecycle-pattern.swift b/macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift similarity index 100% rename from macOS/.agent/skills/upsert-case/references/lifecycle-pattern.swift rename to macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift diff --git a/macOS/.agent/skills/upsert-case/references/thread-safety-pattern.swift b/macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift similarity index 100% rename from macOS/.agent/skills/upsert-case/references/thread-safety-pattern.swift rename to macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift diff --git a/macOS/AGENTS.md b/macOS/AGENTS.md index 2116452eb..9de63e1de 100644 --- a/macOS/AGENTS.md +++ b/macOS/AGENTS.md @@ -61,9 +61,9 @@ All work must conform to the rules defined in `ARCHITECTURE.md`: ### Use Project-Level SKILLs -For broader tasks, use the skills in `.agent/skills/`: +For broader tasks, use the skills in `.agents/skills/`: | Task | Skill | When to use | |------|-------|-------------| -| Add or modify an example | `.agent/skills/upsert-case/` | Need to create a new API demo or update an existing one | -| Code review | `.agent/skills/review-case/` | Review example code for lifecycle, thread safety, and convention compliance | +| Add or modify an example | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | +| Code review | `.agents/skills/review-case/` | Review example code for lifecycle, thread safety, and convention compliance | diff --git a/macOS/ARCHITECTURE.md b/macOS/ARCHITECTURE.md index e44673345..ad24858be 100644 --- a/macOS/ARCHITECTURE.md +++ b/macOS/ARCHITECTURE.md @@ -34,7 +34,7 @@ macOS/ ├── APIExample.xcworkspace/ # Xcode workspace ├── libs/ # SDK libraries ├── Pods/ # CocoaPods dependencies -├── .agent/skills/ # Agent skills +├── .agents/skills/ # Agent skills │ ├── create-api-example/ │ ├── find-api-example/ │ └── migrate-api-to-project/ diff --git a/windows/.agent/skills/review-case/SKILL.md b/windows/.agents/skills/review-case/SKILL.md similarity index 100% rename from windows/.agent/skills/review-case/SKILL.md rename to windows/.agents/skills/review-case/SKILL.md diff --git a/windows/.agent/skills/review-case/references/correct-lifecycle.cpp b/windows/.agents/skills/review-case/references/correct-lifecycle.cpp similarity index 100% rename from windows/.agent/skills/review-case/references/correct-lifecycle.cpp rename to windows/.agents/skills/review-case/references/correct-lifecycle.cpp diff --git a/windows/.agent/skills/review-case/references/correct-thread-safety.cpp b/windows/.agents/skills/review-case/references/correct-thread-safety.cpp similarity index 100% rename from windows/.agent/skills/review-case/references/correct-thread-safety.cpp rename to windows/.agents/skills/review-case/references/correct-thread-safety.cpp diff --git a/windows/.agent/skills/review-case/references/incorrect-lifecycle.cpp b/windows/.agents/skills/review-case/references/incorrect-lifecycle.cpp similarity index 100% rename from windows/.agent/skills/review-case/references/incorrect-lifecycle.cpp rename to windows/.agents/skills/review-case/references/incorrect-lifecycle.cpp diff --git a/windows/.agent/skills/review-case/references/incorrect-thread-safety.cpp b/windows/.agents/skills/review-case/references/incorrect-thread-safety.cpp similarity index 100% rename from windows/.agent/skills/review-case/references/incorrect-thread-safety.cpp rename to windows/.agents/skills/review-case/references/incorrect-thread-safety.cpp diff --git a/windows/.agent/skills/upsert-case/SKILL.md b/windows/.agents/skills/upsert-case/SKILL.md similarity index 100% rename from windows/.agent/skills/upsert-case/SKILL.md rename to windows/.agents/skills/upsert-case/SKILL.md diff --git a/windows/.agent/skills/upsert-case/references/event-handler-pattern.cpp b/windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp similarity index 100% rename from windows/.agent/skills/upsert-case/references/event-handler-pattern.cpp rename to windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp diff --git a/windows/.agent/skills/upsert-case/references/example-template.cpp b/windows/.agents/skills/upsert-case/references/example-template.cpp similarity index 100% rename from windows/.agent/skills/upsert-case/references/example-template.cpp rename to windows/.agents/skills/upsert-case/references/example-template.cpp diff --git a/windows/.agent/skills/upsert-case/references/example-template.h b/windows/.agents/skills/upsert-case/references/example-template.h similarity index 100% rename from windows/.agent/skills/upsert-case/references/example-template.h rename to windows/.agents/skills/upsert-case/references/example-template.h diff --git a/windows/.agent/skills/upsert-case/references/lifecycle-pattern.cpp b/windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp similarity index 100% rename from windows/.agent/skills/upsert-case/references/lifecycle-pattern.cpp rename to windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp diff --git a/windows/.agent/skills/upsert-case/references/message-map-pattern.cpp b/windows/.agents/skills/upsert-case/references/message-map-pattern.cpp similarity index 100% rename from windows/.agent/skills/upsert-case/references/message-map-pattern.cpp rename to windows/.agents/skills/upsert-case/references/message-map-pattern.cpp diff --git a/windows/AGENTS.md b/windows/AGENTS.md index 5e3601839..0f301373a 100644 --- a/windows/AGENTS.md +++ b/windows/AGENTS.md @@ -62,9 +62,9 @@ All work must conform to the rules defined in `ARCHITECTURE.md`: ### Use Project-Level SKILLs -For broader tasks, use the skills in `.agent/skills/`: +For broader tasks, use the skills in `.agents/skills/`: | Task | Skill | When to use | |------|-------|-------------| -| Add or modify an example | `.agent/skills/upsert-case/` | Need to create a new API demo or update an existing one | -| Code review | `.agent/skills/review-case/` | Review example code for lifecycle, thread safety, and convention compliance | +| Add or modify an example | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | +| Code review | `.agents/skills/review-case/` | Review example code for lifecycle, thread safety, and convention compliance | diff --git a/windows/ARCHITECTURE.md b/windows/ARCHITECTURE.md index 5affa57d0..9492b4d2b 100644 --- a/windows/ARCHITECTURE.md +++ b/windows/ARCHITECTURE.md @@ -39,7 +39,7 @@ windows/ │ ├── APIExample.sln # Visual Studio solution │ ├── cicd/ # CI/CD scripts │ └── .vscode/ # VS Code configuration -├── .agent/skills/ # Agent skills +├── .agents/skills/ # Agent skills │ ├── create-api-example/ │ ├── find-api-example/ │ └── migrate-api-to-project/ From 9b1b584e7a15d100f019bdee47a8daa4b784eb79 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 25 Aug 2026 11:12:17 +0800 Subject: [PATCH 32/49] chore: update ai-engineering --- .../api-example-release-iteration/SKILL.md | 8 +- .github/workflows/ai-engineering-checks.yml | 31 +- .../ai-engineering/case-maintenance-matrix.md | 149 ++++----- docs/ai-engineering/knowledge-index.md | 40 +-- docs/ai-engineering/release-iteration-gate.md | 9 +- docs/ai-engineering/release-known-issues.md | 4 +- docs/ai-engineering/repository-profile.json | 29 +- .../acceptance-manifest-template.json | 2 + docs/ai-engineering/tools/README.md | 6 +- .../tools/generate_case_backlog.py | 17 +- .../tools/generate_case_backlog_test.py | 56 ++-- .../ai-engineering/tools/lint_agent_assets.py | 21 +- .../tools/lint_agent_assets_test.py | 21 ++ .../tools/orchestrate_case_execution.py | 116 ++++++- .../tools/orchestrate_case_execution_test.py | 204 +++++++++++- .../tools/prepare_case_execution.py | 69 +++- .../tools/prepare_case_execution_test.py | 144 ++++++++- .../tools/validate_acceptance_manifest.py | 303 +++++++++++++++++- .../validate_acceptance_manifest_test.py | 215 ++++++++++++- macOS/.agents/skills/query-cases/SKILL.md | 50 +++ macOS/AGENTS.md | 1 + macOS/ARCHITECTURE.md | 7 +- windows/.agents/skills/query-cases/SKILL.md | 54 ++++ windows/AGENTS.md | 1 + windows/ARCHITECTURE.md | 7 +- 25 files changed, 1307 insertions(+), 257 deletions(-) create mode 100644 macOS/.agents/skills/query-cases/SKILL.md create mode 100644 windows/.agents/skills/query-cases/SKILL.md diff --git a/.agents/skills/api-example-release-iteration/SKILL.md b/.agents/skills/api-example-release-iteration/SKILL.md index 6c469dfde..e87ecb2e8 100644 --- a/.agents/skills/api-example-release-iteration/SKILL.md +++ b/.agents/skills/api-example-release-iteration/SKILL.md @@ -17,7 +17,7 @@ Identify before editing: - Verification budget and available platform hosts. - Target RTC SDK version for each platform. Platforms may remain on different release lines. -Android, iOS, macOS, and Windows are required by default. Contract may choose the appropriate full/audio/Compose/UIKit/Objective-C project for each platform; marking a platform not required needs an explicit waiver reason. +Android, iOS, macOS, and Windows are required by default. Their targets are fixed to `Android/APIExample/`, `iOS/APIExample/`, `macOS/`, and `windows/`. Audio, Compose, SwiftUI, and Objective-C variants are outside this workflow; marking a primary platform not required needs an explicit waiver reason. ## Source Route @@ -60,7 +60,7 @@ python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ `--target-sdk-version` supplies the baseline for all four platforms. Repeat `--platform-sdk-version platform=x.y.z` only for platforms whose target differs. The execution package and manifest store the resolved four-platform mapping, so staggered platform releases never require script or repository-profile changes. -The default `docs/ai-engineering/repository-profile.json` supplies repository-specific SDK package names and version sources. Use `--repository-profile` only for a checked-in alternative profile. The execution package records its repository-relative path and SHA-256. +The default `docs/ai-engineering/repository-profile.json` supplies repository-specific SDK package names and version sources. Use `--repository-profile` only for a checked-in alternative profile. The execution package, input snapshots, and acceptance manifest record its repository-relative path and SHA-256; the standalone validator recomputes release checks from that binding. For a new requirement not yet actionable in the matrix, also pass `--sdk-family ""` and repeat `--key-api ""` as needed. @@ -102,7 +102,7 @@ python3 docs/ai-engineering/tools/orchestrate_case_execution.py assemble \ ## Platform Rules -- Contract selects the target project and allowed files for each platform. +- Contract preserves the fixed primary target project and defines allowed files for each platform. - The dispatcher runs platform roles from that target project. In a shared checkout it refreshes the package/dependency snapshot, completes and reconciles one Implementation before starting the next, derives that role's cumulative net repository delta, and rejects changes outside Contract paths. - Each Implementation agent follows only its platform/project instructions. - Platform source files, dependencies, and build scripts are never shared across roots. @@ -117,7 +117,7 @@ On macOS, Windows Verification may perform repository-local static review only. ## Matrix And Release -Each platform Implementation may propose zero or more matrix updates. `DONE` requires final `PASS`, platform parity/build `PASS`, and no skipped checks. Release data is limited to repository SDK-version consistency. Jenkins packaging, QA validation, artifact distribution, and external website publication are outside this workflow and must not be recorded as manifest gates. +Each platform Implementation may propose zero or more matrix updates. `DONE` requires final `PASS`, platform parity/build `PASS`, and no skipped checks. Release data is limited to repository SDK-version consistency. An externally injected SDK without a trustworthy tracked version source keeps its platform SDK check and final acceptance `BLOCKED`; build success is not version evidence. Jenkins packaging, QA validation, artifact distribution, and external website publication are outside this workflow and must not be recorded as manifest gates. ## Final Output diff --git a/.github/workflows/ai-engineering-checks.yml b/.github/workflows/ai-engineering-checks.yml index e28dfdfb2..dd66c6597 100644 --- a/.github/workflows/ai-engineering-checks.yml +++ b/.github/workflows/ai-engineering-checks.yml @@ -1,8 +1,8 @@ name: AI Engineering Checks # Guards the AI engineering assets (AGENTS.md routing, SKILL.md bundles) and the -# orchestration tooling under docs/ai-engineering/tools/. These checks are file-only: -# they never build a platform sample, so they stay fast enough to run on every PR. +# orchestration tooling under docs/ai-engineering/tools/. These checks are file-only +# and run on every host family used by verification; platform builds remain in Azure. on: push: @@ -36,9 +36,16 @@ concurrency: jobs: checks: - name: Lint assets and run tooling tests - runs-on: ubuntu-latest + name: Tooling checks (${{ matrix.os }}) + runs-on: ${{ matrix.os }} timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest env: PYTHONDONTWRITEBYTECODE: '1' steps: @@ -52,13 +59,13 @@ jobs: python-version: '3.11' - name: Lint AI engineering assets - run: python3 docs/ai-engineering/tools/lint_agent_assets.py + run: python docs/ai-engineering/tools/lint_agent_assets.py - name: Run AI engineering tooling tests - run: | - python3 -m unittest \ - docs/ai-engineering/tools/lint_agent_assets_test.py \ - docs/ai-engineering/tools/validate_acceptance_manifest_test.py \ - docs/ai-engineering/tools/generate_case_backlog_test.py \ - docs/ai-engineering/tools/prepare_case_execution_test.py \ - docs/ai-engineering/tools/orchestrate_case_execution_test.py + run: > + python -m unittest + docs/ai-engineering/tools/lint_agent_assets_test.py + docs/ai-engineering/tools/validate_acceptance_manifest_test.py + docs/ai-engineering/tools/generate_case_backlog_test.py + docs/ai-engineering/tools/prepare_case_execution_test.py + docs/ai-engineering/tools/orchestrate_case_execution_test.py diff --git a/docs/ai-engineering/case-maintenance-matrix.md b/docs/ai-engineering/case-maintenance-matrix.md index 1f1e0cdac..c149856f1 100644 --- a/docs/ai-engineering/case-maintenance-matrix.md +++ b/docs/ai-engineering/case-maintenance-matrix.md @@ -1,14 +1,14 @@ # API Examples Case Maintenance Matrix -This matrix is the planning artifact for cross-platform API Example parity work. It does not replace platform `AGENTS.md`, project `ARCHITECTURE.md`, or project `.agents/skills/*`; every row must be verified against the target project before implementation. +This matrix is the planning artifact for parity work across the four primary APIExample projects. It does not replace platform `AGENTS.md`, project `ARCHITECTURE.md`, or project `.agents/skills/*`; every row must be verified against the target project before implementation. ## How To Use 1. Start from the product request and identify the feature name, SDK family, and key SDK APIs. 2. Find the matching row in this matrix. 3. For each target platform unit, verify the path with the project `query-cases` skill when present. -4. Treat each required platform project as an independent delivery/verification unit inside one requirement-level acceptance manifest. -5. Update this matrix when a case is added, moved, renamed, or confirmed not applicable. +4. Treat each of the four primary projects as an independent delivery/verification unit inside one requirement-level acceptance manifest. +5. Update this matrix when a case is added, moved, renamed, or confirmed not applicable in a primary project. ## Status Values @@ -18,7 +18,7 @@ This matrix is the planning artifact for cross-platform API Example parity work. | `HIDDEN(path)` | Source is retained at the listed path, but the case is intentionally not registered in the user-visible list. | | `PARTIAL(note)` | Exists but parity, registration, docs, verification, runtime smoke, or API coverage needs review. | | `MISSING` | Required for parity but no implementation is currently recorded here. | -| `N/A(reason)` | Not applicable because the SDK family, platform, or project variant does not support it. | +| `N/A(reason)` | Not applicable because the SDK family or platform does not support it. | | `UNKNOWN` | Not inventoried yet; run the project case query before making decisions. | ## Active Parity Acceptance Rule @@ -35,99 +35,92 @@ If runtime/device behavior is required but unavailable, use `PARTIAL(...)` and n ## Platform Units +Only these primary projects participate in repository-level parity planning and execution: + | Unit | Project | Primary Source | | --- | --- | --- | | Android full | `Android/APIExample/` | `Android/APIExample/ARCHITECTURE.md` | -| Android audio | `Android/APIExample-Audio/` | `Android/APIExample-Audio/ARCHITECTURE.md` | -| Android Compose | `Android/APIExample-Compose/` | `Android/APIExample-Compose/ARCHITECTURE.md` | | iOS UIKit | `iOS/APIExample/` | `iOS/APIExample/ARCHITECTURE.md` | -| iOS SwiftUI | `iOS/APIExample-SwiftUI/` | `iOS/APIExample-SwiftUI/ARCHITECTURE.md` | -| iOS Objective-C | `iOS/APIExample-OC/` | `iOS/APIExample-OC/ARCHITECTURE.md` | -| iOS audio | `iOS/APIExample-Audio/` | `iOS/APIExample-Audio/ARCHITECTURE.md` | | macOS | `macOS/` | `macOS/ARCHITECTURE.md` | | Windows | `windows/` | `windows/ARCHITECTURE.md` | +Audio, Compose, SwiftUI, and Objective-C variants are outside this matrix and do not create backlog or acceptance units. + ## Pilot Matrix -Seeded from the project `ARCHITECTURE.md` indexes and source tree, then re-verified against `dev/4.7.0` (`ee77cc85`). Paths are relative to the projects listed in Platform Units. - -Inventory scope: rows were derived from the `Android/APIExample/` case index and its cross-platform counterparts. Cases that exist only outside Android full are **not inventoried yet** — including `ARKit`, `FaceCapture`, `MutliCamera`/`MultiCameraSourece`, `RawMediaData`, `CustomVideoSourceMediaIO`, `VideoChat`, and the Windows-only dialogs (`AudioProfile`, `AudioVolume`, `RegionConn`, `ReportInCall`, `RTMPinject`, `MultiVideoSource`). Their absence from this matrix is a documented inventory gap, not a coverage claim. - -| Feature | SDK Family | Key APIs | Android full | Android audio | Android Compose | iOS UIKit | iOS SwiftUI | iOS Objective-C | iOS audio | macOS | Windows | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| Join channel video | Full RTC | `joinChannel`, `setupLocalVideo`, `setupRemoteVideo` | `DONE(basic/JoinChannelVideo.java)` | `N/A(audio SDK)` | `DONE(samples/JoinChannelVideo.kt)` | `DONE(Basic/JoinChannelVideo/)` | `DONE(Basic/JoinChannelVideo/)` | `DONE(Basic/JoinChannelVideo/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo/)` | `PARTIAL(Basic/JoinChannelVideoByToken)` | Windows records token variant only; non-token parity gap. | -| Join channel video with token | Full RTC | `joinChannel` with token, local and remote video setup | `DONE(basic/JoinChannelVideoByToken.java)` | `N/A(audio SDK)` | `DONE(samples/JoinChannelVideoToken.kt)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideoByToken/)` | Verify token-generation behavior per platform before changing auth flow. | -| Join channel audio | Full RTC or audio SDK | `joinChannel`, `setAudioProfile`, audio routing or volume APIs | `DONE(basic/JoinChannelAudio.java)` | `DONE(basic/JoinChannelAudio.java)` | `DONE(samples/JoinChannelAudio.kt)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | -| Live streaming role switch | Full RTC | `setClientRole`, `joinChannel`, streaming role configuration | `DONE(advanced/LiveStreaming.java)` | `N/A(video/live)` | `DONE(samples/LiveStreaming.kt)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Advanced/LiveStreaming/)` | `N/A(video/live)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Basic/LiveBroadcasting/)` | Names differ by platform; keep product scenario wording stable. | -| RTMP or CDN streaming | Full RTC | `startRtmpStreamWithTranscoding`, `startRtmpStreamWithoutTranscoding`, `stopRtmpStream` | `DONE(advanced/RTMPStreaming.java)` | `N/A(video/live)` | `DONE(samples/RTMPStreaming.kt)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStream/)` | `DONE(Advanced/RTMPStreaming/)` | `N/A(video/live)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStream/)` | Windows also has `RTMPinject`; verify requested API before routing. | -| Media metadata | Full RTC | `registerMediaMetadataObserver` or platform metadata delegate APIs | `DONE(advanced/MediaMetadata.java)` | `N/A(audio SDK)` | `DONE(samples/MediaMetadata.kt)` | `DONE(Advanced/VideoMetadata/)` | `DONE(Advanced/VideoMetadata/)` | `DONE(Advanced/VideoMetadata/)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/Metadata/)` | macOS has no metadata case. iOS names this VideoMetadata. | -| Data stream messaging | Full RTC | `createDataStream`, `sendStreamMessage` | `DONE(advanced/SendDataStream.java)` | `MISSING` | `DONE(samples/SendDataStream.kt)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | Windows, Android audio, and iOS audio have no data stream case. | -| Custom audio source | Full RTC or audio SDK | `createCustomAudioTrack`, `setExternalAudioSource`, `pushExternalAudioFrame` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(samples/CustomAudioSource.kt)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomPCMAudioSource/)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioCapture/)` | Platform APIs differ; compare demonstrated behavior, not only method names. | -| Custom audio render | Full RTC or audio SDK | `setExternalAudioSink`, pull playback audio frame APIs | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(samples/CustomAudioRender.kt)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `MISSING` | Windows has no custom audio render case. | -| Custom video source | Full RTC | `setExternalVideoSource`, `pushExternalVideoFrame`, custom video track APIs | `DONE(advanced/PushExternalVideoYUV.java)` | `N/A(audio SDK)` | `DONE(samples/CustomVideoSource.kt)` | `DONE(Advanced/CustomVideoSourcePush/)` | `MISSING` | `DONE(Advanced/CustomVideoSourcePush/)` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoCapture/)` | SwiftUI has neither a source-push nor a CustomVideoRender case (re-verified on `dev/4.7.0`). Multi-source variants are separate rows. | -| Raw audio data | Full RTC or audio SDK | `registerAudioFrameObserver`, audio frame delegate APIs | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(samples/OriginAudioData.kt)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/OriginalAudio/)` | Check callback threading in every platform review. | -| Raw video data | Full RTC | `registerVideoFrameObserver`, video frame delegate APIs | `DONE(advanced/ProcessRawData.java)` | `N/A(audio SDK)` | `DONE(samples/OriginVideoData.kt)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `N/A(audio SDK)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/OriginalVideo/)` | Raw media combined samples are tracked separately. | -| Screen sharing | Full RTC | `startScreenCapture`, `updateScreenCaptureParameters`, `stopScreenCapture` | `DONE(advanced/ScreenSharing.java)` | `N/A(audio SDK)` | `DONE(samples/ScreenSharing.kt)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `N/A(audio SDK)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | iOS requires ReplayKit extension; Android requires foreground service. | -| Media player | Full RTC | `createMediaPlayer`, `open`, `play`, publish/update channel options | `DONE(advanced/MediaPlayer.java)` | `MISSING` | `DONE(samples/MediaPlayer.kt)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | `MISSING` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | Android audio and iOS audio have no standalone media player case. | -| Spatial audio | Full RTC or audio SDK | spatial audio engine, remote position, media player spatialization APIs | `DONE(advanced/SpatialSound.java)` | `DONE(advanced/SpatialSound.java)` | `DONE(samples/SpatialSound.kt)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | iOS and Android architecture docs record different key APIs; verify scenario first. | -| Stream encryption | Full RTC | `enableEncryption`, encryption configuration APIs | `DONE(advanced/ChannelEncryption.java)` | `MISSING` | `DONE(samples/ChannelEncryption.kt)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/StreamEncryption/)` | `MISSING` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/CustomEncrypt/ + MediaEncrypt/)` | Windows has multiple encryption rows; route by requested API. Audio projects lack encryption cases. | -| Pre-call test | Full RTC or audio SDK | `startEchoTest`, `stopEchoTest`, last-mile probe or network test APIs | `DONE(advanced/PreCallTest.java)` | `DONE(advanced/PreCallTest.java)` | `DONE(samples/PreCallTest.kt)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PrecallTest/)` | `MISSING` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PreCallTest/)` | iOS Objective-C has no pre-call test case. | -| Third-party beauty | Full RTC | extension enablement, third-party SDK initialization, video frame processing | `DONE(advanced/ThirdPartyBeauty.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/ThirdBeautify/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `MISSING` | `MISSING` | Only Android full and iOS UIKit have third-party beauty. Release work must check license expiration. | -| Extension sample | Full RTC extension | `enableExtension`, extension property APIs, matching native headers | `DONE(advanced/SimpleExtension.java)` | `N/A(audio SDK)` | `PARTIAL(VideoProcessExtension covers enableExtension)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | `DONE(Advanced/SimpleFilter/)` | `N/A(audio SDK)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | During SDK bumps, verify Android extension include files and iOS/macOS native assets. | -| Audio mixing | Full RTC or audio SDK | `startAudioMixing`, `stopAudioMixing`, `adjustAudioMixingVolume`, effect APIs | `DONE(advanced/PlayAudioFiles.java)` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(samples/PlayAudioFiles.kt)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage across all projects. | -| Rhythm player | Full RTC or audio SDK | `startRhythmPlayer`, `stopRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(samples/RhythmPlayer.kt)` | `HIDDEN(Advanced/RhythmPlayer/)` | `HIDDEN(Advanced/RhythmPlayer/)` | `HIDDEN(Advanced/RhythmPlayer/)` | `HIDDEN(Advanced/RhythmPlayer/)` | `N/A(no case)` | `N/A(no case)` | All implemented Rhythm Player sources are retained for reference but hidden because the APIs are deprecated since RTC SDK 4.6.0. | -| Media recorder | Full RTC | `createMediaRecorder`, `startRecording`, `stopRecording` | `DONE(advanced/MediaRecorder.java)` | `N/A(audio SDK)` | `DONE(samples/MediaRecorder.kt)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `N/A(audio SDK)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Advanced/MediaRecorder/)` | iOS/macOS embed recorder in a join-channel variant; Android/Compose/Windows have standalone cases. | -| Local video transcoding | Full RTC | `startLocalVideoTranscoder`, `startCameraCapture`, compositing APIs | `DONE(advanced/LocalVideoTranscoding.java)` | `N/A(audio SDK)` | `DONE(samples/LocalVideoTranscoding.kt)` | `DONE(Advanced/LocalCompositeGraph/)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalCompositeGraph/)` | `N/A(audio SDK)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalVideoTranscoding/)` | iOS UIKit and OC name this LocalCompositeGraph; SwiftUI splits transcoding and composition. | -| Multi-channel join | Full RTC | `joinChannelEx`, `leaveChannelEx`, multi-channel connection APIs | `DONE(advanced/JoinMultipleChannel.java)` | `N/A(audio SDK)` | `DONE(samples/JoinMultiChannel.kt)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `N/A(audio SDK)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/MultiChannel/)` | Full coverage across full-RTC projects. | -| Multipath | Full RTC | `enableMultipath`, `setMultipathConfig`, or `updateChannel` with multipath options | `DONE(advanced/Multipath.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | `N/A(audio SDK)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | Android Compose has no multipath case. | -| Content inspect | Full RTC | `enableContentInspect` | `DONE(advanced/ContentInspect.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `N/A(audio SDK)` | `DONE(Advanced/ContentInspect/)` | `MISSING` | Android Compose and Windows have no content inspect case. | -| Channel media relay | Full RTC | `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay`, pause/resume APIs | `DONE(advanced/HostAcrossChannel.java)` | `N/A(audio SDK)` | `DONE(samples/HostAcrossChannel.kt)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/MediaChannelRelay/)` | `N/A(audio SDK)` | `DONE(Advanced/ChannelMediaRelay/)` | `DONE(Advanced/CrossChannel/)` | Full coverage across full-RTC projects. | -| Picture in picture | Full RTC | `enterPictureInPictureMode` or `AVPictureInPictureController` | `DONE(advanced/PictureInPicture.java)` | `N/A(audio SDK)` | `DONE(samples/PictureInPicture.kt)` | `DONE(Advanced/PictureInPicture/)` | `DONE(Advanced/PictureInPicture/)` | `DONE(Advanced/PictureInPicture/)` | `N/A(audio SDK)` | `MISSING` | `MISSING` | macOS and Windows have no PiP case. | -| Voice beautifier and effects | Full RTC or audio SDK | `setVoiceBeautifierPreset`, `setAudioEffectPreset`, `setVoiceConversionPreset` | `DONE(advanced/VoiceEffects.java)` | `DONE(advanced/VoiceEffects.java)` | `DONE(samples/VoiceEffects.kt)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/BeautyAudio/)` | Full coverage, verified by `setVoiceBeautifierPreset`. Windows bundles it with video beauty in `BeautyAudio`. | -| Audio waveform | Full RTC or audio SDK | `enableAudioVolumeIndication`, audio frame observer waveform rendering | `DONE(audio/AudioWaveform.java)` | `DONE(audio/AudioWaveform.java)` | `MISSING` | `DONE(Advanced/AudioWaveform/)` | `DONE(Advanced/AudioWaveform/)` | `MISSING` | `MISSING` | `MISSING` | `MISSING` | Windows `AudioVolume` adjusts signal volume and is not a waveform case. | -| Third-party player audio routing | Full RTC or audio SDK | third-party player playback combined with the SDK audio route | `DONE(audio/AudioRouterPlayer.java)` | `MISSING` | `MISSING` | `DONE(Advanced/AudioRouterPlayer/)` | `MISSING` | `MISSING` | `MISSING` | `MISSING` | `MISSING` | Android ships Exo/Ijk/Native variants. Confirm the third-party player dependency is available before porting. | -| Agora beauty (video effect object) | Full RTC | `createVideoEffectObject`, `setVideoEffectStringParam`, `setVideoEffectFloatParam` | `DONE(advanced/AgoraBeauty.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/AgoraBeauty/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `DONE(Advanced/AgoraBeauty/)` | `DONE(Advanced/Beauty2.0/)` | Distinct from the built-in `setBeautyEffectOptions` video-enhancement case. Verified by `createVideoEffectObject`. | -| Simulcast | Full RTC | `setSimulcastConfig` | `DONE(advanced/Simulcast.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | `N/A(audio SDK)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | Android Compose is the only full-RTC gap. Verified by `setSimulcastConfig`. | -| Multiple custom video tracks | Full RTC | `createCustomVideoTrack`, `joinChannelEx`, `pushExternalVideoFrameById` | `DONE(advanced/MultiVideoSourceTracks.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `DONE(Advanced/MultiVideoSourceTracks/)` | Case names differ per platform; correspondence verified by `createCustomVideoTrack`. | -| Custom remote video render | Full RTC | remote render path via `registerVideoFrameObserver` or `setVideoFrameDelegate` | `DONE(advanced/CustomRemoteVideoRender.java)` | `N/A(audio SDK)` | `DONE(samples/CustomVideoRender.kt)` | `DONE(Advanced/CustomVideoRender/)` | `MISSING` | `DONE(Advanced/CustomVideoRender/)` | `N/A(audio SDK)` | `DONE(Advanced/CustomVideoRender/)` | `MISSING` | SwiftUI has no CustomVideoRender case. Windows `OriginalVideo` is raw video data, tracked in its own row. | -| Transparent background rendering | Full RTC | alpha mask and alpha stitch mode with encoded alpha | `DONE(advanced/TransparentRendering.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/TransparentRender/)` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/TransparentBg/)` | macOS uses alpha options only inside `VideoProcess`/`AgoraBeauty`, not in a transparent-rendering case. | -| Quick channel switch | Full RTC | rapid `joinChannel` / `leaveChannel` switching across channels | `DONE(advanced/VideoQuickSwitch.java)` | `N/A(audio SDK)` | `MISSING` | `DONE(Advanced/QuickSwitchChannel/)` | `DONE(Advanced/QuickSwitchChannel/)` | `MISSING` | `N/A(audio SDK)` | `DONE(Advanced/QuickSwitchChannel/)` | `MISSING` | Correspondence is by scenario and case name; this feature has no single distinctive API to verify against. | -| URL live stream player | Full RTC | RTE player `openWithUrl`, `startWithUrl` | `DONE(advanced/UrlLiveStream.java)` | `N/A(audio SDK)` | `MISSING` | `UNKNOWN` | `MISSING` | `MISSING` | `N/A(audio SDK)` | `UNKNOWN` | `UNKNOWN` | iOS UIKit, macOS, and Windows ship an `RtePlayer` case that was not confirmed to demonstrate the same scenario. Run `query-cases` before treating these as gaps. | -| KTV copyright music | None (docs link) | none — the case only opens the Agora documentation page | `DONE(advanced/KtvCopyrightMusic.java)` | `N/A(docs-link case)` | `N/A(docs-link case)` | `DONE(Advanced/KtvCopyrightMusic/)` | `DONE(Advanced/KtvCopyrightMusic/)` | `N/A(docs-link case)` | `N/A(docs-link case)` | `N/A(docs-link case)` | `N/A(docs-link case)` | All three implementations only open a docs URL (Android `WebView`, iOS/SwiftUI hand off to the browser). No SDK surface to port, so absences are not gaps. | +Seeded from the primary project `ARCHITECTURE.md` indexes and source trees, then re-verified against `dev/4.7.0` (`ee77cc85`). Paths are relative to the projects listed in Platform Units. + +Inventory scope: the initial rows were derived from the `Android/APIExample/` case index and its counterparts in the other three primary projects. Features that exist only outside the Android primary project are not inventoried yet, including `ARKit`, `FaceCapture`, `MutliCamera`/`MultiCameraSourece`, `RawMediaData`, `CustomVideoSourceMediaIO`, `VideoChat`, and Windows-only dialogs such as `AudioProfile`, `AudioVolume`, `RegionConn`, `ReportInCall`, `RTMPinject`, and `MultiVideoSource`. Their absence from this matrix is an inventory gap, not a coverage claim. + +| Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Join channel video | Full RTC | `joinChannel`, `setupLocalVideo`, `setupRemoteVideo` | `DONE(basic/JoinChannelVideo.java)` | `DONE(Basic/JoinChannelVideo/)` | `DONE(Basic/JoinChannelVideo/)` | `PARTIAL(Basic/JoinChannelVideoByToken)` | Windows records the token variant only; non-token parity remains unverified. | +| Join channel video with token | Full RTC | `joinChannel` with token, local and remote video setup | `DONE(basic/JoinChannelVideoByToken.java)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideoByToken/)` | Verify token-generation behavior per platform before changing auth flow. | +| Join channel audio | Full RTC | `joinChannel`, `setAudioProfile`, audio routing or volume APIs | `DONE(basic/JoinChannelAudio.java)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | +| Live streaming role switch | Full RTC | `setClientRole`, `joinChannel`, streaming role configuration | `DONE(advanced/LiveStreaming.java)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Basic/LiveBroadcasting/)` | Names differ by platform; keep product scenario wording stable. | +| RTMP or CDN streaming | Full RTC | `startRtmpStreamWithTranscoding`, `startRtmpStreamWithoutTranscoding`, `stopRtmpStream` | `DONE(advanced/RTMPStreaming.java)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStream/)` | Windows also has `RTMPinject`; verify the requested API before routing. | +| Media metadata | Full RTC | `registerMediaMetadataObserver` or platform metadata delegate APIs | `DONE(advanced/MediaMetadata.java)` | `DONE(Advanced/VideoMetadata/)` | `MISSING` | `DONE(Advanced/Metadata/)` | macOS has no metadata case. iOS names this VideoMetadata. | +| Data stream messaging | Full RTC | `createDataStream`, `sendStreamMessage` | `DONE(advanced/SendDataStream.java)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | Windows has no data stream case. | +| Custom audio source | Full RTC | `createCustomAudioTrack`, `setExternalAudioSource`, `pushExternalAudioFrame` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioCapture/)` | Platform APIs differ; compare demonstrated behavior, not only method names. | +| Custom audio render | Full RTC | `setExternalAudioSink`, pull playback audio frame APIs | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `MISSING` | Windows has no custom audio render case. | +| Custom video source | Full RTC | `setExternalVideoSource`, `pushExternalVideoFrame`, custom video track APIs | `DONE(advanced/PushExternalVideoYUV.java)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoCapture/)` | Multi-source variants are tracked separately. | +| Raw audio data | Full RTC | `registerAudioFrameObserver`, audio frame delegate APIs | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/OriginalAudio/)` | Check callback threading in every platform review. | +| Raw video data | Full RTC | `registerVideoFrameObserver`, video frame delegate APIs | `DONE(advanced/ProcessRawData.java)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/OriginalVideo/)` | Raw media combined samples are tracked separately. | +| Screen sharing | Full RTC | `startScreenCapture`, `updateScreenCaptureParameters`, `stopScreenCapture` | `DONE(advanced/ScreenSharing.java)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | iOS requires a ReplayKit extension; Android requires a foreground service. | +| Media player | Full RTC | `createMediaPlayer`, `open`, `play`, publish/update channel options | `DONE(advanced/MediaPlayer.java)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | Standalone media player coverage exists in all four primary projects. | +| Spatial audio | Full RTC | spatial audio engine, remote position, media player spatialization APIs | `DONE(advanced/SpatialSound.java)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | iOS and Android architecture docs record different key APIs; verify the scenario first. | +| Stream encryption | Full RTC | `enableEncryption`, encryption configuration APIs | `DONE(advanced/ChannelEncryption.java)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/CustomEncrypt/ + MediaEncrypt/)` | Windows has multiple encryption cases; route by requested API. | +| Pre-call test | Full RTC | `startEchoTest`, `stopEchoTest`, last-mile probe or network test APIs | `DONE(advanced/PreCallTest.java)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PreCallTest/)` | Primary-project coverage exists on all four platforms. | +| Third-party beauty | Full RTC | extension enablement, third-party SDK initialization, video frame processing | `DONE(advanced/ThirdPartyBeauty.java)` | `DONE(Advanced/ThirdBeautify/)` | `MISSING` | `MISSING` | Release work must check third-party license expiration. | +| Extension sample | Full RTC extension | `enableExtension`, extension property APIs, matching native headers | `DONE(advanced/SimpleExtension.java)` | `DONE(Advanced/SimpleFilter/)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | During SDK bumps, verify Android extension include files and Apple native assets. | +| Audio mixing | Full RTC | `startAudioMixing`, `stopAudioMixing`, `adjustAudioMixingVolume`, effect APIs | `DONE(advanced/PlayAudioFiles.java)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage across the four primary projects. | +| Rhythm player | Full RTC | `startRhythmPlayer`, `stopRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(Advanced/RhythmPlayer/)` | `N/A(no case)` | `N/A(no case)` | Android and iOS sources are retained but hidden because the APIs are deprecated since RTC SDK 4.6.0. | +| Media recorder | Full RTC | `createMediaRecorder`, `startRecording`, `stopRecording` | `DONE(advanced/MediaRecorder.java)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Advanced/MediaRecorder/)` | iOS and macOS embed recorder behavior in a join-channel variant; Android and Windows use standalone cases. | +| Local video transcoding | Full RTC | `startLocalVideoTranscoder`, `startCameraCapture`, compositing APIs | `DONE(advanced/LocalVideoTranscoding.java)` | `DONE(Advanced/LocalCompositeGraph/)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalVideoTranscoding/)` | iOS names the equivalent case LocalCompositeGraph. | +| Multi-channel join | Full RTC | `joinChannelEx`, `leaveChannelEx`, multi-channel connection APIs | `DONE(advanced/JoinMultipleChannel.java)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/MultiChannel/)` | Full coverage across the four primary projects. | +| Multipath | Full RTC | `enableMultipath`, `setMultipathConfig`, or `updateChannel` with multipath options | `DONE(advanced/Multipath.java)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | Full coverage across the four primary projects. | +| Content inspect | Full RTC | `enableContentInspect` | `DONE(advanced/ContentInspect.java)` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `MISSING` | Windows has no content inspect case. | +| Channel media relay | Full RTC | `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay`, pause/resume APIs | `DONE(advanced/HostAcrossChannel.java)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/ChannelMediaRelay/)` | `DONE(Advanced/CrossChannel/)` | Full coverage across the four primary projects. | +| Picture in picture | Full RTC | `enterPictureInPictureMode` or `AVPictureInPictureController` | `DONE(advanced/PictureInPicture.java)` | `DONE(Advanced/PictureInPicture/)` | `MISSING` | `MISSING` | macOS and Windows have no picture-in-picture case. | +| Voice beautifier and effects | Full RTC | `setVoiceBeautifierPreset`, `setAudioEffectPreset`, `setVoiceConversionPreset` | `DONE(advanced/VoiceEffects.java)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/BeautyAudio/)` | Windows bundles voice effects with video beauty in `BeautyAudio`. | +| Audio waveform | Full RTC | `enableAudioVolumeIndication`, audio frame observer waveform rendering | `DONE(audio/AudioWaveform.java)` | `DONE(Advanced/AudioWaveform/)` | `MISSING` | `MISSING` | Windows `AudioVolume` adjusts signal volume and is not a waveform case. | +| Third-party player audio routing | Full RTC | third-party player playback combined with the SDK audio route | `DONE(audio/AudioRouterPlayer.java)` | `DONE(Advanced/AudioRouterPlayer/)` | `MISSING` | `MISSING` | Confirm the third-party player dependency is available before porting. | +| Agora beauty (video effect object) | Full RTC | `createVideoEffectObject`, `setVideoEffectStringParam`, `setVideoEffectFloatParam` | `DONE(advanced/AgoraBeauty.java)` | `DONE(Advanced/AgoraBeauty/)` | `DONE(Advanced/AgoraBeauty/)` | `DONE(Advanced/Beauty2.0/)` | Distinct from the built-in `setBeautyEffectOptions` video-enhancement case. | +| Simulcast | Full RTC | `setSimulcastConfig` | `DONE(advanced/Simulcast.java)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | Verified by `setSimulcastConfig`. | +| Multiple custom video tracks | Full RTC | `createCustomVideoTrack`, `joinChannelEx`, `pushExternalVideoFrameById` | `DONE(advanced/MultiVideoSourceTracks.java)` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `DONE(Advanced/MultiVideoSourceTracks/)` | Case names differ per platform; correspondence is verified by `createCustomVideoTrack`. | +| Custom remote video render | Full RTC | remote render path via `registerVideoFrameObserver` or `setVideoFrameDelegate` | `DONE(advanced/CustomRemoteVideoRender.java)` | `DONE(Advanced/CustomVideoRender/)` | `DONE(Advanced/CustomVideoRender/)` | `MISSING` | Windows `OriginalVideo` is raw video data and is tracked in its own row. | +| Transparent background rendering | Full RTC | alpha mask and alpha stitch mode with encoded alpha | `DONE(advanced/TransparentRendering.java)` | `DONE(Advanced/TransparentRender/)` | `MISSING` | `DONE(Advanced/TransparentBg/)` | macOS uses alpha options only inside `VideoProcess`/`AgoraBeauty`, not in a dedicated case. | +| Quick channel switch | Full RTC | rapid `joinChannel` / `leaveChannel` switching across channels | `DONE(advanced/VideoQuickSwitch.java)` | `DONE(Advanced/QuickSwitchChannel/)` | `DONE(Advanced/QuickSwitchChannel/)` | `MISSING` | Correspondence is by scenario and case name; this feature has no single distinctive API. | +| URL live stream player | Full RTC | RTE player `openWithUrl`, `startWithUrl` | `DONE(advanced/UrlLiveStream.java)` | `UNKNOWN` | `UNKNOWN` | `UNKNOWN` | Apple and Windows ship an `RtePlayer` case that was not confirmed to demonstrate the same scenario. Run `query-cases` before treating these as gaps. | +| KTV copyright music | None (docs link) | none - the case only opens the Agora documentation page | `DONE(advanced/KtvCopyrightMusic.java)` | `DONE(Advanced/KtvCopyrightMusic/)` | `N/A(docs-link case)` | `N/A(docs-link case)` | The Android and iOS implementations only open a docs URL; absence elsewhere is not a parity gap. | ## Confirmed Gaps -These gaps are derived from the pilot matrix above and should be prioritized when parity work is requested. +These gaps are derived from the primary-project matrix above and should be prioritized when parity work is requested. | Gap | Affected Units | Severity | | --- | --- | --- | -| Basic audio-only join channel | Windows | High — missing foundational case | -| Media metadata | macOS | Medium — full-RTC platform gap | -| Data stream messaging | Android audio, iOS audio, Windows | Medium — audio and Windows gap | -| Custom audio render | Windows | Medium — full-RTC platform gap | -| Media player (standalone) | Android audio, iOS audio | Low — audio projects may not need standalone player | -| Stream encryption | Android audio, iOS audio | Low — audio projects may not need encryption | -| Pre-call test | iOS Objective-C | Medium — full-RTC variant gap | -| Third-party beauty | Android Compose, iOS SwiftUI, iOS OC, macOS, Windows | Low — third-party SDK and license overhead | -| Extension sample | iOS SwiftUI, Windows | Medium — extension enablement gap | -| Multipath | Android Compose | Low — Compose variant gap | -| Content inspect | Android Compose, Windows | Low — moderation feature gap | -| Picture in picture | macOS, Windows | Low — platform feature gap | -| Custom video source (push) | iOS SwiftUI | Medium — SwiftUI variant gap | -| Audio waveform | Android Compose, iOS Objective-C, iOS audio, macOS, Windows | Medium — audio-visualisation gap on five units | -| Simulcast | Android Compose | Medium — only full-RTC gap for a shipped 4.x feature | -| Custom remote video render | iOS SwiftUI, Windows | Medium — full-RTC variant gap | -| Multiple custom video tracks | Android Compose, iOS SwiftUI, iOS Objective-C | Medium — multi-track gap | -| Transparent background rendering | Android Compose, iOS SwiftUI, iOS Objective-C, macOS | Low — niche rendering feature | -| Quick channel switch | Android Compose, iOS Objective-C, Windows | Low — switching-performance demo gap | -| Agora beauty (video effect object) | Android Compose, iOS SwiftUI, iOS Objective-C | Low — beauty v2 gap | -| Third-party player audio routing | Android audio, Android Compose, iOS SwiftUI, iOS Objective-C, iOS audio, macOS, Windows | Low — depends on third-party player dependencies | +| Join channel video | Windows | High - foundational non-token flow remains partial | +| Basic audio-only join channel | Windows | High - missing foundational case | +| Media metadata | macOS | Medium - full-RTC platform gap | +| Data stream messaging | Windows | Medium - full-RTC platform gap | +| Custom audio render | Windows | Medium - full-RTC platform gap | +| Third-party beauty | macOS, Windows | Low - third-party SDK and license overhead | +| Extension sample | Windows | Medium - extension enablement gap | +| Content inspect | Windows | Low - moderation feature gap | +| Picture in picture | macOS, Windows | Low - platform feature gap | +| Audio waveform | macOS, Windows | Medium - audio visualization gap | +| Third-party player audio routing | macOS, Windows | Low - depends on third-party player dependencies | +| Custom remote video render | Windows | Medium - full-RTC platform gap | +| Transparent background rendering | macOS | Low - niche rendering feature | +| Quick channel switch | Windows | Low - switching-performance demo gap | ## Maintenance Rules -- Do not change a `UNKNOWN` cell to `MISSING` until the target project `query-cases` skill or architecture file has been checked. +- Keep the matrix header fixed to `Android full`, `iOS UIKit`, `macOS`, and `Windows`; project variants are outside repository-level parity scope. +- Do not change an `UNKNOWN` cell to `MISSING` until the primary target project's `query-cases` skill or architecture file has been checked. - Do not change a `PARTIAL` cell to `DONE` unless registration, discoverability, reference parity, review, and build evidence are confirmed for the active work. - Prefer feature names based on user-visible scenario rather than platform-specific class names. - Keep API names in the matrix broad enough to route work, then validate exact signatures in the target SDK version. - Keep release-risk notes in this matrix short; detailed release checks belong in `docs/ai-engineering/release-known-issues.md`. -- When a new case is added to any project, update the matching row in the same edit or follow-up. +- When a new case is added to a primary project, update the matching row in the same edit or follow-up. diff --git a/docs/ai-engineering/knowledge-index.md b/docs/ai-engineering/knowledge-index.md index 825440fc3..3b9b3a09a 100644 --- a/docs/ai-engineering/knowledge-index.md +++ b/docs/ai-engineering/knowledge-index.md @@ -32,43 +32,35 @@ The repository has four independent platform families: Never share source files, build scripts, SDK dependencies, or generated project metadata across platform roots unless a platform rule explicitly says to do so. -## Project Selection +## Primary Project Selection -| Request Type | Default Target | +Repository-level AI engineering always targets these four primary APIExample projects: + +| Platform | Target | | --- | --- | -| Android full RTC, video, screen sharing, beauty, extensions | `Android/APIExample/` | -| Android voice-only or audio-only SDK behavior | `Android/APIExample-Audio/` | -| Android Compose case or Compose parity work | `Android/APIExample-Compose/` | -| iOS UIKit Swift full RTC | `iOS/APIExample/` | -| iOS SwiftUI parity work | `iOS/APIExample-SwiftUI/` | -| iOS Objective-C parity work | `iOS/APIExample-OC/` | -| iOS audio-only SDK behavior | `iOS/APIExample-Audio/` | -| macOS Swift Cocoa sample | `macOS/` | -| Windows C++ MFC sample | `windows/` | - -If the product request does not name a platform, default to Android, iOS, macOS, and Windows. Narrow the scope only when the user or Contract records an explicit waiver reason. Treat each required platform as a separate implementation and verification unit. +| Android | `Android/APIExample/` | +| iOS | `iOS/APIExample/` | +| macOS | `macOS/` | +| Windows | `windows/` | + +Audio, Compose, SwiftUI, and Objective-C variants remain independent repository projects, but they are outside the repository-level release-iteration matrix, backlog, Contract, and acceptance scope. If the product request does not name a platform, require all four primary projects. Narrow the scope only when the user or Contract records an explicit waiver reason. Treat each required primary project as a separate implementation and verification unit. ## Existing Local Skills | Scope | Skills | | --- | --- | | `Android/APIExample/` | `query-cases`, `upsert-case`, `review-case` | -| `Android/APIExample-Audio/` | `query-cases`, `upsert-case`, `review-case` | -| `Android/APIExample-Compose/` | `query-cases`, `upsert-case`, `review-case` | | `iOS/APIExample/` | `query-cases`, `upsert-case`, `review-case` | -| `iOS/APIExample-Audio/` | `query-cases`, `upsert-case`, `review-case` | -| `iOS/APIExample-OC/` | `query-cases`, `upsert-case`, `review-case` | -| `iOS/APIExample-SwiftUI/` | `query-cases`, `upsert-case`, `review-case` | -| `macOS/` | `upsert-case`, `review-case` | -| `windows/` | `upsert-case`, `review-case` | +| `macOS/` | `query-cases`, `upsert-case`, `review-case` | +| `windows/` | `query-cases`, `upsert-case`, `review-case` | The repository-level orchestration skill is `.agents/skills/api-example-release-iteration/SKILL.md`. ## Repository Profile -`docs/ai-engineering/repository-profile.json` contains the SDK version sources that differ between API Examples distributions. Shared Python tools parse source kinds such as Gradle properties, CocoaPods packages, and SDK archive names without embedding distribution package names in code. +`docs/ai-engineering/repository-profile.json` contains the SDK version sources that differ between API Examples distributions. Shared Python tools parse source kinds such as Gradle properties, CocoaPods packages, and SDK archive names without embedding distribution package names in code. An `external-injection` entry records that a primary project has no trustworthy tracked version source and deliberately keeps that platform's SDK check blocked. -The orchestrator stores the profile path and SHA-256 in the execution package and input snapshots. Dispatch and assembly stop if the checked-in profile changes after initialization. Coverage state and historical knowledge do not belong in this profile; keep them in the matrix and durable knowledge documents. +The orchestrator stores the profile path and SHA-256 in the execution package, input snapshots, and acceptance manifest. Dispatch and assembly stop if the checked-in profile changes after initialization, and the standalone manifest validator recomputes release checks from the bound live profile. Coverage state and historical knowledge do not belong in this profile; keep them in the matrix and durable knowledge documents. ## Codex Role Routing @@ -100,7 +92,7 @@ For case backfill work, generate candidate platform execution units from the mat python3 docs/ai-engineering/tools/generate_case_backlog.py ``` -Use the generated priority to select the next product requirement, then deliver that requirement across all four official platform families. Contract selects the applicable project variant inside each platform. +Use the generated priority to select the next product requirement, then deliver that requirement across all four primary APIExample projects. Contract must preserve the fixed target mapping above. ## Case Implementation Knowledge @@ -208,8 +200,10 @@ Use `docs/ai-engineering/templates/release-dry-run-template.md` as the starting | iOS/macOS certificates expire on build machines | Inspect or print certificate expiration during release preparation. | | SDK dependency version is not bumped on release branch | Verify platform SDK version files against the release target. | | Model-declared build result is not backed by command telemetry | Bind each executed verification command to the hashed Codex JSONL log and exit code, and accept build status only for recognized platform build tools. | +| Codex command telemetry omits `cwd` | Record `cwd_source=dispatch`; the dispatcher pins both its process cwd and `codex exec -C` to the main project, while build-command validation rejects directory changes and sibling-project paths. If a future command event supplies `cwd`, require it to match the dispatch target. | | Platform run misses nested instructions | Start `codex exec` in the Contract-selected target so root/platform/project `AGENTS.md` files load automatically. | | External packaging metadata leaks into repository acceptance | Keep Jenkins jobs, artifact URLs, QA evidence, and website publication outside the acceptance manifest. | +| An externally injected SDK is assigned a version through unrelated pod metadata | Mark the source as `external-injection`; keep the repository SDK gate blocked until tracked project evidence exists. | | Windows packaging fails from path length or permissions | Run Windows script preflight after path or packaging changes. | ## Output Contract diff --git a/docs/ai-engineering/release-iteration-gate.md b/docs/ai-engineering/release-iteration-gate.md index 6b0101ab1..ea75e6610 100644 --- a/docs/ai-engineering/release-iteration-gate.md +++ b/docs/ai-engineering/release-iteration-gate.md @@ -15,7 +15,7 @@ Contract runs once. In a shared checkout, Android/iOS/macOS/Windows Implementati ## Intake Gate -Before Contract starts, identify feature behavior, SDK family, key APIs, target SDK version for each platform, reference case, expected user flow, repository scope, and available target verification hosts. Platform targets may differ when SDK release lines are staggered. Confirm that the checked-in repository profile describes the current SDK package names and version sources. All four top-level platforms are required by default. Contract may choose the appropriate project variant per platform. +Before Contract starts, identify feature behavior, SDK family, key APIs, target SDK version for each platform, reference case, expected user flow, repository scope, and available target verification hosts. SDK targets may differ when release lines are staggered. Confirm that the checked-in repository profile describes the current SDK package names and version sources. All four primary projects are required by default: `Android/APIExample/`, `iOS/APIExample/`, `macOS/`, and `windows/`. Contract may waive a platform with an explicit reason, but may not substitute an audio, Compose, SwiftUI, or Objective-C variant for a primary project. ## Contract Gate @@ -25,6 +25,7 @@ Contract passes only when: - A reference source and result are recorded when parity is required. - Cross-platform invariant behavior is explicit. - Android, iOS, macOS, and Windows each have `required`, `target_project`, constraints, and allowed files. +- Every required `target_project` matches the fixed primary-project mapping from the Intake Gate. - Lifecycle, callback threading, registration, SDK family, and credential boundaries are explicit. Nearby cases guide framework patterns; product behavior comes from the shared Contract/reference. @@ -49,7 +50,7 @@ Each Verification independently checks: - Contract/reference parity, SDK types/defaults, lifecycle, threading, errors, and cleanup. - Registration, display name, inputs, feedback, layout, and audio/video affordances. - The strongest valid build or static command on the current verification host. -- Each executed PASS/FAIL command is bound to the hashed Codex JSONL command event and exit code; build results use recognized platform build actions from the Contract working directory. +- Each executed PASS/FAIL command is bound to the hashed Codex JSONL command event and exit code; build results use recognized platform build actions from the Contract working directory. When the Codex event includes `cwd`, it must match; current CLI versions that omit it record `cwd_source=dispatch`, backed by both the process cwd and `codex exec -C` target. - Dispatch provenance records the host platform. Windows build `PASS` requires a Windows host. - The Verification run does not modify tracked or untracked repository content. - Concrete reasons for every skipped check. @@ -72,10 +73,12 @@ Any required platform blocker or cross-platform result other than `PASS` forces ## Release Checklist -Release is mandatory manifest data, not an agent. `requirement.target_sdk_versions` and `release.target_sdk_versions` must contain the same Android, iOS, macOS, and Windows mapping. The orchestrator compares each platform only with its own target while refreshing dependency versions from live repository files during assembly. Non-`BLOCKED` acceptance requires all SDK-version checks to pass and permits no skipped repository release checks. A platform remaining on an older SDK is valid and does not require changing the script or repository profile. +Release is mandatory manifest data, not an agent. `requirement.target_sdk_versions` and `release.target_sdk_versions` must contain the same Android, iOS, macOS, and Windows mapping. The manifest binds `release.repository_profile` by repository-relative path and SHA-256; assembly and the standalone validator recompute SDK checks from that live profile. The orchestrator compares each platform only with its own target while refreshing dependency versions from live repository files during assembly. Non-`BLOCKED` acceptance requires all SDK-version checks to pass and permits no skipped repository release checks. A platform remaining on an older SDK is valid and does not require changing the script or repository profile. Jenkins packaging, QA validation, artifact URLs, and external website publication are downstream processes outside this repository workflow. A final `PASS` means the API Example source and repository checks are ready for that external handoff; the manifest does not represent packaging or QA completion. +When a primary project receives an SDK outside tracked repository files, declare that boundary as `external-injection` in the repository profile. Its SDK-version check remains `BLOCKED` until a trustworthy project-owned version source is available. A successful project build proves compatibility with the injected binaries, not their exact SDK version; do not change packaging scripts, pod metadata, or comments solely to force the repository gate to pass. + ## Manifest V4 The single state tree is: diff --git a/docs/ai-engineering/release-known-issues.md b/docs/ai-engineering/release-known-issues.md index 69f662935..ce312a546 100644 --- a/docs/ai-engineering/release-known-issues.md +++ b/docs/ai-engineering/release-known-issues.md @@ -6,8 +6,8 @@ This file records repository-specific risks that can affect API Example source a | Area | Risk | Repository Gate | | --- | --- | --- | -| SDK versions | Android, iOS, macOS, and Windows dependency declarations can drift across release branches. | Validate every source declared in `repository-profile.json` against the target SDK version. | -| iOS full demo SDK source | `iOS/APIExample/Podfile` keeps `pod 'AgoraRtcEngine_iOS'` commented out and builds from the local `sdk.podspec`, which vendors `libs/*.xcframework` and declares an unrelated `spec.version`. No RTC semver is readable from the repository, so `sdk-version-ios` reports `BLOCKED` with `no active SDK version declaration`. A commented-out pod line is never accepted as evidence. | Before release, restore the pinned `pod 'AgoraRtcEngine_iOS', ''` line, or record an explicit waiver stating the SDK was supplied out-of-band and state how its version was confirmed. | +| SDK versions | The Android, iOS, macOS, and Windows main APIExample version declarations or evidence can drift across release branches. | Validate every main-project source declared in `repository-profile.json` against the platform target SDK version; keep unverifiable external injection blocked. | +| iOS full demo SDK source | `iOS/APIExample/Podfile` uses a local `sdk.podspec`, while the ignored `libs/*.xcframework` binaries are injected outside the repository. The podspec package version and commented remote pod example do not prove the injected RTC SDK version. | Keep the repository SDK check `BLOCKED` until a trustworthy project-owned version source exists; a successful build alone is not version evidence. | | Android extensions | Extension samples can retain headers or native assets from an older SDK. | Review extension `include` files and matching assets whenever the SDK dependency changes. | | Third-party beauty | Sample licenses and bundled assets can expire or become incompatible. | Record the current license or asset limitation when the related example changes. | | iOS/macOS signing | Local verification may not cover external package-signing configuration. | Treat signing and packaging as an external handoff; do not report repository acceptance as package completion. | diff --git a/docs/ai-engineering/repository-profile.json b/docs/ai-engineering/repository-profile.json index 009841cc0..788899fee 100644 --- a/docs/ai-engineering/repository-profile.json +++ b/docs/ai-engineering/repository-profile.json @@ -6,38 +6,13 @@ "path": "Android/APIExample/gradle.properties", "kind": "gradle-property", "key": "rtc_sdk_version" - }, - { - "path": "Android/APIExample-Audio/gradle.properties", - "kind": "gradle-property", - "key": "rtc_sdk_version" - }, - { - "path": "Android/APIExample-Compose/gradle.properties", - "kind": "gradle-property", - "key": "rtc_sdk_version" } ], "ios": [ { "path": "iOS/APIExample/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_iOS" - }, - { - "path": "iOS/APIExample-Audio/Podfile", - "kind": "cocoapods", - "package": "AgoraAudio_iOS" - }, - { - "path": "iOS/APIExample-OC/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_iOS" - }, - { - "path": "iOS/APIExample-SwiftUI/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_iOS" + "kind": "external-injection", + "reason": "The RTC SDK frameworks are injected outside the repository, so their version cannot be verified from tracked project files." } ], "macos": [ diff --git a/docs/ai-engineering/templates/acceptance-manifest-template.json b/docs/ai-engineering/templates/acceptance-manifest-template.json index d92e990a0..6052a0cd3 100644 --- a/docs/ai-engineering/templates/acceptance-manifest-template.json +++ b/docs/ai-engineering/templates/acceptance-manifest-template.json @@ -237,6 +237,8 @@ }, "release": { "required": true, + "repository_profile": "docs/ai-engineering/repository-profile.json", + "repository_profile_sha256": "", "target_sdk_versions": { "android": "", "ios": "", diff --git a/docs/ai-engineering/tools/README.md b/docs/ai-engineering/tools/README.md index d6127c98a..bf5bf27fe 100644 --- a/docs/ai-engineering/tools/README.md +++ b/docs/ai-engineering/tools/README.md @@ -27,7 +27,7 @@ For a new feature outside the matrix backlog, add `--sdk-family "Full RTC" --key `--target-sdk-version` is a baseline expanded to Android, iOS, macOS, and Windows. Repeat `--platform-sdk-version platform=x.y.z` for any platform on a different SDK line. For example, the command above resolves to Android 4.6.3 and iOS/macOS/Windows 4.6.2. The final mapping is stored in the execution package and manifest; platform release cadence changes do not require edits to Python or the repository profile. -The default `docs/ai-engineering/repository-profile.json` defines repository-specific SDK version sources. Shared tools support Gradle property, CocoaPods package, and SDK archive-name sources without hard-coding a distribution in Python. `init` accepts `--repository-profile` for a checked-in alternative and binds its path and SHA-256 into the execution package. +The default `docs/ai-engineering/repository-profile.json` defines repository-specific SDK version sources. Shared tools support Gradle property, CocoaPods package, SDK archive-name, and explicit external-injection sources without hard-coding a distribution in Python. An external-injection source keeps its platform SDK check `BLOCKED` because tracked project files cannot prove the injected binary version. `init` accepts `--repository-profile` for a checked-in alternative and binds its path and SHA-256 into the execution package, input snapshots, and acceptance manifest. Dispatch Contract, then the two platform phases: @@ -59,7 +59,7 @@ python3 docs/ai-engineering/tools/orchestrate_case_execution.py assemble \ --cross-platform-evidence "Windows-host verification pending" ``` -Manifest v4 stores one shared Contract and one Implementation/Verification pair per official platform. Implementation retries preserve attempt history and expose one cumulative net delta whose file list must match the manifest. Matrix updates are applied only after structural and evidence-file validation and never for final `BLOCKED`. Assembly also refreshes all live platform SDK dependency versions. +Manifest v4 stores one shared Contract and one Implementation/Verification pair per official platform. Implementation retries preserve attempt history and expose one cumulative net delta whose file list must match the manifest. Matrix updates are applied only after structural and evidence-file validation and never for final `BLOCKED`. Assembly also refreshes all repository-verifiable platform SDK dependency versions and preserves `BLOCKED` for external injection boundaries. Assembly does not accept CI job URLs, package artifact URLs, or QA metadata. Jenkins packaging, QA validation, artifact distribution, and website publication are external handoff processes rather than repository acceptance gates. @@ -91,7 +91,7 @@ python3 docs/ai-engineering/tools/prepare_case_execution.py \ python3 docs/ai-engineering/tools/validate_acceptance_manifest.py ``` -The standalone validator resolves evidence paths relative to the manifest and recomputes prompt, role-artifact, input-snapshot, command-log, and repository-delta hashes. +The standalone validator resolves evidence paths relative to the manifest, verifies the bound repository profile hash, recomputes live SDK checks, and recomputes prompt, role-artifact, input-snapshot, command-log, and repository-delta hashes. Routine manifests and dispatch logs stay outside the repository unless the user requests an evidence snapshot. diff --git a/docs/ai-engineering/tools/generate_case_backlog.py b/docs/ai-engineering/tools/generate_case_backlog.py index 7ea159cb2..34435fc11 100644 --- a/docs/ai-engineering/tools/generate_case_backlog.py +++ b/docs/ai-engineering/tools/generate_case_backlog.py @@ -13,12 +13,7 @@ NON_PLATFORM_COLUMNS = {"Feature", "SDK Family", "Key APIs", "Notes"} PLATFORM_PROJECTS = { "Android full": "Android/APIExample/", - "Android audio": "Android/APIExample-Audio/", - "Android Compose": "Android/APIExample-Compose/", "iOS UIKit": "iOS/APIExample/", - "iOS SwiftUI": "iOS/APIExample-SwiftUI/", - "iOS Objective-C": "iOS/APIExample-OC/", - "iOS audio": "iOS/APIExample-Audio/", "macOS": "macOS/", "Windows": "windows/", } @@ -92,6 +87,17 @@ def find_matrix_table(lines): raise ValueError("could not find pilot matrix table with Feature and Key APIs columns") +def validate_matrix_header(header): + expected = list(PLATFORM_PROJECTS) + actual = [column for column in header if column not in NON_PLATFORM_COLUMNS] + if actual != expected: + raise ValueError( + "platform columns must be exactly: " + + ", ".join(expected) + + f"; found: {', '.join(actual) or 'none'}" + ) + + def find_confirmed_gap_table(lines): for index, line in enumerate(lines): if not line.startswith("|"): @@ -158,6 +164,7 @@ def find_reference_candidates(header, row, target_platform): def generate_execution_units(matrix_path): lines = matrix_path.read_text(encoding="utf-8").splitlines() header, rows = find_matrix_table(lines) + validate_matrix_header(header) gap_severities = build_gap_severity_map(lines) feature_index = header.index("Feature") sdk_index = header.index("SDK Family") diff --git a/docs/ai-engineering/tools/generate_case_backlog_test.py b/docs/ai-engineering/tools/generate_case_backlog_test.py index ebe3dcb46..53d490d5e 100644 --- a/docs/ai-engineering/tools/generate_case_backlog_test.py +++ b/docs/ai-engineering/tools/generate_case_backlog_test.py @@ -34,11 +34,11 @@ def run_generator(self, matrix_text): def test_generates_execution_units_for_missing_and_partial_cells(self): matrix = textwrap.dedent( """ - | Feature | SDK Family | Key APIs | Android full | Android Compose | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | - | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `DONE(samples/JoinChannelAudio.kt)` | `MISSING` | Windows has no basic audio-only join case. | - | Media metadata | Full RTC | `registerMediaMetadataObserver` | `DONE(advanced/MediaMetadata.java)` | `DONE(samples/MediaMetadata.kt)` | `PARTIAL(Advanced/Metadata; smoke pending)` | Runtime metadata smoke pending. | - | Audio mixing | Full RTC | `startAudioMixing` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(samples/PlayAudioFiles.kt)` | `DONE(Advanced/AudioMixing/)` | Full coverage. | + | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | + | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | + | Media metadata | Full RTC | `registerMediaMetadataObserver` | `DONE(advanced/MediaMetadata.java)` | `DONE(Advanced/VideoMetadata/)` | `DONE(Advanced/VideoMetadata/)` | `PARTIAL(Advanced/Metadata; smoke pending)` | Runtime metadata smoke pending. | + | Audio mixing | Full RTC | `startAudioMixing` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage. | ## Confirmed Gaps @@ -72,10 +72,15 @@ def test_generates_execution_units_for_missing_and_partial_cells(self): "path": "basic/JoinChannelAudio.java", }, { - "platform_unit": "Android Compose", - "project": "Android/APIExample-Compose/", - "path": "samples/JoinChannelAudio.kt", - } + "platform_unit": "iOS UIKit", + "project": "iOS/APIExample/", + "path": "Basic/JoinChannelAudio/", + }, + { + "platform_unit": "macOS", + "project": "macOS/", + "path": "Basic/JoinChannelAudio/", + }, ], ) self.assertEqual(payload["execution_units"][1]["platform_unit"], "Windows") @@ -87,9 +92,9 @@ def test_generates_execution_units_for_missing_and_partial_cells(self): def test_hidden_cell_is_parsed_but_not_added_to_backlog(self): matrix = textwrap.dedent( """ - | Feature | SDK Family | Key APIs | Android full | Notes | - | --- | --- | --- | --- | --- | - | Rhythm player | Full RTC | `startRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | Deprecated API retained for reference. | + | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | + | Rhythm player | Full RTC | `startRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(Advanced/RhythmPlayer/)` | `N/A(no case)` | `N/A(no case)` | Deprecated API retained for reference. | """ ) @@ -105,9 +110,9 @@ def test_hidden_cell_is_parsed_but_not_added_to_backlog(self): def test_unknown_platform_column_fails(self): matrix = textwrap.dedent( """ - | Feature | SDK Family | Key APIs | Unknown Platform | Notes | - | --- | --- | --- | --- | --- | - | Feature A | Full RTC | `apiA` | `MISSING` | Missing. | + | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Unknown Platform | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | + | Feature A | Full RTC | `apiA` | `DONE(path)` | `DONE(path)` | `DONE(path)` | `MISSING` | Missing. | """ ) @@ -119,9 +124,9 @@ def test_unknown_platform_column_fails(self): def test_confirmed_gap_alias_applies_priority(self): matrix = textwrap.dedent( """ - | Feature | SDK Family | Key APIs | Android full | Windows | Notes | - | --- | --- | --- | --- | --- | --- | - | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `MISSING` | Windows has no basic audio-only join case. | + | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | + | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | ## Confirmed Gaps @@ -140,6 +145,21 @@ def test_confirmed_gap_alias_applies_priority(self): self.assertEqual(unit["severity"], "High") self.assertEqual(unit["priority"], 10) + def test_legacy_project_variant_column_fails_even_without_actionable_status(self): + matrix = textwrap.dedent( + """ + | Feature | SDK Family | Key APIs | Android full | Android Compose | iOS UIKit | macOS | Windows | Notes | + | --- | --- | --- | --- | --- | --- | --- | --- | --- | + | Feature A | Full RTC | `apiA` | `DONE(path)` | `DONE(path)` | `DONE(path)` | `DONE(path)` | `DONE(path)` | Covered. | + """ + ) + + result = self.run_generator(matrix) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("platform columns must be exactly", result.stderr) + self.assertIn("Android Compose", result.stderr) + if __name__ == "__main__": unittest.main() diff --git a/docs/ai-engineering/tools/lint_agent_assets.py b/docs/ai-engineering/tools/lint_agent_assets.py index 26da6b03a..fca55b4a2 100644 --- a/docs/ai-engineering/tools/lint_agent_assets.py +++ b/docs/ai-engineering/tools/lint_agent_assets.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Lint the repository AI engineering assets: AGENTS.md, SKILL.md, and skill bundles. +"""Lint the repository AI engineering assets and skill bundles. Each rule guards a drift class that has actually broken these assets before: @@ -7,7 +7,7 @@ - `skill-frontmatter` a SKILL.md has no YAML frontmatter, so it cannot be selected - `skill-name` `name` is missing, malformed, or disagrees with its directory - `skill-description` `description` is missing or exceeds the specification limit -- `asset-reference` an AGENTS.md or SKILL.md points at a file that does not exist +- `asset-reference` an AGENTS.md, ARCHITECTURE.md, or SKILL.md points at a missing file Exit code is 0 when every rule passes and 1 when any finding is reported. """ @@ -42,7 +42,7 @@ "xcuserdata", } -ASSET_FILE_NAMES = ("AGENTS.md", "SKILL.md") +ASSET_FILE_NAMES = ("AGENTS.md", "ARCHITECTURE.md", "SKILL.md") FRONTMATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) FRONTMATTER_KEY_RE = re.compile(r"^([A-Za-z0-9_-]+):\s*(.*)$") @@ -55,6 +55,13 @@ MAX_SKILL_DESCRIPTION_LENGTH = 1024 INLINE_CODE_RE = re.compile(r"`([^`\n]+)`") +# ARCHITECTURE directory trees commonly put exact skill paths inside fenced code +# blocks rather than inline code spans. Match those paths without interpreting +# arbitrary project source paths as agent-asset references. +BARE_SKILL_REFERENCE_RE = re.compile( + r"(?/-])" + r"((?:[A-Za-z0-9_.-]+/)*\.agents/skills/[a-z0-9]+(?:-[a-z0-9]+)*/)" +) PLACEHOLDER_CHARS = set("<>*?|$()[]") # Only references that identify an agent asset are resolved. Project source paths are # written relative to a source root that varies per platform, so checking them here @@ -267,8 +274,12 @@ def check_asset_references(asset_path, root): for line_number, line in enumerate( asset_path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1 ): - for match in INLINE_CODE_RE.finditer(line): - ref = match.group(1).strip().rstrip(",.;:") + references = [match.group(1) for match in INLINE_CODE_RE.finditer(line)] + references.extend( + match.group(1) for match in BARE_SKILL_REFERENCE_RE.finditer(line) + ) + for raw_ref in references: + ref = raw_ref.strip().rstrip(",.;:") if not is_asset_reference(ref): continue if ref in seen: diff --git a/docs/ai-engineering/tools/lint_agent_assets_test.py b/docs/ai-engineering/tools/lint_agent_assets_test.py index 9fc260431..66db55b84 100644 --- a/docs/ai-engineering/tools/lint_agent_assets_test.py +++ b/docs/ai-engineering/tools/lint_agent_assets_test.py @@ -65,6 +65,7 @@ def test_clean_tree_reports_no_findings(self): self.assertEqual(findings, [], self.messages(findings)) self.assertIn("Android/APIExample/.agents/skills/upsert-case/SKILL.md", payload["checked_files"]) + self.assertIn("Android/APIExample/ARCHITECTURE.md", payload["checked_files"]) self.assertEqual(payload["findings"], []) def test_flags_legacy_agent_skills_directory(self): @@ -198,6 +199,26 @@ def test_flags_agents_md_pointing_at_missing_skill_directory(self): self.assertEqual(self.rules(findings), ["asset-reference"]) self.assertIn(".agents/skills/review-case/", self.messages(findings)) + def test_flags_architecture_md_pointing_at_missing_skill_directory(self): + with tempfile.TemporaryDirectory() as tmpdir: + self.write_clean_tree(tmpdir) + self.write( + tmpdir, + "Android/APIExample/ARCHITECTURE.md", + """ + ```text + APIExample/ + .agents/skills/query-cases/ # Missing inventory skill + ``` + """, + ) + + payload, findings = lint(tmpdir) + + self.assertIn("Android/APIExample/ARCHITECTURE.md", payload["checked_files"]) + self.assertEqual(self.rules(findings), ["asset-reference"]) + self.assertIn(".agents/skills/query-cases/", self.messages(findings)) + def test_flags_skill_pointing_at_missing_bundled_reference(self): with tempfile.TemporaryDirectory() as tmpdir: self.write_clean_tree(tmpdir) diff --git a/docs/ai-engineering/tools/orchestrate_case_execution.py b/docs/ai-engineering/tools/orchestrate_case_execution.py index 5429945d2..b23f920af 100644 --- a/docs/ai-engineering/tools/orchestrate_case_execution.py +++ b/docs/ai-engineering/tools/orchestrate_case_execution.py @@ -21,8 +21,10 @@ prepare_case_execution, ) from validate_acceptance_manifest import ( + MAIN_PLATFORM_TARGETS, PLATFORM_ROOTS, is_platform_build_command, + normalize_command, path_is_within, validate_contract, validate_evidence_files, @@ -258,6 +260,17 @@ def write_prompt(run_dir, name, role, platform, package, routing, dependencies): "waiver_reason field, use SKIPPED for Verification result/parity_result/build_result, " "and report no executed build command." ) + contract_context = "" + if role == "contract": + contract_context = f"""Selected platform units: +{json.dumps(package['platform_units'], indent=2, ensure_ascii=False)} + +Prepared reference contract: +{json.dumps(package['reference_contract'], indent=2, ensure_ascii=False)} + +Use this routing and reference context when producing platform_targets and reference. Every +target_project must remain the main APIExample project declared for that platform. +""" body = f"""# {name} Agent Task Requirement: {requirement['feature']} @@ -273,6 +286,8 @@ def write_prompt(run_dir, name, role, platform, package, routing, dependencies): Required output: {json.dumps(package['role_contracts'][role], indent=2, ensure_ascii=False)} +{contract_context} + Dependency artifacts: {format_dependency_context(dependencies)} @@ -301,6 +316,12 @@ def host_constraint(platform): "substitute compiler as platform evidence. Run repository-local static checks only " "and report unavailable Windows build/runtime evidence as BLOCKED." ) + if platform in {"ios", "macos"} and sys.platform != "darwin": + return ( + f"This host is {sys.platform}. {sys.platform} cannot provide {platform} Xcode build " + "evidence. Run repository-local static checks only and report unavailable native " + "build/runtime evidence as BLOCKED. A build_result=PASS requires host_platform=darwin." + ) return "" @@ -532,10 +553,10 @@ def resolve_working_directory(package, spec, dependencies): if not spec["platform"]: return REPO_ROOT target = resolved_platform_target(package, spec["platform"], dependencies) - platform_root = PLATFORM_ROOTS[spec["platform"]] - if not path_is_within(target, platform_root): + expected_target = MAIN_PLATFORM_TARGETS[spec["platform"]] + if target != expected_target: raise ValueError( - f"{spec['platform']} target must be inside {platform_root}: {target}" + f"{spec['platform']} target must be exactly {expected_target}: {target}" ) path = (REPO_ROOT / target).resolve() try: @@ -750,7 +771,7 @@ def resolve_model(profile, override): def read_codex_version(codex_bin): try: result = subprocess.run( - [codex_bin, "--version"], + [*codex_command_prefix(codex_bin), "--version"], cwd=REPO_ROOT, text=True, stdout=subprocess.PIPE, @@ -766,6 +787,13 @@ def read_codex_version(codex_bin): return result.stdout.strip() +def codex_command_prefix(codex_bin): + path = str(codex_bin) + if Path(path).suffix.lower() == ".py": + return [sys.executable, path] + return [path] + + def write_input_snapshot(run_dir, phase, name, dependencies, resolved_routing): package = read_json(run_dir / "execution-package.json") dependency_hashes = { @@ -1001,7 +1029,7 @@ def build_codex_command( ): raw_result_path.parent.mkdir(parents=True, exist_ok=True) command = [ - codex_bin, + *codex_command_prefix(codex_bin), "exec", "-C", str(working_directory), @@ -1175,8 +1203,6 @@ def terminate_process_tree(process): def validate_verification_host(platform, agent_result, host_platform): - if platform != "windows" or host_platform == "win32": - return output = agent_result.get("output", {}) has_build_execution = any( isinstance(command, dict) @@ -1184,11 +1210,21 @@ def validate_verification_host(platform, agent_result, host_platform): and command.get("result") in {"PASS", "FAIL"} for command in output.get("commands", []) ) - if agent_result.get("status") == "WAIVED": + has_pass_build = any( + isinstance(command, dict) + and command.get("kind") == "build" + and command.get("result") == "PASS" + for command in output.get("commands", []) + ) + if ( + platform == "windows" + and host_platform != "win32" + and agent_result.get("status") == "WAIVED" + ): if output.get("build_result") == "PASS" or has_build_execution: raise ValueError("WAIVED Windows Verification cannot report build execution") return - if ( + if platform == "windows" and host_platform != "win32" and ( agent_result.get("status") != "BLOCKED" or output.get("result") != "BLOCKED" or output.get("build_result") != "BLOCKED" @@ -1198,6 +1234,15 @@ def validate_verification_host(platform, agent_result, host_platform): "Windows Verification build evidence requires a Windows host; " f"current host_platform={host_platform}" ) + if ( + platform in {"ios", "macos"} + and host_platform != "darwin" + and (output.get("build_result") == "PASS" or has_pass_build) + ): + raise ValueError( + f"{platform} Verification build PASS requires a Darwin host; " + f"current host_platform={host_platform}" + ) def bind_verification_command_evidence( @@ -1218,27 +1263,55 @@ def bind_verification_command_evidence( f"{declared.get('command')}" ) declared_text = normalize_command(declared.get("command")) + candidates = [ + event_index + for event_index, event in enumerate(events) + if event_index not in used and normalize_command(event["command"]) == declared_text + ] + if not candidates: + raise ValueError( + f"Verification command {index} has no matching command_execution event: {declared.get('command')}" + ) match_index = next( ( event_index - for event_index, event in enumerate(events) - if event_index not in used and normalize_command(event["command"]) == declared_text + for event_index in candidates + if events[event_index].get("cwd") + and command_event_cwd_matches( + events[event_index]["cwd"], working_directory_manifest + ) ), None, ) if match_index is None: + # Codex CLI 0.144.x omits cwd from command_execution events. In that + # format the subprocess cwd and `codex exec -C` target are the bound + # source; recognized build commands separately reject cwd overrides. + match_index = next( + ( + event_index + for event_index in candidates + if not events[event_index].get("cwd") + ), + None, + ) + if match_index is None: + actual_cwds = [events[event_index].get("cwd") for event_index in candidates] raise ValueError( - f"Verification command {index} has no matching command_execution event: {declared.get('command')}" + f"Verification command {index} cwd does not match dispatched working directory " + f"{working_directory_manifest}: {actual_cwds}" ) used.add(match_index) event = events[match_index] + cwd_source = "command_event" if event.get("cwd") else "dispatch" actual_result = "PASS" if event["exit_code"] == 0 else "FAIL" if result != actual_result: raise ValueError( f"Verification command {declared.get('command')} declared {result} but exit_code={event['exit_code']}" ) declared["evidence"] = ( - f"{log_path}#{event['id']}; exit_code={event['exit_code']}" + f"{log_path}#{event['id']}; exit_code={event['exit_code']}; " + f"cwd={working_directory_manifest}; cwd_source={cwd_source}" ) @@ -1259,15 +1332,20 @@ def extract_command_events(jsonl): "id": str(item.get("id") or f"command-{len(events)}"), "command": item.get("command"), "exit_code": item["exit_code"], + "cwd": item.get("cwd") or item.get("working_directory"), } ) return events -def normalize_command(command): - if isinstance(command, list): - command = " ".join(str(part) for part in command) - return " ".join(str(command or "").split()) +def command_event_cwd_matches(event_cwd, working_directory_manifest): + if not isinstance(event_cwd, str) or not event_cwd.strip(): + return False + expected = (REPO_ROOT / working_directory_manifest).resolve() + actual = Path(event_cwd) + if not actual.is_absolute(): + actual = REPO_ROOT / actual + return actual.resolve() == expected def extract_run_id(jsonl): @@ -1315,6 +1393,10 @@ def assemble_workspace(args): "evidence": args.cross_platform_evidence, "differences": args.cross_platform_difference, } + manifest["release"]["repository_profile"] = package["repository_profile"] + manifest["release"]["repository_profile_sha256"] = package[ + "repository_profile_sha256" + ] manifest["release"]["checks"] = collect_sdk_version_checks( manifest["requirement"]["target_sdk_versions"], profile_path=profile_path ) diff --git a/docs/ai-engineering/tools/orchestrate_case_execution_test.py b/docs/ai-engineering/tools/orchestrate_case_execution_test.py index 07875122d..3cbcdc9ae 100644 --- a/docs/ai-engineering/tools/orchestrate_case_execution_test.py +++ b/docs/ai-engineering/tools/orchestrate_case_execution_test.py @@ -18,7 +18,12 @@ sys.path.insert(0, str(TOOLS_DIR)) import orchestrate_case_execution as orchestrator -from orchestrate_case_execution import collect_repository_fingerprints, run_codex_role +from orchestrate_case_execution import ( + bind_verification_command_evidence, + collect_repository_fingerprints, + run_codex_role, + validate_verification_host, +) class RequirementOrchestratorTest(unittest.TestCase): @@ -76,7 +81,7 @@ def init_workspace(self, matrix_path, run_dir, platform_sdk_versions=None): def write_fake_codex( self, directory, implementation_sleep=0, fail_implementation_platform=None ): - path = Path(directory) / "fake-codex" + path = Path(directory) / "fake-codex.py" path.write_text( textwrap.dedent( f"""\ @@ -144,19 +149,23 @@ def write_fake_codex( "matrix_updates": [], }} else: - if platform == "windows" and sys.platform != "win32": + host_mismatch = ( + (platform == "windows" and sys.platform != "win32") + or (platform in {{"ios", "macos"}} and sys.platform != "darwin") + ) + if host_mismatch: status = "BLOCKED" output = {{ "result": "BLOCKED", - "findings": ["Windows build requires a Windows host."], + "findings": [f"{{platform}} build requires its native host."], "parity_result": "PASS", "entry_point": "Basic > Join channel audio", "ux_notes": "Static review only on this host.", "commands": [], "build_result": "BLOCKED", "skipped_checks": [{{ - "name": "Windows MSBuild", - "reason": "Current host is not Windows.", + "name": f"{{platform}} native build", + "reason": "Current host cannot provide native build evidence.", }}], }} else: @@ -192,6 +201,7 @@ def write_fake_codex( "command": build_commands[platform], "status": "completed", "exit_code": 0, + "cwd": str(Path.cwd()), }}, }}), flush=True) """ @@ -237,6 +247,14 @@ def test_init_creates_shared_contract_and_eight_platform_role_tasks(self): "docs/ai-engineering/repository-profile.json", ) self.assertEqual(len(package["repository_profile_sha256"]), 64) + self.assertEqual( + manifest["release"]["repository_profile"], + package["repository_profile"], + ) + self.assertEqual( + manifest["release"]["repository_profile_sha256"], + package["repository_profile_sha256"], + ) def test_execution_configuration_rejects_repository_profile_drift(self): routing_path = REPO_ROOT / "docs/ai-engineering/role-routing.json" @@ -296,6 +314,26 @@ def test_init_accepts_new_requirement_outside_the_matrix_backlog(self): contract_prompt = (run_dir / "role-prompts/contract.md").read_text() self.assertNotIn("If Contract marks this platform required=false", contract_prompt) + def test_contract_prompt_includes_platform_units_and_prepared_reference(self): + matrix_path = self.write_matrix() + with tempfile.TemporaryDirectory() as tmpdir: + run_dir = Path(tmpdir) / "run" + self.init_workspace(matrix_path, run_dir) + + package = json.loads((run_dir / "execution-package.json").read_text()) + contract_prompt = (run_dir / "role-prompts/contract.md").read_text() + + self.assertIn("Selected platform units:", contract_prompt) + self.assertIn( + json.dumps(package["platform_units"], indent=2, ensure_ascii=False), + contract_prompt, + ) + self.assertIn("Prepared reference contract:", contract_prompt) + self.assertIn( + json.dumps(package["reference_contract"], indent=2, ensure_ascii=False), + contract_prompt, + ) + def test_init_persists_and_prompts_staggered_platform_sdk_versions(self): matrix_path = self.write_matrix() with tempfile.TemporaryDirectory() as tmpdir: @@ -765,6 +803,132 @@ def test_macos_rejects_windows_build_pass(self): timeout_seconds=5, ) + def test_non_darwin_host_rejects_apple_build_pass(self): + result = { + "status": "PASS", + "output": { + "result": "PASS", + "commands": [ + { + "kind": "build", + "command": "xcodebuild -scheme APIExample build", + "result": "PASS", + } + ], + "build_result": "PASS", + }, + } + + for platform in ["ios", "macos"]: + with self.subTest(platform=platform): + with self.assertRaisesRegex(ValueError, "requires a Darwin host"): + validate_verification_host(platform, result, "linux") + + def test_command_evidence_requires_dispatched_working_directory(self): + output = { + "commands": [ + { + "kind": "build", + "command": "xcodebuild -scheme APIExample build", + "result": "PASS", + } + ] + } + wrong_cwd_event = json.dumps( + { + "type": "item.completed", + "item": { + "id": "build", + "type": "command_execution", + "command": "xcodebuild -scheme APIExample build", + "exit_code": 0, + "cwd": str(REPO_ROOT / "iOS/APIExample-Audio"), + }, + } + ) + + with self.assertRaisesRegex(ValueError, "cwd does not match"): + bind_verification_command_evidence( + output, + wrong_cwd_event, + "dispatch-logs/ios-verification.jsonl", + "ios", + "iOS/APIExample/", + ) + + def test_command_evidence_uses_dispatch_cwd_when_event_omits_it(self): + output = { + "commands": [ + { + "kind": "build", + "command": "xcodebuild -scheme APIExample build", + "result": "PASS", + } + ] + } + missing_cwd_event = json.dumps( + { + "type": "item.completed", + "item": { + "id": "build", + "type": "command_execution", + "command": "/bin/zsh -lc 'xcodebuild -scheme APIExample build'", + "exit_code": 0, + }, + } + ) + + bind_verification_command_evidence( + output, + missing_cwd_event, + "dispatch-logs/ios-verification.jsonl", + "ios", + "iOS/APIExample/", + ) + + self.assertEqual( + output["commands"][0]["evidence"], + "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " + "cwd=iOS/APIExample/; cwd_source=dispatch", + ) + + def test_command_evidence_preserves_verified_cwd_binding(self): + output = { + "commands": [ + { + "kind": "build", + "command": "xcodebuild -scheme APIExample build", + "result": "PASS", + } + ] + } + event = json.dumps( + { + "type": "item.completed", + "item": { + "id": "build", + "type": "command_execution", + "command": "xcodebuild -scheme APIExample build", + "exit_code": 0, + "cwd": str(REPO_ROOT / "iOS/APIExample"), + }, + } + ) + + bind_verification_command_evidence( + output, + event, + "dispatch-logs/ios-verification.jsonl", + "ios", + "iOS/APIExample/", + ) + + self.assertEqual( + output["commands"][0]["evidence"], + "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " + "cwd=iOS/APIExample/; cwd_source=command_event", + ) + def test_verification_semantic_failure_becomes_retryable(self): matrix_path = self.write_matrix() with tempfile.TemporaryDirectory() as tmpdir: @@ -813,7 +977,7 @@ def test_platform_filter_dispatches_only_requested_agent(self): self.assertIn("-C", command) self.assertIn(str(REPO_ROOT / "windows"), command) - def test_platform_prompt_uses_target_selected_by_completed_contract(self): + def test_platform_dispatch_rejects_non_main_project_selected_by_contract(self): matrix_path = self.write_matrix() with tempfile.TemporaryDirectory() as tmpdir: run_dir = Path(tmpdir) / "run" @@ -839,10 +1003,11 @@ def test_platform_prompt_uses_target_selected_by_completed_contract(self): "--dry-run", ) - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - prompt = (run_dir / "role-prompts/ios-implementation.md").read_text() - self.assertIn("Target: iOS/APIExample-OC/", prompt) - self.assertIn("If Contract marks this platform required=false", prompt) + self.assertEqual(result.returncode, 1) + self.assertIn( + "contract.output.platform_targets.ios.target_project must be exactly iOS/APIExample/", + result.stderr, + ) def test_verification_uses_corresponding_platform_implementation(self): matrix_path = self.write_matrix() @@ -1017,6 +1182,22 @@ def test_assemble_reports_only_repository_gates_without_external_metadata(self): with tempfile.TemporaryDirectory() as tmpdir: run_dir = Path(tmpdir) / "run" self.init_workspace(matrix_path, run_dir) + manifest_path = run_dir / "acceptance-manifest.json" + manifest = json.loads(manifest_path.read_text()) + ios = next( + check + for check in manifest["release"]["checks"] + if check["name"] == "sdk-version-ios" + ) + ios.update( + { + "result": "PASS", + "actual_versions": {"iOS/APIExample/sdk.podspec": self.TARGET_SDK_VERSION}, + "evidence": "Untrusted pre-assembly value.", + "reason": "", + } + ) + manifest_path.write_text(json.dumps(manifest), encoding="utf-8") codex_bin = self.write_fake_codex(tmpdir) self.dispatch_contract(run_dir, codex_bin) for phase in ["implementation", "verification"]: @@ -1049,6 +1230,7 @@ def test_assemble_reports_only_repository_gates_without_external_metadata(self): self.assertEqual(result.returncode, 1) self.assertIn("sdk-version-android", result.stderr) + self.assertIn("non-BLOCKED acceptance requires sdk-version-ios=PASS", result.stderr) self.assertNotIn("qa_acceptance", result.stderr) def test_assemble_rejects_matrix_path_changed_since_init(self): diff --git a/docs/ai-engineering/tools/prepare_case_execution.py b/docs/ai-engineering/tools/prepare_case_execution.py index c62e47524..85807710d 100644 --- a/docs/ai-engineering/tools/prepare_case_execution.py +++ b/docs/ai-engineering/tools/prepare_case_execution.py @@ -2,6 +2,7 @@ """Prepare one cross-platform API Examples requirement package.""" import argparse +import hashlib import json import re import sys @@ -15,6 +16,7 @@ PROFILE_SOURCE_FIELDS = { "gradle-property": {"path", "kind", "key"}, "cocoapods": {"path", "kind", "package"}, + "external-injection": {"path", "kind", "reason"}, "archive-name": {"path", "kind", "prefix", "suffix"}, } SEMVER_CAPTURE = r"([0-9]+\.[0-9]+\.[0-9]+)" @@ -27,12 +29,7 @@ } PLATFORM_UNIT_GROUPS = { "Android full": "android", - "Android audio": "android", - "Android Compose": "android", "iOS UIKit": "ios", - "iOS SwiftUI": "ios", - "iOS Objective-C": "ios", - "iOS audio": "ios", "macOS": "macos", "Windows": "windows", } @@ -222,6 +219,26 @@ def sdk_version_pattern(source): raise ValueError(f"unsupported SDK version source kind: {kind}") +def primary_project_version_sources(sources): + selected = {} + for platform, target in DEFAULT_PLATFORM_TARGETS.items(): + target_path = Path(target) + platform_sources = [] + for source in sources[platform]: + source_path = Path(source["path"]) + try: + source_path.relative_to(target_path) + except ValueError: + continue + platform_sources.append(source) + if not platform_sources: + raise ValueError( + f"repository profile has no {platform} SDK version source inside {target}" + ) + selected[platform] = platform_sources + return selected + + def collect_sdk_version_checks( target_sdk_versions, repo_root=REPO_ROOT, @@ -230,6 +247,7 @@ def collect_sdk_version_checks( ): if sources is None: sources = load_repository_profile(profile_path)["sdk_version_sources"] + sources = primary_project_version_sources(sources) checks = [] for platform in PLATFORMS: target_sdk_version = target_sdk_versions[platform] @@ -243,6 +261,10 @@ def collect_sdk_version_checks( actual_versions[path_text] = "" problems.append(f"missing {path_text}") continue + if source["kind"] == "external-injection": + actual_versions[path_text] = "" + problems.append(f"{path_text}: {source['reason']}") + continue pattern = sdk_version_pattern(source) matches = sorted(set(re.findall(pattern, path.read_text(encoding="utf-8")))) if not matches: @@ -263,10 +285,10 @@ def collect_sdk_version_checks( "result": result, "expected_version": target_sdk_version, "actual_versions": actual_versions, - "evidence": ( - "; ".join(f"{path}={version}" for path, version in actual_versions.items()) - if actual_versions - else "" + "evidence": "; ".join( + f"{path}={version}" + for path, version in actual_versions.items() + if version ), "reason": "" if not problems else f"Expected {target_sdk_version}; " + "; ".join(problems), } @@ -274,7 +296,22 @@ def collect_sdk_version_checks( return checks -def build_manifest_seed(requirement, source_case, version_sources): +def repository_profile_binding(profile_path): + resolved = Path(profile_path).resolve() + try: + relative = resolved.relative_to(REPO_ROOT.resolve()).as_posix() + except ValueError as exc: + raise ValueError("repository profile must be inside the repository") from exc + return relative, hashlib.sha256(resolved.read_bytes()).hexdigest() + + +def build_manifest_seed( + requirement, + source_case, + version_sources, + repository_profile, + repository_profile_sha256, +): reference_required = bool(source_case) reference_result = "BLOCKED" if reference_required else "SKIPPED" targets = default_platform_targets() @@ -336,6 +373,8 @@ def build_manifest_seed(requirement, source_case, version_sources): }, "release": { "required": True, + "repository_profile": repository_profile, + "repository_profile_sha256": repository_profile_sha256, "target_sdk_versions": dict(requirement["target_sdk_versions"]), "checks": collect_sdk_version_checks( requirement["target_sdk_versions"], sources=version_sources @@ -376,7 +415,9 @@ def prepare_case_execution( target_sdk_versions = resolve_target_sdk_versions( target_sdk_version, platform_sdk_versions ) - profile = load_repository_profile(repository_profile) + profile_path = Path(repository_profile).resolve() + profile = load_repository_profile(profile_path) + profile_manifest_path, profile_sha256 = repository_profile_binding(profile_path) backlog = generate_execution_units(matrix_path) matching = [ unit for unit in backlog["execution_units"] if feature is None or unit["feature"] == feature @@ -416,7 +457,11 @@ def prepare_case_execution( "Validate the manifest before applying matrix updates.", ], "acceptance_manifest_seed": build_manifest_seed( - requirement, source_case, profile["sdk_version_sources"] + requirement, + source_case, + profile["sdk_version_sources"], + profile_manifest_path, + profile_sha256, ), "validation_command": "python3 docs/ai-engineering/tools/validate_acceptance_manifest.py ", "blockers": blockers, diff --git a/docs/ai-engineering/tools/prepare_case_execution_test.py b/docs/ai-engineering/tools/prepare_case_execution_test.py index a4195fa1c..bf782825a 100644 --- a/docs/ai-engineering/tools/prepare_case_execution_test.py +++ b/docs/ai-engineering/tools/prepare_case_execution_test.py @@ -14,6 +14,7 @@ collect_sdk_version_checks, load_repository_profile, prepare_case_execution, + primary_project_version_sources, resolve_target_sdk_versions, ) from validate_acceptance_manifest import validate_manifest @@ -70,6 +71,11 @@ def test_prepares_one_requirement_with_four_platform_delivery_units(self): self.target_sdk_versions(), ) self.assertTrue(manifest["release"]["required"]) + self.assertEqual( + manifest["release"]["repository_profile"], + "docs/ai-engineering/repository-profile.json", + ) + self.assertRegex(manifest["release"]["repository_profile_sha256"], r"^[0-9a-f]{64}$") self.assertEqual( manifest["release"]["target_sdk_versions"], self.target_sdk_versions(), @@ -97,6 +103,18 @@ def test_platform_defaults_select_official_full_sdk_projects(self): self.assertEqual(targets["macos"]["target_project"], "macOS/") self.assertEqual(targets["windows"]["target_project"], "windows/") self.assertTrue(all(target["required"] for target in targets.values())) + self.assertEqual( + { + platform: unit["target_project"] + for platform, unit in package["platform_units"].items() + }, + { + "android": "Android/APIExample/", + "ios": "iOS/APIExample/", + "macos": "macOS/", + "windows": "windows/", + }, + ) def test_selects_highest_priority_feature_when_omitted(self): package = prepare_case_execution( @@ -190,7 +208,7 @@ def test_collects_live_sdk_version_evidence(self): ) ) - archive = root / "windows/install.ps1" + archive = root / "windows/APIExample/install.ps1" archive.write_text( f"{distribution}_Native_SDK_for_Windows_v4.6.2_FULL.zip\n", encoding="utf-8", @@ -206,6 +224,69 @@ def test_collects_live_sdk_version_evidence(self): self.assertEqual(windows["result"], "BLOCKED") self.assertIn("4.6.2", windows["reason"]) + def test_ignores_non_primary_project_sdk_version_sources(self): + sources = { + "android": [ + { + "path": "Android/APIExample/gradle.properties", + "kind": "gradle-property", + "key": "rtc_sdk_version", + }, + { + "path": "Android/APIExample-Compose/gradle.properties", + "kind": "gradle-property", + "key": "rtc_sdk_version", + }, + ], + "ios": [ + { + "path": "iOS/APIExample/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_iOS", + }, + { + "path": "iOS/APIExample-SwiftUI/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_iOS", + }, + ], + "macos": [ + { + "path": "macOS/Podfile", + "kind": "cocoapods", + "package": "AgoraRtcEngine_macOS", + } + ], + "windows": [ + { + "path": "windows/APIExample/install.ps1", + "kind": "archive-name", + "prefix": "Agora_Native_SDK_for_Windows_v", + "suffix": "_FULL.zip", + } + ], + } + + selected = primary_project_version_sources(sources) + + self.assertEqual(len(selected["android"]), 1) + self.assertEqual( + selected["android"][0]["path"], "Android/APIExample/gradle.properties" + ) + self.assertEqual(len(selected["ios"]), 1) + self.assertEqual(selected["ios"][0]["path"], "iOS/APIExample/Podfile") + + def test_requires_primary_project_sdk_version_source(self): + sources = { + "android": [{"path": "Android/APIExample-Compose/gradle.properties"}], + "ios": [{"path": "iOS/APIExample/Podfile"}], + "macos": [{"path": "macOS/Podfile"}], + "windows": [{"path": "windows/APIExample/install.ps1"}], + } + + with self.assertRaisesRegex(ValueError, "no android SDK version source"): + primary_project_version_sources(sources) + def test_rejects_commented_out_cocoapods_declaration(self): for comment_line in [ " # pod 'AgoraRtcEngine_iOS', '4.6.4'", @@ -215,7 +296,7 @@ def test_rejects_commented_out_cocoapods_declaration(self): with self.subTest(comment_line=comment_line), tempfile.TemporaryDirectory() as tmpdir: root = Path(tmpdir) profile_path = self.write_repository_profile(root, "Agora") - (root / "iOS/Podfile").write_text( + (root / "iOS/APIExample/Podfile").write_text( f"def common_pods\n{comment_line}\n" " pod 'sdk', :path => 'sdk.podspec'\nend\n", encoding="utf-8", @@ -229,14 +310,16 @@ def test_rejects_commented_out_cocoapods_declaration(self): ios = next(check for check in checks if check["name"] == "sdk-version-ios") self.assertEqual(ios["result"], "BLOCKED") - self.assertEqual(ios["actual_versions"]["iOS/Podfile"], "") - self.assertIn("no active SDK version declaration in iOS/Podfile", ios["reason"]) + self.assertEqual(ios["actual_versions"]["iOS/APIExample/Podfile"], "") + self.assertIn( + "no active SDK version declaration in iOS/APIExample/Podfile", ios["reason"] + ) def test_accepts_cocoapods_declaration_with_trailing_comment(self): with tempfile.TemporaryDirectory() as tmpdir: root = Path(tmpdir) profile_path = self.write_repository_profile(root, "Agora") - (root / "iOS/Podfile").write_text( + (root / "iOS/APIExample/Podfile").write_text( " pod 'AgoraRtcEngine_iOS', '4.6.4' # pinned for this release\n", encoding="utf-8", ) @@ -249,16 +332,49 @@ def test_accepts_cocoapods_declaration_with_trailing_comment(self): ios = next(check for check in checks if check["name"] == "sdk-version-ios") self.assertEqual(ios["result"], "PASS") - self.assertEqual(ios["actual_versions"]["iOS/Podfile"], "4.6.4") + self.assertEqual(ios["actual_versions"]["iOS/APIExample/Podfile"], "4.6.4") + + def test_external_injection_remains_blocked_without_repository_version_evidence(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + profile_path = self.write_repository_profile(root, "Agora") + profile = json.loads(profile_path.read_text(encoding="utf-8")) + profile["sdk_version_sources"]["ios"] = [ + { + "path": "iOS/APIExample/Podfile", + "kind": "external-injection", + "reason": "RTC SDK binaries are injected outside the repository.", + } + ] + profile_path.write_text(json.dumps(profile), encoding="utf-8") + podspec = root / "iOS/APIExample/sdk.podspec" + podspec.parent.mkdir(parents=True, exist_ok=True) + podspec.write_text( + "spec.version = \"4.6.4\"\n", + encoding="utf-8", + ) + + checks = collect_sdk_version_checks( + self.target_sdk_versions(), repo_root=root, profile_path=profile_path + ) + ios = next(check for check in checks if check["name"] == "sdk-version-ios") + self.assertEqual(ios["result"], "BLOCKED") + self.assertEqual(ios["actual_versions"], {"iOS/APIExample/Podfile": ""}) + self.assertEqual(ios["evidence"], "") + self.assertIn("injected outside the repository", ios["reason"]) def test_collects_staggered_platform_sdk_versions(self): with tempfile.TemporaryDirectory() as tmpdir: root = Path(tmpdir) profile_path = self.write_repository_profile(root, "Agora") - (root / "Android/gradle.properties").write_text( + (root / "Android/APIExample/gradle.properties").write_text( "rtc_sdk_version = 4.6.3\n", encoding="utf-8" ) - for path_text in ["iOS/Podfile", "macOS/Podfile", "windows/install.ps1"]: + for path_text in [ + "iOS/APIExample/Podfile", + "macOS/Podfile", + "windows/APIExample/install.ps1", + ]: path = root / path_text path.write_text( path.read_text(encoding="utf-8").replace("4.6.4", "4.6.2"), @@ -285,14 +401,14 @@ def write_repository_profile(self, root, distribution): sources = { "android": [ { - "path": "Android/gradle.properties", + "path": "Android/APIExample/gradle.properties", "kind": "gradle-property", "key": "rtc_sdk_version", } ], "ios": [ { - "path": "iOS/Podfile", + "path": "iOS/APIExample/Podfile", "kind": "cocoapods", "package": f"{distribution}RtcEngine_iOS", } @@ -306,7 +422,7 @@ def write_repository_profile(self, root, distribution): ], "windows": [ { - "path": "windows/install.ps1", + "path": "windows/APIExample/install.ps1", "kind": "archive-name", "prefix": f"{distribution}_Native_SDK_for_Windows_v", "suffix": "_FULL.zip", @@ -314,10 +430,10 @@ def write_repository_profile(self, root, distribution): ], } contents = { - "Android/gradle.properties": "rtc_sdk_version = 4.6.4\n", - "iOS/Podfile": f"pod '{distribution}RtcEngine_iOS', '4.6.4'\n", + "Android/APIExample/gradle.properties": "rtc_sdk_version = 4.6.4\n", + "iOS/APIExample/Podfile": f"pod '{distribution}RtcEngine_iOS', '4.6.4'\n", "macOS/Podfile": f"pod '{distribution}RtcEngine_macOS', '4.6.4'\n", - "windows/install.ps1": ( + "windows/APIExample/install.ps1": ( f"{distribution}_Native_SDK_for_Windows_v4.6.4_FULL.zip\n" ), } diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest.py b/docs/ai-engineering/tools/validate_acceptance_manifest.py index 72c455dbf..0bc726ed3 100644 --- a/docs/ai-engineering/tools/validate_acceptance_manifest.py +++ b/docs/ai-engineering/tools/validate_acceptance_manifest.py @@ -9,6 +9,7 @@ from pathlib import Path from generate_case_backlog import PLATFORM_PROJECTS, parse_matrix_cell +from prepare_case_execution import collect_sdk_version_checks, load_repository_profile REPO_ROOT = Path(__file__).resolve().parents[3] @@ -20,6 +21,12 @@ "macos": "macOS/", "windows": "windows/", } +MAIN_PLATFORM_TARGETS = { + "android": "Android/APIExample/", + "ios": "iOS/APIExample/", + "macos": "macOS/", + "windows": "windows/", +} FINAL_STATUSES = {"PASS", "PASS WITH RISKS", "BLOCKED"} RESULT_STATUSES = {"PASS", "FAIL", "BLOCKED", "SKIPPED"} ROLE_STATUSES = {"PASS", "FAIL", "BLOCKED", "WAIVED"} @@ -35,7 +42,14 @@ "knowledge_updates", } REQUIREMENT_FIELDS = {"feature", "sdk_family", "key_apis", "target_sdk_versions"} -RELEASE_FIELDS = {"required", "target_sdk_versions", "checks", "skipped_checks"} +RELEASE_FIELDS = { + "required", + "repository_profile", + "repository_profile_sha256", + "target_sdk_versions", + "checks", + "skipped_checks", +} CONTRACT_OUTPUT_FIELDS = { "scenario", "key_apis", @@ -238,16 +252,17 @@ def validate_contract(manifest, errors, seen_agent_ids): f"contract.output.platform_targets.{platform}.files_allowed is required when Contract passes" ) target_project = target.get("target_project") + expected_target = MAIN_PLATFORM_TARGETS[platform] if is_non_empty(target_project): validate_paths( [target_project], f"contract.output.platform_targets.{platform}.target_project", errors, ) - if not path_is_within(target_project, PLATFORM_ROOTS[platform]): - errors.append( - f"contract.output.platform_targets.{platform}.target_project must be inside {PLATFORM_ROOTS[platform]}" - ) + if target_project != expected_target: + errors.append( + f"contract.output.platform_targets.{platform}.target_project must be exactly {expected_target}" + ) validate_paths( target.get("files_allowed", []), f"contract.output.platform_targets.{platform}.files_allowed", @@ -255,9 +270,10 @@ def validate_contract(manifest, errors, seen_agent_ids): ) allowed_paths = target.get("files_allowed", []) for allowed_path in allowed_paths if isinstance(allowed_paths, list) else []: - if not path_is_within(allowed_path, PLATFORM_ROOTS[platform]): + if not path_is_within(allowed_path, MAIN_PLATFORM_TARGETS[platform]): errors.append( - f"contract.output.platform_targets.{platform}.files_allowed must stay inside {PLATFORM_ROOTS[platform]}: {allowed_path}" + f"contract.output.platform_targets.{platform}.files_allowed must stay inside " + f"{MAIN_PLATFORM_TARGETS[platform]}: {allowed_path}" ) for platform in sorted(set(targets) - set(PLATFORMS)): errors.append(f"contract.output.platform_targets has unsupported platform: {platform}") @@ -514,10 +530,11 @@ def validate_platform_implementation(platform, target, output, verification, man def is_platform_build_command(platform, command, working_directory=None): if ( - platform not in PLATFORM_ROOTS + platform not in MAIN_PLATFORM_TARGETS or not isinstance(command, str) or not is_non_empty(working_directory) - or not path_is_within(working_directory, PLATFORM_ROOTS[platform]) + or normalize_manifest_path(working_directory).rstrip("/") + != normalize_manifest_path(MAIN_PLATFORM_TARGETS[platform]).rstrip("/") ): return False try: @@ -883,10 +900,34 @@ def validate_platform_verification( not command_log or not str(command.get("evidence")).startswith(f"{command_log}#") or "exit_code=" not in str(command.get("evidence")) + or "cwd=" not in str(command.get("evidence")) + or "cwd_source=" not in str(command.get("evidence")) ): errors.append( - f"platforms.{platform}.verification.output.commands[{index}].evidence must bind to dispatch.command_log and exit_code" + f"platforms.{platform}.verification.output.commands[{index}].evidence must bind " + "to dispatch.command_log, exit_code, cwd, and cwd_source" ) + elif command.get("result") in {"PASS", "FAIL"}: + evidence_fields = parse_command_evidence( + command.get("evidence"), command_log + ) + evidence_cwd = evidence_fields.get("cwd") + cwd_source = evidence_fields.get("cwd_source") + expected_cwd = dispatch.get("working_directory") + if ( + not evidence_cwd + or normalize_manifest_path(evidence_cwd).rstrip("/") + != normalize_manifest_path(expected_cwd).rstrip("/") + ): + errors.append( + f"platforms.{platform}.verification.output.commands[{index}].evidence cwd " + f"must match dispatch.working_directory {expected_cwd}" + ) + if cwd_source not in {"command_event", "dispatch"}: + errors.append( + f"platforms.{platform}.verification.output.commands[{index}].evidence " + "cwd_source must be command_event or dispatch" + ) for index, skipped in enumerate(output.get("skipped_checks", [])): if not isinstance(skipped, dict) or not skipped.get("reason"): errors.append( @@ -906,9 +947,25 @@ def validate_platform_verification( errors.append( f"platforms.{platform}.verification.output.build_result=PASS requires a bound PASS build command" ) - if platform == "windows" and output.get("build_result") == "PASS" and host_platform != "win32": + required_build_host = { + "ios": "darwin", + "macos": "darwin", + "windows": "win32", + }.get(platform) + has_pass_build_command = any( + isinstance(command, dict) + and command.get("kind") == "build" + and command.get("result") == "PASS" + for command in commands + ) + if ( + required_build_host + and (output.get("build_result") == "PASS" or has_pass_build_command) + and host_platform != required_build_host + ): errors.append( - "platforms.windows.verification.output.build_result=PASS requires host_platform=win32" + f"platforms.{platform}.verification.output.build_result=PASS requires " + f"host_platform={required_build_host}" ) if required and manifest.get("final_status") == "PASS" and output.get("skipped_checks"): errors.append("final_status=PASS cannot include skipped checks") @@ -945,6 +1002,15 @@ def validate_release(manifest, errors): errors.append("release.required must be true") for field in sorted(set(release) - RELEASE_FIELDS): errors.append(f"unsupported release field: {field}") + profile_value = release.get("repository_profile") + if not isinstance(profile_value, str) or not profile_value: + errors.append("release.repository_profile is required") + else: + profile_path = Path(profile_value) + if profile_path.is_absolute() or ".." in profile_path.parts: + errors.append("release.repository_profile must be repository-relative") + if not SHA256_RE.fullmatch(str(release.get("repository_profile_sha256") or "")): + errors.append("release.repository_profile_sha256 must be a SHA-256 hash") requirement = manifest.get("requirement", {}) target_sdk_versions = requirement.get("target_sdk_versions") release_target_sdk_versions = release.get("target_sdk_versions") @@ -1009,6 +1075,49 @@ def validate_release(manifest, errors): if manifest.get("final_status") != "BLOCKED" and release.get("skipped_checks"): errors.append("non-BLOCKED acceptance cannot include skipped release checks") + +def validate_repository_release(manifest, repo_root=REPO_ROOT): + errors = [] + release = manifest.get("release") + if not isinstance(release, dict): + return errors + profile_value = release.get("repository_profile") + profile_sha256 = release.get("repository_profile_sha256") + if not isinstance(profile_value, str) or not profile_value: + return errors + relative_path = Path(profile_value) + if relative_path.is_absolute() or ".." in relative_path.parts: + return errors + root = Path(repo_root).resolve() + profile_path = (root / relative_path).resolve() + try: + profile_path.relative_to(root) + except ValueError: + errors.append("release.repository_profile must resolve inside the repository") + return errors + if not profile_path.is_file(): + errors.append(f"release.repository_profile does not exist: {profile_value}") + return errors + actual_sha256 = hashlib.sha256(profile_path.read_bytes()).hexdigest() + if profile_sha256 != actual_sha256: + errors.append("release.repository_profile_sha256 does not match repository profile") + return errors + try: + load_repository_profile(profile_path) + expected_checks = collect_sdk_version_checks( + release.get("target_sdk_versions", {}), + repo_root=root, + profile_path=profile_path, + ) + except (OSError, ValueError, KeyError) as exc: + errors.append(f"failed to evaluate release.repository_profile: {exc}") + return errors + if release.get("checks") != expected_checks: + errors.append( + "release.checks must match live repository SDK checks from the bound repository profile" + ) + return errors + def validate_knowledge_updates(manifest, errors): updates = manifest.get("knowledge_updates", []) for index, update in enumerate(updates): @@ -1115,6 +1224,99 @@ def path_is_within(path_text, allowed_text): return path == allowed or path.startswith(f"{allowed}/") +def parse_command_evidence(evidence, command_log): + if not isinstance(evidence, str) or not isinstance(command_log, str): + return {} + prefix = f"{command_log}#" + if not evidence.startswith(prefix): + return {} + event_id, separator, details = evidence[len(prefix) :].partition("; ") + if not separator or not event_id: + return {} + fields = {"event_id": event_id} + for detail in details.split("; "): + key, equals, value = detail.partition("=") + if equals and key: + fields[key] = value + return fields + + +def extract_command_events(jsonl): + events = [] + for line in jsonl.splitlines(): + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + item = event.get("item") + if event.get("type") != "item.completed" or not isinstance(item, dict): + continue + if item.get("type") != "command_execution" or not isinstance(item.get("exit_code"), int): + continue + events.append( + { + "id": str(item.get("id") or f"command-{len(events)}"), + "command": item.get("command"), + "exit_code": item["exit_code"], + "cwd": item.get("cwd") or item.get("working_directory"), + } + ) + return events + + +def normalize_command(command): + if isinstance(command, list): + command = " ".join(str(part) for part in command) + command = str(command or "") + try: + tokens = shlex.split(command) + except ValueError: + return " ".join(command.split()) + if not tokens: + return "" + executable = tokens[0].replace("\\", "/").rsplit("/", 1)[-1].lower() + if executable in {"sh", "bash", "zsh"}: + command_option = next( + ( + index + for index, token in enumerate(tokens[:-1]) + if token.startswith("-") and "c" in token + ), + None, + ) + if command_option is not None: + command = tokens[command_option + 1] + elif executable in { + "cmd", + "cmd.exe", + "powershell", + "powershell.exe", + "pwsh", + "pwsh.exe", + }: + command_option = next( + ( + index + for index, token in enumerate(tokens[:-1]) + if token.lower() in {"/c", "-command", "-c"} + ), + None, + ) + if command_option is not None: + command = " ".join(tokens[command_option + 1 :]) + return " ".join(command.split()) + + +def command_event_cwd_matches(event_cwd, working_directory): + if not isinstance(event_cwd, str) or not event_cwd.strip(): + return False + normalized_cwd = event_cwd.replace("\\", "/").rstrip("/") + normalized_expected = normalize_manifest_path(working_directory).rstrip("/") + if normalized_cwd == normalized_expected: + return True + return normalized_cwd.endswith(f"/{normalized_expected}") + + def iter_manifest_artifacts(manifest): contract = manifest.get("contract") if isinstance(contract, dict): @@ -1183,6 +1385,10 @@ def validate_evidence_files(manifest, evidence_root): errors.append( f"{artifact_path}.dispatch.{hash_field} does not match {path_text}" ) + elif path_field == "command_log" and artifact_path.endswith(".verification"): + validate_command_log_evidence( + artifact_path, artifact, path, errors + ) elif path_field == "repository_delta": try: delta = json.loads(path.read_text(encoding="utf-8")) @@ -1210,6 +1416,78 @@ def validate_evidence_files(manifest, evidence_root): return errors +def validate_command_log_evidence(artifact_path, artifact, log_path, errors): + try: + events = extract_command_events(log_path.read_text(encoding="utf-8")) + except OSError as exc: + errors.append(f"{artifact_path}.dispatch.command_log cannot be read: {exc}") + return + dispatch = artifact.get("dispatch", {}) + command_log = dispatch.get("command_log") + working_directory = dispatch.get("working_directory") + output = artifact.get("output", {}) + commands = output.get("commands", []) if isinstance(output, dict) else [] + used = set() + for index, command in enumerate(commands): + if not isinstance(command, dict) or command.get("result") not in {"PASS", "FAIL"}: + continue + field_name = f"{artifact_path}.output.commands[{index}].evidence" + fields = parse_command_evidence(command.get("evidence"), command_log) + if not fields: + errors.append(f"{field_name} is not valid bound command evidence") + continue + candidates = [ + event_index + for event_index, event in enumerate(events) + if event_index not in used + and event["id"] == fields.get("event_id") + and normalize_command(event["command"]) + == normalize_command(command.get("command")) + ] + if not candidates: + errors.append(f"{field_name} has no matching command_execution event") + continue + match_index = next( + ( + event_index + for event_index in candidates + if events[event_index].get("cwd") + and command_event_cwd_matches( + events[event_index]["cwd"], working_directory + ) + ), + None, + ) + if match_index is None: + match_index = next( + ( + event_index + for event_index in candidates + if not events[event_index].get("cwd") + ), + None, + ) + if match_index is None: + errors.append( + f"{field_name} command_execution cwd must match " + f"dispatch.working_directory {working_directory}" + ) + continue + event = events[match_index] + used.add(match_index) + expected_cwd_source = "command_event" if event.get("cwd") else "dispatch" + if fields.get("cwd_source") != expected_cwd_source: + errors.append( + f"{field_name} cwd_source must be {expected_cwd_source} for its " + "command_execution event" + ) + if fields.get("exit_code") != str(event["exit_code"]): + errors.append(f"{field_name} exit_code does not match command_execution event") + actual_result = "PASS" if event["exit_code"] == 0 else "FAIL" + if command.get("result") != actual_result: + errors.append(f"{field_name} result does not match command_execution exit_code") + + def main(argv): if len(argv) != 2: print("usage: validate_acceptance_manifest.py ", file=sys.stderr) @@ -1224,6 +1502,7 @@ def main(argv): print(f"invalid JSON: {exc}", file=sys.stderr) return 2 errors = validate_manifest(manifest) + errors.extend(validate_repository_release(manifest)) errors.extend(validate_evidence_files(manifest, manifest_path.parent)) if errors: for error in errors: diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py index 5c5f671d5..678abf118 100644 --- a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py +++ b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py @@ -13,11 +13,15 @@ from validate_acceptance_manifest import ( is_durable_knowledge_path, + validate_command_log_evidence, validate_evidence_files, validate_manifest, + validate_repository_release, ) +from prepare_case_execution import collect_sdk_version_checks +REPO_ROOT = Path(__file__).resolve().parents[3] PLATFORMS = ["android", "ios", "macos", "windows"] TARGETS = { "android": "Android/APIExample/", @@ -112,7 +116,8 @@ def base_manifest(): "command": BUILD_COMMANDS[platform], "result": "PASS", "evidence": ( - f"dispatch-logs/{platform}-verification.jsonl#build; exit_code=0" + f"dispatch-logs/{platform}-verification.jsonl#build; " + f"exit_code=0; cwd={target}; cwd_source=command_event" ), } ], @@ -154,6 +159,10 @@ def base_manifest(): }, "release": { "required": True, + "repository_profile": "docs/ai-engineering/repository-profile.json", + "repository_profile_sha256": hashlib.sha256( + (REPO_ROOT / "docs/ai-engineering/repository-profile.json").read_bytes() + ).hexdigest(), "target_sdk_versions": {platform: "4.6.4" for platform in PLATFORMS}, "checks": [ { @@ -506,6 +515,72 @@ def test_release_sdk_check_requires_all_sources_to_match_target(self): self.assert_error_contains(manifest, "sdk-version-android actual versions must all match 4.6.4") + def test_blocked_external_sdk_check_is_valid_only_for_blocked_acceptance(self): + manifest = base_manifest() + manifest["final_status"] = "BLOCKED" + ios = next( + check + for check in manifest["release"]["checks"] + if check["name"] == "sdk-version-ios" + ) + ios.update( + { + "result": "BLOCKED", + "actual_versions": {"iOS/APIExample/Podfile": ""}, + "evidence": "", + "reason": "The RTC SDK is injected outside tracked repository files.", + } + ) + + self.assertEqual(validate_manifest(manifest), []) + + for final_status in ["PASS", "PASS WITH RISKS"]: + with self.subTest(final_status=final_status): + manifest["final_status"] = final_status + self.assert_error_contains( + manifest, + "non-BLOCKED acceptance requires sdk-version-ios=PASS", + ) + + def test_repository_release_rejects_forged_external_sdk_pass(self): + manifest = base_manifest() + targets = {platform: "4.7.0" for platform in PLATFORMS} + manifest["requirement"]["target_sdk_versions"] = targets + manifest["release"]["target_sdk_versions"] = targets + manifest["release"]["checks"] = collect_sdk_version_checks(targets) + ios = next( + check + for check in manifest["release"]["checks"] + if check["name"] == "sdk-version-ios" + ) + self.assertEqual(ios["result"], "BLOCKED") + ios.update( + { + "result": "PASS", + "actual_versions": {"iOS/APIExample/Podfile": "4.7.0"}, + "evidence": "Forged repository evidence.", + "reason": "", + } + ) + + errors = validate_repository_release(manifest) + + self.assertIn( + "release.checks must match live repository SDK checks from the bound repository profile", + errors, + ) + + def test_repository_release_rejects_profile_hash_drift(self): + manifest = base_manifest() + manifest["release"]["repository_profile_sha256"] = "0" * 64 + + errors = validate_repository_release(manifest) + + self.assertIn( + "release.repository_profile_sha256 does not match repository profile", + errors, + ) + def test_rejects_external_ci_and_qa_metadata(self): manifest = base_manifest() manifest["release"]["qa_acceptance"] = {} @@ -617,6 +692,54 @@ def test_windows_build_pass_requires_windows_host_provenance(self): "platforms.windows.verification.output.build_result=PASS requires host_platform=win32", ) + def test_apple_build_pass_requires_darwin_host_provenance(self): + for platform in ["ios", "macos"]: + with self.subTest(platform=platform): + manifest = base_manifest() + manifest["platforms"][platform]["verification"]["dispatch"][ + "host_platform" + ] = "linux" + + self.assert_error_contains( + manifest, + f"platforms.{platform}.verification.output.build_result=PASS requires host_platform=darwin", + ) + + def test_rejects_non_main_project_target_inside_platform_root(self): + manifest = base_manifest() + target = manifest["contract"]["output"]["platform_targets"]["ios"] + target["target_project"] = "iOS/APIExample-OC/" + target["files_allowed"] = ["iOS/APIExample-OC/"] + + self.assert_error_contains( + manifest, + "contract.output.platform_targets.ios.target_project must be exactly iOS/APIExample/", + ) + + def test_rejects_files_allowed_in_sibling_project(self): + manifest = base_manifest() + target = manifest["contract"]["output"]["platform_targets"]["ios"] + target["files_allowed"] = ["iOS/APIExample-OC/"] + + self.assert_error_contains( + manifest, + "contract.output.platform_targets.ios.files_allowed must stay inside iOS/APIExample/", + ) + + def test_command_evidence_cwd_must_match_dispatch_working_directory(self): + manifest = base_manifest() + command = manifest["platforms"]["ios"]["verification"]["output"]["commands"][0] + command["evidence"] = ( + "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " + "cwd=iOS/APIExample-Audio/; cwd_source=command_event" + ) + + self.assert_error_contains( + manifest, + "platforms.ios.verification.output.commands[0].evidence cwd must match " + "dispatch.working_directory iOS/APIExample/", + ) + def test_rejects_platform_target_outside_its_platform_root(self): manifest = base_manifest() target = manifest["contract"]["output"]["platform_targets"]["android"] @@ -625,7 +748,7 @@ def test_rejects_platform_target_outside_its_platform_root(self): self.assert_error_contains( manifest, - "contract.output.platform_targets.android.target_project must be inside Android/", + "contract.output.platform_targets.android.target_project must be exactly Android/APIExample/", ) def test_rejects_platform_target_that_escapes_root_with_parent_segments(self): @@ -636,7 +759,7 @@ def test_rejects_platform_target_that_escapes_root_with_parent_segments(self): self.assert_error_contains( manifest, - "contract.output.platform_targets.android.target_project must be inside Android/", + "contract.output.platform_targets.android.target_project must be exactly Android/APIExample/", ) def test_rejects_dispatch_path_outside_workspace(self): @@ -712,6 +835,23 @@ def test_evidence_files_are_rehashed_from_manifest_directory(self): content = json.dumps( {"changed_files": item["output"]["files_changed"]} ).encode() + elif path_field == "command_log" and "commands" in item.get("output", {}): + command = item["output"]["commands"][0] + content = ( + json.dumps( + { + "type": "item.completed", + "item": { + "id": "build", + "type": "command_execution", + "command": command["command"], + "exit_code": 0, + "cwd": item["dispatch"]["working_directory"], + }, + } + ) + + "\n" + ).encode() else: content = f"{index}:{path_field}\n".encode() path.write_bytes(content) @@ -724,6 +864,75 @@ def test_evidence_files_are_rehashed_from_manifest_directory(self): errors = validate_evidence_files(manifest, root) self.assertTrue(any("contract.dispatch.prompt_sha256 does not match" in error for error in errors)) + def test_evidence_validation_rejects_command_event_from_wrong_cwd(self): + manifest = base_manifest() + verification = manifest["platforms"]["ios"]["verification"] + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + log_path = root / verification["dispatch"]["command_log"] + log_path.parent.mkdir(parents=True) + command = verification["output"]["commands"][0] + content = ( + json.dumps( + { + "type": "item.completed", + "item": { + "id": "build", + "type": "command_execution", + "command": f"/bin/zsh -lc '{command['command']}'", + "exit_code": 0, + "cwd": "iOS/APIExample-Audio/", + }, + } + ) + + "\n" + ) + log_path.write_text(content, encoding="utf-8") + verification["dispatch"]["command_log_sha256"] = hashlib.sha256( + content.encode() + ).hexdigest() + + errors = validate_evidence_files(manifest, root) + + self.assertTrue( + any("command_execution cwd must match" in error for error in errors), + errors, + ) + + def test_evidence_validation_accepts_dispatch_cwd_when_event_omits_it(self): + manifest = base_manifest() + verification = manifest["platforms"]["ios"]["verification"] + command = verification["output"]["commands"][0] + command["evidence"] = ( + "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " + "cwd=iOS/APIExample/; cwd_source=dispatch" + ) + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + log_path = root / verification["dispatch"]["command_log"] + log_path.parent.mkdir(parents=True) + content = ( + json.dumps( + { + "type": "item.completed", + "item": { + "id": "build", + "type": "command_execution", + "command": f"/bin/zsh -lc '{command['command']}'", + "exit_code": 0, + }, + } + ) + + "\n" + ) + log_path.write_text(content, encoding="utf-8") + errors = [] + validate_command_log_evidence( + "platforms.ios.verification", verification, log_path, errors + ) + + self.assertEqual(errors, []) + def test_evidence_validation_rejects_delta_content_mismatch(self): manifest = base_manifest() implementation = manifest["platforms"]["android"]["implementation"] diff --git a/macOS/.agents/skills/query-cases/SKILL.md b/macOS/.agents/skills/query-cases/SKILL.md new file mode 100644 index 000000000..9c1fa3d12 --- /dev/null +++ b/macOS/.agents/skills/query-cases/SKILL.md @@ -0,0 +1,50 @@ +--- +name: query-cases +description: > + Query existing macOS APIExample cases by feature name, Agora RTC API, category, + menu label, controller identifier, or storyboard. Use before adding a case or + when checking whether a feature is already implemented and fully registered. + This skill is read-only and verifies live menu and storyboard wiring instead of + relying only on folder names or the architecture index. +--- + +# Query Cases - macOS + +## Scope + +Use this skill only to inspect the main macOS `APIExample` project. It answers which +cases exist, which SDK APIs they demonstrate, where their source lives, and whether +they are reachable from the application menu. It must not edit or register cases. + +## Sources Of Truth + +1. `ARCHITECTURE.md` for the fast case index +2. `APIExample/ViewController.swift` for active `MenuItem` registrations +3. `APIExample/Examples/Basic/` and `APIExample/Examples/Advanced/` for implementation and API usage +4. Per-case `Base.lproj/*.storyboard` files for storyboard names and controller identifiers +5. `APIExample.xcodeproj/project.pbxproj` for target membership when registration completeness matters + +## Procedure + +1. Search the Case Index in `ARCHITECTURE.md` by case name, feature, or exact SDK API. +2. Re-scan the `menus` array in `APIExample/ViewController.swift` before claiming that a case is registered. Ignore commented-out `MenuItem` lines and header cells. +3. For API queries not answered by the index, search Swift sources under both example categories and report the actual call site. +4. For a fully registered case, verify all of the following: + - an active menu item supplies a display name, `controller`, and `storyboard`; + - the named storyboard exists under the case folder; + - the storyboard contains the registered controller identifier; + - the Swift source and storyboard belong to the `APIExample` Xcode target. +5. If documentation, source, menu, or storyboard evidence disagree, report the discrepancy instead of selecting one silently. + +## Output + +Report the case name, category, source path, key APIs, and registration status. For +partial or missing registration, identify the exact missing link. Keep "source +exists" distinct from "case is reachable in the app." + +## Never + +- Never count a commented-out menu item as active. +- Never infer registration from a folder or storyboard alone. +- Never claim an API is demonstrated without locating its source call or an up-to-date Case Index entry. +- Never edit source, storyboards, project files, or documentation while using this skill. diff --git a/macOS/AGENTS.md b/macOS/AGENTS.md index 9de63e1de..e12785572 100644 --- a/macOS/AGENTS.md +++ b/macOS/AGENTS.md @@ -65,5 +65,6 @@ For broader tasks, use the skills in `.agents/skills/`: | Task | Skill | When to use | |------|-------|-------------| +| Find an existing example | `.agents/skills/query-cases/` | Need to locate a case by feature or API and verify its menu/storyboard registration | | Add or modify an example | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | | Code review | `.agents/skills/review-case/` | Review example code for lifecycle, thread safety, and convention compliance | diff --git a/macOS/ARCHITECTURE.md b/macOS/ARCHITECTURE.md index ad24858be..230634b4d 100644 --- a/macOS/ARCHITECTURE.md +++ b/macOS/ARCHITECTURE.md @@ -34,10 +34,9 @@ macOS/ ├── APIExample.xcworkspace/ # Xcode workspace ├── libs/ # SDK libraries ├── Pods/ # CocoaPods dependencies -├── .agents/skills/ # Agent skills -│ ├── create-api-example/ -│ ├── find-api-example/ -│ └── migrate-api-to-project/ +├── .agents/skills/query-cases/ # Read-only case and registration discovery +├── .agents/skills/upsert-case/ # Add or modify a case +├── .agents/skills/review-case/ # Review lifecycle, threading, and conventions ├── AGENTS.md # Agent guide └── ARCHITECTURE.md # This file ``` diff --git a/windows/.agents/skills/query-cases/SKILL.md b/windows/.agents/skills/query-cases/SKILL.md new file mode 100644 index 000000000..7c88fc7e4 --- /dev/null +++ b/windows/.agents/skills/query-cases/SKILL.md @@ -0,0 +1,54 @@ +--- +name: query-cases +description: > + Query existing Windows APIExample cases by feature name, Agora RTC API, category, + scene label, or dialog class. Use before adding a case or when checking whether a + feature is already implemented and fully registered. This skill is read-only and + verifies the live MFC scene list, dialog routing, project membership, and localized + labels instead of relying only on folder names or the architecture index. +--- + +# Query Cases - Windows + +## Scope + +Use this skill only to inspect the main Windows `APIExample` solution. It answers +which cases exist, which SDK APIs they demonstrate, where their dialog sources live, +and whether they are reachable from the MFC scene list. It must not edit or register +cases. + +## Sources Of Truth + +1. `ARCHITECTURE.md` for the fast case index +2. `APIExample/APIExample/APIExampleDlg.h` and `APIExample/APIExample/APIExampleDlg.cpp` for scene ownership and active routing +3. `APIExample/APIExample/Basic/` and `APIExample/APIExample/Advanced/` for implementation and API usage +4. `APIExample/APIExample/Language.h`, `APIExample/APIExample/stdafx.cpp`, `APIExample/APIExample/en.ini`, and `APIExample/APIExample/zh-cn.ini` for scene labels +5. `APIExample/APIExample/APIExample.vcxproj` for compiled source and header membership + +## Procedure + +1. Search the Case Index in `ARCHITECTURE.md` by case name, feature, or exact SDK API. +2. Re-scan active `m_vecBasic.push_back(...)` and `m_vecAdvanced.push_back(...)` calls in `InitSceneDialog()` before claiming that a case appears in the application. Ignore commented-out calls. +3. For API queries not answered by the index, search the matching dialog sources under both case categories and report the actual call site. +4. For a fully registered case, verify all of the following: + - the dialog header is included and the dialog is owned by `CAPIExampleDlg`; + - an active scene vector entry exists and its dialog is created; + - `CreateScene()` routes the same scene label to the dialog's initialize/show path; + - `ReleaseScene()` routes it to the cleanup/hide path; + - the label is declared in `Language.h`, initialized from a key in `stdafx.cpp`, and present in both language files; + - the dialog sources belong to `APIExample.vcxproj`. +5. If documentation, source, scene-list, routing, project, or localization evidence disagree, report the discrepancy instead of selecting one silently. + +## Output + +Report the case name, category, source files, key APIs, and registration status. For +partial or missing registration, identify the exact missing link. Keep "dialog +source exists" distinct from "case is selectable and safely released in the app." + +## Never + +- Never count a commented-out scene vector entry as active. +- Never use `CSceneDialog.cpp` as the registration source; this project registers cases in `APIExampleDlg`. +- Never infer registration from a folder, include, or constructed dialog alone. +- Never claim an API is demonstrated without locating its source call or an up-to-date Case Index entry. +- Never edit source, resources, project files, or documentation while using this skill. diff --git a/windows/AGENTS.md b/windows/AGENTS.md index 0f301373a..a9c728ab4 100644 --- a/windows/AGENTS.md +++ b/windows/AGENTS.md @@ -66,5 +66,6 @@ For broader tasks, use the skills in `.agents/skills/`: | Task | Skill | When to use | |------|-------|-------------| +| Find an existing example | `.agents/skills/query-cases/` | Need to locate a case by feature or API and verify its scene/dialog registration | | Add or modify an example | `.agents/skills/upsert-case/` | Need to create a new API demo or update an existing one | | Code review | `.agents/skills/review-case/` | Review example code for lifecycle, thread safety, and convention compliance | diff --git a/windows/ARCHITECTURE.md b/windows/ARCHITECTURE.md index 9492b4d2b..53cdd2212 100644 --- a/windows/ARCHITECTURE.md +++ b/windows/ARCHITECTURE.md @@ -39,10 +39,9 @@ windows/ │ ├── APIExample.sln # Visual Studio solution │ ├── cicd/ # CI/CD scripts │ └── .vscode/ # VS Code configuration -├── .agents/skills/ # Agent skills -│ ├── create-api-example/ -│ ├── find-api-example/ -│ └── migrate-api-to-project/ +├── .agents/skills/query-cases/ # Read-only case and registration discovery +├── .agents/skills/upsert-case/ # Add or modify a case +├── .agents/skills/review-case/ # Review lifecycle, threading, and conventions ├── AGENTS.md # Agent guide └── ARCHITECTURE.md # This file ``` From fcb79cc15d98ad3691eff118ca83682cd7b2f106 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Mon, 7 Sep 2026 11:06:45 +0800 Subject: [PATCH 33/49] fix: refine RTC 4.7.0 example compatibility --- .github/workflows/mobile-ci.yml | 47 +++++++ .../advanced/beauty/AgoraBeautySDK.kt | 1 - .../faceunity/FaceUnityBeautyAPIImpl.kt | 8 +- .../faceunity/utils/egl/GLFrameBuffer.java | 6 +- .../sensetime/utils/egl/GLFrameBuffer.java | 6 +- .../APIExample.xcodeproj/project.pbxproj | 10 +- .../Advanced/AgoraBeauty/AgoraBeauty.swift | 20 ++- .../AgoraBeauty/AgoraBeautyManager.swift | 133 ++++++++++++++---- .../zh-Hans.lproj/Localizable.strings | 4 + macOS/APIExample.xcodeproj/project.pbxproj | 22 ++- 10 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/mobile-ci.yml diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml new file mode 100644 index 000000000..febbc32db --- /dev/null +++ b/.github/workflows/mobile-ci.yml @@ -0,0 +1,47 @@ +name: Mobile CI + +on: + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mobile-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + android-static: + name: Android Static Checks + runs-on: ubuntu-latest + defaults: + run: + working-directory: Android/APIExample + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + cache: gradle + cache-dependency-path: Android/APIExample/**/*.gradle* + - uses: android-actions/setup-android@v3 + - name: Validate Gradle configuration + run: ./gradlew :app:tasks --all + - name: Validate Android XML resources + run: python3 -c 'import pathlib, xml.etree.ElementTree as ET; [ET.parse(path) for path in pathlib.Path("app/src/main").rglob("*.xml")]' + + ios-static: + name: iOS Static Checks + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Validate Xcode projects + run: plutil -lint iOS/APIExample/APIExample.xcodeproj/project.pbxproj macOS/APIExample.xcodeproj/project.pbxproj + - name: Parse Swift sources + run: find iOS/APIExample/APIExample macOS/APIExample -name '*.swift' ! -name 'KeyCenter.swift' -print0 | xargs -0 xcrun swiftc -frontend -parse + - name: Validate localizations + run: find iOS/APIExample/APIExample macOS/APIExample -name '*.strings' -print0 | xargs -0 plutil -lint diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/AgoraBeautySDK.kt b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/AgoraBeautySDK.kt index cd6894f55..c800aae17 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/AgoraBeautySDK.kt +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/AgoraBeautySDK.kt @@ -169,7 +169,6 @@ object AgoraBeautySDK { set(value) { field = value val effectObj = videoEffectObject ?: return - effectObj.setVideoEffectStringParam("beauty_effect_option", "whiten_lut_path", "") effectObj.setVideoEffectFloatParam("beauty_effect_option", "lightness", value) } diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt index bb3b019e4..ac492705c 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt @@ -42,7 +42,7 @@ import io.agora.base.VideoFrame import io.agora.base.VideoFrame.I420Buffer import io.agora.base.VideoFrame.SourceType import io.agora.base.VideoFrame.TextureBuffer -import io.agora.base.internal.video.EglBaseLock +import io.agora.base.internal.video.EglBase import io.agora.base.internal.video.TextureBufferPool import io.agora.base.internal.video.YuvHelper import io.agora.beautyapi.faceunity.utils.APIReporter @@ -616,7 +616,7 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { } val ret = texBufferHelper.invoke { var result = -1 - EglBaseLock.lock { + synchronized(EglBase.lock) { result = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } result @@ -730,7 +730,7 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { return@invoke -1 } var fuTexId = -1 - EglBaseLock.lock { + synchronized(EglBase.lock) { fuTexId = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } outGLFrameBuffer.setSize(videoFrame.rotatedWidth, videoFrame.rotatedHeight) @@ -853,7 +853,7 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { } var fuTexId = -1 - EglBaseLock.lock { + synchronized(EglBase.lock) { fuTexId = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } outGLFrameBuffer.setSize(videoFrame.rotatedWidth, videoFrame.rotatedHeight) diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java index 8d8c013f2..79ecc3856 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java @@ -4,7 +4,7 @@ import android.opengl.GLES11Ext; import android.opengl.GLES20; -import io.agora.base.internal.video.EglBaseLock; +import io.agora.base.internal.video.EglBase; import io.agora.base.internal.video.GlRectDrawer; import io.agora.base.internal.video.RendererCommon; @@ -113,13 +113,13 @@ public int process(int textureId, int textureType) { transform.preTranslate(-0.5f, -0.5f); float[] matrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(transform); - EglBaseLock.lock(() -> { + synchronized (EglBase.lock) { if(textureType == GLES11Ext.GL_TEXTURE_EXTERNAL_OES){ drawer.drawOes(textureId, 0,matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); }else{ drawer.drawRgb(textureId, 0,matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); } - }); + } GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE); GLES20.glFinish(); diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java index 4d8cf377f..f36cbd5b6 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java @@ -4,7 +4,7 @@ import android.opengl.GLES11Ext; import android.opengl.GLES20; -import io.agora.base.internal.video.EglBaseLock; +import io.agora.base.internal.video.EglBase; import io.agora.base.internal.video.GlRectDrawer; import io.agora.base.internal.video.RendererCommon; @@ -113,13 +113,13 @@ public int process(int textureId, int textureType) { transform.preTranslate(-0.5f, -0.5f); float[] matrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(transform); - EglBaseLock.lock(() -> { + synchronized (EglBase.lock) { if (textureType == GLES11Ext.GL_TEXTURE_EXTERNAL_OES) { drawer.drawOes(textureId, 0, matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); } else { drawer.drawRgb(textureId, 0, matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); } - }); + } GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glFinish(); diff --git a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index cfb7bde61..9f8cbbf7f 100644 --- a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj +++ b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj @@ -204,7 +204,6 @@ F728B9DC2CA295D7007813BB /* PixelBufferPIPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D32CA295D7007813BB /* PixelBufferPIPService.swift */; }; F728B9DD2CA295D7007813BB /* PixelBufferPIPViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D42CA295D7007813BB /* PixelBufferPIPViewController.swift */; }; F728B9DE2CA295D7007813BB /* PixelBufferRenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F728B9D52CA295D7007813BB /* PixelBufferRenderView.swift */; }; - F734791C2DF5C9CD0030C0CE /* AgoraBeautyMaterial.bundle in Resources */ = {isa = PBXBuildFile; fileRef = F734791B2DF5C9CD0030C0CE /* AgoraBeautyMaterial.bundle */; }; F734791E2DF5CA0D0030C0CE /* AgoraBeauty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F734791D2DF5CA0D0030C0CE /* AgoraBeauty.swift */; }; F73479242DF5CA820030C0CE /* AgoraBeauty.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73479222DF5CA820030C0CE /* AgoraBeauty.storyboard */; }; F73479262DF5CAEB0030C0CE /* AgoraBeautyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73479252DF5CAEB0030C0CE /* AgoraBeautyManager.swift */; }; @@ -1490,7 +1489,7 @@ 03D13BCA2448758900B599B3 /* Resources */, 1B6F6CF9B678035E221EAFDE /* [CP] Embed Pods Frameworks */, 0339BEBA25205B80007D4FDD /* Embed App Extensions */, - E76F80122AF0A7A200CCB9D6 /* ShellScript */, + E76F80122AF0A7A200CCB9D6 /* Copy Optional Beauty Resources */, DB2429109D6D95174925E0A2 /* [CP] Copy Pods Resources */, ); buildRules = ( @@ -1625,7 +1624,6 @@ DD6ED7F12CB69FFB007B3B5D /* LocalCompositeGraph.storyboard in Resources */, 67CB2F0D27EB318200CB19D2 /* SpatialAudio.storyboard in Resources */, 033A9F4D252D89DB00BC26E1 /* CustomAudioSource.storyboard in Resources */, - F734791C2DF5C9CD0030C0CE /* AgoraBeautyMaterial.bundle in Resources */, E7A49CB12900FC6400F06DD4 /* KtvCopyrightMusic.storyboard in Resources */, E7A49CFD29029E0000F06DD4 /* FUBeautify.storyboard in Resources */, 8B5E5B5B274CEFE20040E97D /* RhythmPlayer.storyboard in Resources */, @@ -1757,8 +1755,9 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - E76F80122AF0A7A200CCB9D6 /* ShellScript */ = { + E76F80122AF0A7A200CCB9D6 /* Copy Optional Beauty Resources */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1766,13 +1765,14 @@ ); inputPaths = ( ); + name = "Copy Optional Beauty Resources"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = ""; + shellScript = "set -eu\n\nSOURCE=\"${PROJECT_DIR:?}/APIExample/Resources/AgoraBeautyMaterial.bundle\"\nDESTINATION=\"${TARGET_BUILD_DIR:?}/${UNLOCALIZED_RESOURCES_FOLDER_PATH:?}/AgoraBeautyMaterial.bundle\"\n\n/bin/rm -rf \"${DESTINATION}\"\nif [ -d \"${SOURCE}\" ]; then\n /usr/bin/ditto \"${SOURCE}\" \"${DESTINATION}\"\nelse\n echo \"warning: AgoraBeautyMaterial.bundle is unavailable. Internal builds can provide it by running cloud_build.sh with JFROG_API_KEY.\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift index 5a69dd74c..45c8364eb 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift @@ -416,9 +416,13 @@ extension AgoraBeautyMain { switchView.addTarget(self, action: #selector(makeupSwitchAction(_:)), for: .valueChanged) valueView = switchView } else if type == "segment" { - let value = makeupList[i]["value"] as? [String] ?? [] + let value = makeupList[i]["title"] as? [String] ?? [] let segmentView = UISegmentedControl(items: value) - segmentView.selectedSegmentIndex = Int(getDefaultSegmentValueForKey(key: key)) + let defaultValue = Int(getDefaultSegmentValueForKey(key: key)) + let values = makeupList[i]["value"] as? [Int] ?? [] + let defaultIndex = values.firstIndex(of: defaultValue) ?? 0 + + segmentView.selectedSegmentIndex = defaultIndex segmentView.addTarget(self, action: #selector(makeupSegmentAction(_:)), for: .valueChanged) valueView = segmentView } @@ -470,6 +474,8 @@ extension AgoraBeautyMain { return beautyManager.lashStrength } else if key == "browStrength" { return beautyManager.browStrength + } else if key == "wocanStrength" { + return beautyManager.wocanStrength } return 0 } @@ -497,6 +503,8 @@ extension AgoraBeautyMain { return beautyManager.lipStyle } else if key == "lipColor" { return beautyManager.lipColor + } else if key == "wocanStyle" { + return beautyManager.wocanStyle } return 0 } @@ -525,6 +533,8 @@ extension AgoraBeautyMain { beautyManager.lashStrength = value } else if key == "browStrength" { beautyManager.browStrength = value + } else if key == "wocanStrength" { + beautyManager.wocanStrength = value } } @@ -544,7 +554,9 @@ extension AgoraBeautyMain { let index = view.tag - 1000 let makeupList = beautyManager.makeupList let key = makeupList[index]["key"] as? String ?? "" - let value = Int32(view.selectedSegmentIndex) + let values = makeupList[index]["value"] as? [Int] ?? [] + guard values.indices.contains(view.selectedSegmentIndex) else { return } + let value = Int32(values[view.selectedSegmentIndex]) makeupParams[key] = value if key == "pupilStyle" { beautyManager.pupilStyle = value @@ -568,6 +580,8 @@ extension AgoraBeautyMain { beautyManager.lipStyle = value } else if key == "lipColor" { beautyManager.lipColor = value + } else if key == "wocanStyle" { + beautyManager.wocanStyle = value } } diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift index 25f2ff871..83f4569ef 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift @@ -637,13 +637,25 @@ class AgoraBeautyManager { "name": "Eyebrow Style".localized, "key": "browStyle", "type": "segment", - "value": ["Eyebrow Style Off".localized, "Eyebrow Style Type 1".localized, "Eyebrow Style Type 2".localized] - ], [ + "title": ["Eyebrow Style Off".localized, + "Eyebrow Style Type 1".localized, + "Eyebrow Style Type 2".localized, + "Eyebrow Style Type 3".localized], + "value": [0, + 1, + 2, + 3] + ],[ "name": "Eyebrow Color".localized, "key": "browColor", "type": "segment", - "value": ["Eyebrow Color None".localized, "Eyebrow Color Black".localized, "Eyebrow Color Brown".localized] - ], [ + "title": [ + "Eyebrow Color None".localized, "Eyebrow Color Black".localized, "Eyebrow Color Brown".localized + ], + "value": [0, + 1, + 2] + ], [ "name": "Eyebrow Strength".localized, "key": "browStrength", "type": "slider", @@ -652,15 +664,23 @@ class AgoraBeautyManager { "name": "Eyelash Style".localized, "key": "lashStyle", "type": "segment", - "value": ["Eyebrow Style Off".localized, + "title": ["Eyebrow Style Off".localized, "Eyebrow Style Type 1".localized, - "Eyebrow Style Type 2".localized + "Eyebrow Style Type 2".localized], + "value": [0, + 3, + 5 ] ], [ "name": "Eyelash Color".localized, "key": "lashColor", "type": "segment", - "value": ["Eyebrow Color None".localized, "Eyebrow Color Black".localized, "Eyebrow Color Brown".localized] + "title": [ + "Eyebrow Color None".localized, "Eyebrow Color Black".localized, "Eyebrow Color Brown".localized + ], + "value": [0, + 1, + 2] ], [ "name": "Eyelash Strength".localized, "key": "lashStrength", @@ -670,39 +690,54 @@ class AgoraBeautyManager { "name": "Eyeshadow Style".localized, "key": "shadowStyle", "type": "segment", - "value": ["Eyebrow Style Off".localized, - "Eyebrow Style Type 1".localized, - "Eyebrow Style Type 2".localized - ] + "title": [ + "Eyebrow Style Off".localized, + "Eyebrow Style Type 1".localized, + "Eyebrow Style Type 2".localized + ], + "value": [0, + 1, + 6] ], [ "name": "Eyeshadow Strength".localized, "key": "shadowStrength", "type": "slider", "value": [0, 1] - ], [ - "name": "Pupil Style".localized, - "key": "pupilStyle", - "type": "segment", - "value": ["Eyebrow Style Off".localized, "Eyebrow Style Type 1".localized, "Eyebrow Style Type 2".localized] - ], [ - "name": "Pupil Strength".localized, - "key": "pupilStrength", - "type": "slider", - "value": [0, 1] ], [ "name": "Blush Style".localized, "key": "blushStyle", "type": "segment", - "value": ["Eyebrow Style Off".localized, "Eyebrow Style Type 1".localized, "Eyebrow Style Type 2".localized] + "title": ["Eyebrow Style Off".localized, + "Eyebrow Style Type 1".localized, + "Eyebrow Style Type 2".localized, + "Eyebrow Style Type 3".localized, + "Eyebrow Style Type 4".localized], + "value": [ + 0, + 1, + 2, + 4, + 9 + ] ], [ "name": "Blush Color".localized, "key": "blushColor", "type": "segment", - "value": [ + "title": [ "Blush Color None".localized, "Blush Color Shade 1".localized, "Blush Color Shade 2".localized, - "Blush Color Shade 3".localized + "Blush Color Shade 3".localized, + "Blush Color Shade 4".localized, + "Blush Color Shade 5".localized + ], + "value": [ + 0, + 1, + 2, + 3, + 4, + 5 ] ], [ "name": "Blush Strength".localized, @@ -713,22 +748,66 @@ class AgoraBeautyManager { "name": "Lip Style".localized, "key": "lipStyle", "type": "segment", - "value": ["Eyebrow Color None".localized, "Eyebrow Color Black".localized, "Eyebrow Color Brown".localized] + "title": [ + "Eyebrow Style Off".localized, + "Eyebrow Style Type 1".localized, + "Eyebrow Style Type 2".localized, + "Eyebrow Style Type 3".localized, + "Eyebrow Style Type 4".localized, + ], + "value": [0, + 1, + 2, + 3, + 6] ], [ "name": "Lip Color".localized, "key": "lipColor", "type": "segment", - "value": [ + "title": [ "Blush Color None".localized, "Blush Color Shade 1".localized, "Blush Color Shade 2".localized, - "Blush Color Shade 3".localized + "Blush Color Shade 3".localized, + "Blush Color Shade 4".localized, + "Blush Color Shade 5".localized + ], + "value": [ + 0, + 1, + 2, + 3, + 4, + 5 ] ], [ "name": "Lip Strength".localized, "key": "lipStrength", "type": "slider", "value": [0, 1] + ], [ + "name": "Wocan".localized, + "key": "wocanStyle", + "type": "segment", + "title": [ + "Eyebrow Style Off".localized, + "Eyebrow Style Type 1".localized, + "Eyebrow Style Type 2".localized, + "Eyebrow Style Type 3".localized, + "Eyebrow Style Type 4".localized, + ], + "value": [ + 0, + 1, + 2, + 3, + 4 + ] + ], [ + "name": "Wocan Strength".localized, + "key": "wocanStrength", + "type": "slider", + "value": [0, 1] ] ] } diff --git a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings index 499183206..5730c83d5 100644 --- a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings @@ -230,6 +230,8 @@ "Eyebrow Style Off" = "关闭"; "Eyebrow Style Type 1" = "类型1"; "Eyebrow Style Type 2" = "类型2"; +"Eyebrow Style Type 3" = "类型3"; +"Eyebrow Style Type 4" = "类型4"; "Eyebrow Color None" = "无"; "Eyebrow Color Black" = "类型1"; @@ -263,6 +265,8 @@ "Live Channel Name" = "直播频道名"; "default makeup style" = "学妹妆"; +"Wocan" = "卧蚕"; +"Wocan Strength" = "卧蚕强度"; "Male Template Title" = "模板男"; "Female Template Title" = "模板女"; diff --git a/macOS/APIExample.xcodeproj/project.pbxproj b/macOS/APIExample.xcodeproj/project.pbxproj index 19f626568..f19574923 100644 --- a/macOS/APIExample.xcodeproj/project.pbxproj +++ b/macOS/APIExample.xcodeproj/project.pbxproj @@ -145,7 +145,6 @@ E7AD0DF929CD84F800C9A4B0 /* sample.mov in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DF829CD84F800C9A4B0 /* sample.mov */; }; E7AD0E0129CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DFD29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard */; }; E7AD0E0229CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7AD0DFF29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.swift */; }; - F7453DBC2E03BCA5008DDFC8 /* AgoraBeautyMaterial.bundle in Resources */ = {isa = PBXBuildFile; fileRef = F7453DBB2E03BCA5008DDFC8 /* AgoraBeautyMaterial.bundle */; }; F7F28B932E03BADD003B150D /* AgoraBeauty.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7F28B8B2E03BADD003B150D /* AgoraBeauty.storyboard */; }; F7F28B942E03BADD003B150D /* AgoraBeautyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F28B8D2E03BADD003B150D /* AgoraBeautyManager.swift */; }; F7F28B952E03BADD003B150D /* AgoraBeauty.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F28B8C2E03BADD003B150D /* AgoraBeauty.swift */; }; @@ -989,6 +988,7 @@ 03896D2A24F8A00F008593CD /* Resources */, 032C0FA2254873AC00D80A57 /* Embed Frameworks */, D1D867115490181C41253FA3 /* [CP] Embed Pods Frameworks */, + F7453DBD2E03BCA5008DDFC8 /* Copy Optional Beauty Resources */, ); buildRules = ( ); @@ -1105,7 +1105,6 @@ 033A9FCC252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard in Resources */, 671BD67127DF478A0076D5E1 /* CustomVideoRender.storyboard in Resources */, DD33C1AA2CBF9DEA0046F50C /* RtePlayer.storyboard in Resources */, - F7453DBC2E03BCA5008DDFC8 /* AgoraBeautyMaterial.bundle in Resources */, 57A635F42593544600EDC2F7 /* effectA.wav in Resources */, AB1B2D922EC701AF007C8776 /* LocalVideoTranscoding.storyboard in Resources */, E7AD0DEB29CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.storyboard in Resources */, @@ -1192,6 +1191,25 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + F7453DBD2E03BCA5008DDFC8 /* Copy Optional Beauty Resources */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Optional Beauty Resources"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -eu\n\nSOURCE=\"${PROJECT_DIR:?}/APIExample/Resources/AgoraBeautyMaterial.bundle\"\nDESTINATION=\"${TARGET_BUILD_DIR:?}/${UNLOCALIZED_RESOURCES_FOLDER_PATH:?}/AgoraBeautyMaterial.bundle\"\n\n/bin/rm -rf \"${DESTINATION}\"\nif [ -d \"${SOURCE}\" ]; then\n /usr/bin/ditto \"${SOURCE}\" \"${DESTINATION}\"\nelse\n echo \"warning: AgoraBeautyMaterial.bundle is unavailable. Internal builds can provide it by running cloud_build.sh with JFROG_API_KEY.\"\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ From b1c2532a5fe6a7d65adf5b6033af877653861ec3 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Mon, 7 Sep 2026 11:42:19 +0800 Subject: [PATCH 34/49] fix: address Android 4.7.0 build blockers --- .github/workflows/mobile-ci.yml | 2 ++ .../agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt | 8 ++++---- .../beautyapi/faceunity/utils/egl/GLFrameBuffer.java | 6 +++--- .../beautyapi/sensetime/utils/egl/GLFrameBuffer.java | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index febbc32db..48a5cb8eb 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -29,6 +29,8 @@ jobs: cache: gradle cache-dependency-path: Android/APIExample/**/*.gradle* - uses: android-actions/setup-android@v3 + - name: Configure Android build + run: printf 'AGORA_APP_ID=00000000000000000000000000000000\n' > local.properties - name: Validate Gradle configuration run: ./gradlew :app:tasks --all - name: Validate Android XML resources diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt index ac492705c..bb3b019e4 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/FaceUnityBeautyAPIImpl.kt @@ -42,7 +42,7 @@ import io.agora.base.VideoFrame import io.agora.base.VideoFrame.I420Buffer import io.agora.base.VideoFrame.SourceType import io.agora.base.VideoFrame.TextureBuffer -import io.agora.base.internal.video.EglBase +import io.agora.base.internal.video.EglBaseLock import io.agora.base.internal.video.TextureBufferPool import io.agora.base.internal.video.YuvHelper import io.agora.beautyapi.faceunity.utils.APIReporter @@ -616,7 +616,7 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { } val ret = texBufferHelper.invoke { var result = -1 - synchronized(EglBase.lock) { + EglBaseLock.lock { result = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } result @@ -730,7 +730,7 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { return@invoke -1 } var fuTexId = -1 - synchronized(EglBase.lock) { + EglBaseLock.lock { fuTexId = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } outGLFrameBuffer.setSize(videoFrame.rotatedWidth, videoFrame.rotatedHeight) @@ -853,7 +853,7 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { } var fuTexId = -1 - synchronized(EglBase.lock) { + EglBaseLock.lock { fuTexId = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } outGLFrameBuffer.setSize(videoFrame.rotatedWidth, videoFrame.rotatedHeight) diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java index 79ecc3856..8d8c013f2 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java @@ -4,7 +4,7 @@ import android.opengl.GLES11Ext; import android.opengl.GLES20; -import io.agora.base.internal.video.EglBase; +import io.agora.base.internal.video.EglBaseLock; import io.agora.base.internal.video.GlRectDrawer; import io.agora.base.internal.video.RendererCommon; @@ -113,13 +113,13 @@ public int process(int textureId, int textureType) { transform.preTranslate(-0.5f, -0.5f); float[] matrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(transform); - synchronized (EglBase.lock) { + EglBaseLock.lock(() -> { if(textureType == GLES11Ext.GL_TEXTURE_EXTERNAL_OES){ drawer.drawOes(textureId, 0,matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); }else{ drawer.drawRgb(textureId, 0,matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); } - } + }); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_NONE); GLES20.glFinish(); diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java index f36cbd5b6..4d8cf377f 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/egl/GLFrameBuffer.java @@ -4,7 +4,7 @@ import android.opengl.GLES11Ext; import android.opengl.GLES20; -import io.agora.base.internal.video.EglBase; +import io.agora.base.internal.video.EglBaseLock; import io.agora.base.internal.video.GlRectDrawer; import io.agora.base.internal.video.RendererCommon; @@ -113,13 +113,13 @@ public int process(int textureId, int textureType) { transform.preTranslate(-0.5f, -0.5f); float[] matrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(transform); - synchronized (EglBase.lock) { + EglBaseLock.lock(() -> { if (textureType == GLES11Ext.GL_TEXTURE_EXTERNAL_OES) { drawer.drawOes(textureId, 0, matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); } else { drawer.drawRgb(textureId, 0, matrix, mWidth, mHeight, 0, 0, mWidth, mHeight,0); } - } + }); GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); GLES20.glFinish(); From f5695ee9d218b315e90be2f07887b9e9c78a5447 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Mon, 7 Sep 2026 20:08:36 +0800 Subject: [PATCH 35/49] fix: address RTC 4.7.0 review findings --- .../api-example-release-iteration/SKILL.md | 4 +- .github/ci/build/build_android.sh | 5 +- .github/ci/build/common_functions.sh | 6 +- Android/APIExample-Audio/README.md | 4 +- Android/APIExample-Audio/README.zh.md | 4 +- Android/APIExample-Compose/README.md | 4 +- Android/APIExample-Compose/README.zh.md | 4 +- Android/APIExample/ARCHITECTURE.md | 2 +- Android/APIExample/README.md | 17 +- Android/APIExample/README.zh.md | 14 +- .../io/agora/extension/ExtensionManager.java | 4 +- .../io/agora/extension/ResourceHelper.java | 221 ------------------ .../src/main/cpp/CMakeLists.txt | 2 +- Android/APIExample/app/.gitignore | 2 - Android/APIExample/app/proguard-rules.pro | 5 - .../examples/advanced/SimpleExtension.java | 4 +- .../sensetime/SenseTimeBeautyAPIImpl.kt | 34 +-- .../beautyapi/sensetime/utils/APIReporter.kt | 26 ++- .../app/src/main/res/values-zh/strings.xml | 2 - .../app/src/main/res/values/strings.xml | 4 +- Android/APIExample/cloud_build.sh | 3 - README.kr.md | 18 +- README.md | 16 +- README.zh.md | 16 +- .../ai-engineering/case-maintenance-matrix.md | 2 +- docs/ai-engineering/release-iteration-gate.md | 3 +- docs/ai-engineering/tools/README.md | 8 +- iOS/.gitignore | 1 - iOS/APIExample-Audio/Podfile | 2 +- iOS/APIExample-Audio/README.md | 14 +- iOS/APIExample-OC/Podfile | 2 +- iOS/APIExample-OC/README.md | 12 +- iOS/APIExample-SwiftUI/Podfile | 3 +- .../Render/SenseRender/SenseBeautyRender.m | 5 +- .../Manager/VideoProcessingManager.h | 2 + .../Manager/VideoProcessingManager.m | 39 +++- .../ThirdBeautify/ThirdBeautify.swift | 1 - iOS/APIExample/ARCHITECTURE.md | 3 +- iOS/APIExample/Podfile | 2 +- iOS/APIExample/README.md | 12 +- iOS/APIExample/bytedEffect.podspec | 23 -- iOS/APIExample/cloud_project.sh | 6 - macOS/Podfile | 2 +- macOS/README.md | 12 +- windows/README.md | 2 +- windows/README.zh.md | 2 +- 46 files changed, 178 insertions(+), 401 deletions(-) delete mode 100644 Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ResourceHelper.java delete mode 100644 iOS/APIExample/bytedEffect.podspec diff --git a/.agents/skills/api-example-release-iteration/SKILL.md b/.agents/skills/api-example-release-iteration/SKILL.md index e87ecb2e8..40825c60c 100644 --- a/.agents/skills/api-example-release-iteration/SKILL.md +++ b/.agents/skills/api-example-release-iteration/SKILL.md @@ -53,8 +53,8 @@ Initialize one requirement: python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ --matrix docs/ai-engineering/case-maintenance-matrix.md \ --feature "" \ - --target-sdk-version "4.6.2" \ - --platform-sdk-version "android=4.6.3" \ + --target-sdk-version "4.7.0" \ + --platform-sdk-version "android=4.7.0" \ --run-dir /tmp/api-example-requirement ``` diff --git a/.github/ci/build/build_android.sh b/.github/ci/build/build_android.sh index 545f6ff80..490170f08 100644 --- a/.github/ci/build/build_android.sh +++ b/.github/ci/build/build_android.sh @@ -104,7 +104,7 @@ if [ "$api_examples_branch_stripped" = "main" ]; then echo "Branch is main, skipping version consistency check (main branch is trusted)" echo "Using version: $SDK_VERSION" else - # Extract version number from branch name (supports formats like dev/4.6.2, release/4.6.2, etc.) + # Extract version number from branch name (supports formats like dev/4.7.0, release/4.7.0, etc.) BRANCH_NAME_VERSION=$(echo $api_examples_branch | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1) if [ -z "$BRANCH_NAME_VERSION" ]; then @@ -113,7 +113,7 @@ else echo "❌ CI BUILD FAILED: CANNOT EXTRACT VERSION FROM BRANCH" echo "==========================================" echo "Cannot extract version from branch name: $api_examples_branch" - echo "Branch name must contain version number (e.g., dev/4.6.2, release/4.6.2)" + echo "Branch name must contain version number (e.g., dev/4.7.0, release/4.7.0)" echo "==========================================" exit 1 fi @@ -257,4 +257,3 @@ else fi echo "Output package: $OUTPUT_ZIP_NAME" echo "==========================================" - diff --git a/.github/ci/build/common_functions.sh b/.github/ci/build/common_functions.sh index cc8649934..afa3aa654 100755 --- a/.github/ci/build/common_functions.sh +++ b/.github/ci/build/common_functions.sh @@ -56,7 +56,7 @@ get_branch_name() { # Function: Extract version from branch name # Args: # $1 - Branch name -# Returns: Version string (e.g., "4.6.2") or empty if no version is present +# Returns: Version string (e.g., "4.7.0") or empty if no version is present extract_branch_version() { local branch_name="$1" @@ -71,7 +71,7 @@ extract_branch_version() { # Args: # $1 - Project path (e.g., "iOS/APIExample" or "macOS") # $2 - Project name (e.g., "APIExample") -# $3 - Branch version (e.g., "4.6.2") +# $3 - Branch version (e.g., "4.7.0") # $4 - Platform (optional: "ios" or "macos", defaults to "ios") # Returns: 0 on success, 1 on failure validate_project_version() { @@ -225,7 +225,7 @@ run_version_validation() { echo "Error: Branch naming is not compliant!" echo "==========================================" echo "Current branch: $BRANCH_NAME" - echo "Branch name must contain version number (e.g., dev/4.6.2, release/4.6.2)" + echo "Branch name must contain version number (e.g., dev/4.7.0, release/4.7.0)" echo "" echo "Branch naming rules:" echo " - Version branches must contain x.x.x" diff --git a/Android/APIExample-Audio/README.md b/Android/APIExample-Audio/README.md index dcb062296..9324018bd 100644 --- a/Android/APIExample-Audio/README.md +++ b/Android/APIExample-Audio/README.md @@ -6,7 +6,9 @@ This project presents you a set of API examples to help you understand how to us ## Prerequisites -- Android Studio 3.0+ +- Android Studio Ladybug (2024.2.1) or later +- JDK 17 +- Android SDK Platform 35 - Physical Android device - Android simulator is supported diff --git a/Android/APIExample-Audio/README.zh.md b/Android/APIExample-Audio/README.zh.md index 2567a8725..d4a355903 100644 --- a/Android/APIExample-Audio/README.zh.md +++ b/Android/APIExample-Audio/README.zh.md @@ -6,7 +6,9 @@ ## 环境准备 -- Android Studio 3.0+ +- Android Studio Ladybug (2024.2.1) 或更高版本 +- JDK 17 +- Android SDK Platform 35 - Android 真机设备 - 支持模拟器 diff --git a/Android/APIExample-Compose/README.md b/Android/APIExample-Compose/README.md index e7455d89a..89964170d 100644 --- a/Android/APIExample-Compose/README.md +++ b/Android/APIExample-Compose/README.md @@ -6,7 +6,9 @@ This project presents you a set of API examples to help you understand how to us ## Prerequisites -- Android Studio 3.0+ +- Android Studio Ladybug (2024.2.1) or later +- JDK 17 +- Android SDK Platform 35 - Physical Android device - Android simulator is supported diff --git a/Android/APIExample-Compose/README.zh.md b/Android/APIExample-Compose/README.zh.md index d5b3cc5c0..08f2d7ee4 100644 --- a/Android/APIExample-Compose/README.zh.md +++ b/Android/APIExample-Compose/README.zh.md @@ -6,7 +6,9 @@ ## 环境准备 -- Android Studio 3.0+ +- Android Studio Ladybug (2024.2.1) 或更高版本 +- JDK 17 +- Android SDK Platform 35 - Android 真机设备 - 支持模拟器 diff --git a/Android/APIExample/ARCHITECTURE.md b/Android/APIExample/ARCHITECTURE.md index b6835db13..56fb4c020 100644 --- a/Android/APIExample/ARCHITECTURE.md +++ b/Android/APIExample/ARCHITECTURE.md @@ -134,7 +134,7 @@ APIExample/ | 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 | +| Screen 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 (hidden) | `advanced/RhythmPlayer.java` | `startRhythmPlayer()`, `stopRhythmPlayer()`, `enableAudioVolumeIndication()` | Source retained for reference; hidden from the case list because the Rhythm Player APIs are deprecated since RTC SDK 4.6.0 | diff --git a/Android/APIExample/README.md b/Android/APIExample/README.md index 5a422fa8c..245e1a183 100644 --- a/Android/APIExample/README.md +++ b/Android/APIExample/README.md @@ -6,7 +6,9 @@ This project presents you a set of API examples to help you understand how to us ## Prerequisites -- Android Studio 3.0+ +- Android Studio Ladybug (2024.2.1) or later +- JDK 17 +- Android SDK Platform 35 - Physical Android device - Android simulator is supported @@ -44,6 +46,11 @@ This project contains third-party beauty integration examples, which cannot be e without configuring resources and certificates. The resource certificate configuration method is as follows: +During Gradle configuration, the project automatically downloads and caches the SenseTime 9.3.1 +Android libraries declared in `app/vendors.gradle`. The first Gradle sync therefore requires network +access. This download does not include the SenseTime assets or license listed below; provide those +files separately. + #### Agora Beauty 2.0 The Agora Beauty 2.0 material package is not included in this repository. Before building locally: @@ -70,9 +77,9 @@ The material directory is ignored by Git. | SenseTime Beauty SDK Path | Location | |----------------------------------------------------------------------|----------------------------------------------------------| | Android/models | app/src/main/assets/beauty_sensetime/models | -| Android/smaple/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape | -| Android/smaple/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly | -| Android/smaple/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip | +| Android/sample/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape | +| Android/sample/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly | +| Android/sample/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip | | SenseME.lic | app/src/main/assets/beauty_sensetime/license/SenseME.lic | #### FaceUnity @@ -119,7 +126,7 @@ Android/APIExample/agora-simple-filter/src/main/cpp This project contains custom stream encrypt examples, which cannot be enabled by default. The configuration method is as follows: -1. Download [Agora SDK RESOURCE](https://docs.agora.io/en/sdks?platform=android), unzip it and copy c++ .so library (keeps arch folder) to Android/APIExample/agora-stream-encrypt/src/main/agoraLibs; Replace the hight_level_api/include .h files in the Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora directory. +1. Download [Agora SDK RESOURCE](https://docs.agora.io/en/sdks?platform=android), unzip it and copy c++ .so library (keeps arch folder) to Android/APIExample/agora-stream-encrypt/src/main/agoraLibs; Replace the high_level_api/include .h files in the Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora directory. ```text Android/APIExample/agora-stream-encrypt/src/main/agoraLibs diff --git a/Android/APIExample/README.zh.md b/Android/APIExample/README.zh.md index cf84f2549..5b012cc10 100644 --- a/Android/APIExample/README.zh.md +++ b/Android/APIExample/README.zh.md @@ -6,7 +6,9 @@ ## 环境准备 -- Android Studio 3.0+ +- Android Studio Ladybug (2024.2.1) 或更高版本 +- JDK 17 +- Android SDK Platform 35 - Android 真机设备 - 支持模拟器 @@ -41,6 +43,8 @@ 本项目包含第三方美颜集成示例,在没有配置资源和证书的情况下,默认是无法启用的。资源证书配置方法如下: +Gradle 配置阶段会根据 `app/vendors.gradle` 自动下载并缓存商汤 9.3.1 Android 依赖库,因此首次 Gradle Sync 需要联网。该下载不包含下方列出的商汤素材和许可证,这些文件仍需单独提供。 + #### Agora 美颜 2.0 Agora 美颜 2.0 资源包不包含在仓库中。本地构建前: @@ -67,9 +71,9 @@ app/src/main/assets/beauty_agora/ | 商汤SDK文件/目录 | 项目路径 | |----------------------------------------------------------------------|----------------------------------------------------------| | Android/models | app/src/main/assets/beauty_sensetime/models | -| Android/smaple/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape | -| Android/smaple/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly | -| Android/smaple/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip | +| Android/sample/SenseMeEffects/app/src/main/assets/sticker_face_shape | app/src/main/assets/beauty_sensetime/sticker_face_shape | +| Android/sample/SenseMeEffects/app/src/main/assets/style_lightly | app/src/main/assets/beauty_sensetime/style_lightly | +| Android/sample/SenseMeEffects/app/src/main/assets/makeup_lip | app/src/main/assets/beauty_sensetime/makeup_lip | | SenseME.lic | app/src/main/assets/beauty_sensetime/license/SenseME.lic | #### 相芯美颜 @@ -113,7 +117,7 @@ Android/APIExample/agora-simple-filter/src/main/cpp 本项目包含自定义加密示例,默认是不启用的。配置方法如下: -1. 手动下载[Agora SDK包](https://doc.shengwang.cn/doc/rtc/android/resources), 解压后将c++动态库(包括架构文件夹)copy到Android/APIExample/agora-stream-encrypt/src/main/agoraLibs,将 hight_level_api/include 头文件替换到 Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora 目录中 +1. 手动下载[Agora SDK包](https://doc.shengwang.cn/doc/rtc/android/resources), 解压后将c++动态库(包括架构文件夹)copy到Android/APIExample/agora-stream-encrypt/src/main/agoraLibs,将 high_level_api/include 头文件替换到 Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora 目录中 ```text Android/APIExample/agora-stream-encrypt/src/main/agoraLibs diff --git a/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ExtensionManager.java b/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ExtensionManager.java index b4efdd711..fb10ea64c 100644 --- a/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ExtensionManager.java +++ b/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ExtensionManager.java @@ -10,7 +10,7 @@ @Keep public class ExtensionManager { public static final String EXTENSION_NAME = "agora-simple-filter"; // Name of target link library used in CMakeLists.txt - public static final String EXTENSION_VENDOR_NAME = "Agora"; // Provider name used for registering in agora-bytedance.cpp + public static final String EXTENSION_VENDOR_NAME = "Agora"; // Provider name used for registering in agora-simple-filter.cpp public static final String EXTENSION_VIDEO_FILTER_WATERMARK = "Watermark"; // Video filter name defined in ExtensionProvider.h public static final String EXTENSION_AUDIO_FILTER_VOLUME = "VolumeChange"; // Audio filter name defined in ExtensionProvider.h @@ -30,4 +30,4 @@ public static void copyResource(Context context) { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ResourceHelper.java b/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ResourceHelper.java deleted file mode 100644 index 0e22a0d8b..000000000 --- a/Android/APIExample/agora-simple-filter/src/main/java/io/agora/extension/ResourceHelper.java +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright (C) 2018 Beijing Bytedance Network Technology Co., Ltd. -package io.agora.extension; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.annotation.NonNull; - -import java.io.File; - -public class ResourceHelper { - - public static final String RESOURCE = "resource"; - public static final String FILTER_RESOURCE = "FilterResource.bundle/Filter"; - - public static final String FACE = "ModelResource.bundle/ttfacemodel/tt_face_v8.2.model"; - public static final String PETFACE = "ModelResource.bundle/ttpetface/tt_petface_v2.4.model"; - public static final String DetectParamFile = "ModelResource.bundle/handmodel/tt_hand_det_v9.0.model"; - public static final String BoxRegParamFile = "ModelResource.bundle/handmodel/tt_hand_box_reg_v10.0.model"; - public static final String GestureParamFile = "ModelResource.bundle/handmodel/tt_hand_gesture_v8.1.model"; - public static final String KeyPointParamFile = "ModelResource.bundle/handmodel/tt_hand_kp_v5.0.model"; - public static final String SegParamFile = "ModelResource.bundle/handmodel/tt_hand_seg_v1.0.model"; - - private static final String FACEEXTA = "ModelResource.bundle/ttfacemodel/tt_face_extra_v11.0.model"; - private static final String FACEATTRI = "ModelResource.bundle/ttfaceattrmodel/tt_face_attribute_v6.1.model"; - private static final String FACEVERIFY = "ModelResource.bundle/ttfaceverify/tt_faceverify_v5.0.model"; - private static final String SKELETON = "ModelResource.bundle/skeleton_model/tt_skeleton_v6.0.model"; - private static final String PORTRAITMATTING = "ModelResource.bundle/mattingmodel/tt_matting_v9.0.model"; - private static final String HEADSEGMENT = "ModelResource.bundle/headseg/tt_headseg_v5.0.model"; - private static final String HAIRPARSING = "ModelResource.bundle/hairparser/tt_hair_v9.0.model"; -// private static final String LIGHTCLS = "ModelResource.bundle/lightcls/tt_lightcls_v1.0.model"; - private static final String HUMANDIST = "ModelResource.bundle/humandistance/tt_humandist_v1.0.model"; - public static final String GENERAL_OBJECT_DETECT = "ModelResource.bundle/generalobjectmodel/tt_general_obj_detection_v1.0.model"; - public static final String GENERAL_OBJECT_CLS = "ModelResource.bundle/generalobjectmodel/tt_general_obj_detection_cls_v1.0.model"; - public static final String GENERAL_OBJECT_TRACK = "ModelResource.bundle/generalobjectmodel/tt_sample_v1.0.model"; - - private static final String LICENSE_NAME = "Agora_20200915_20210914_agoramarketplace.bytedance.labcv.demo_v3.9.3.1.licbag"; - - private static String getResourcePath(Context context) { - return context.getExternalFilesDir("assets").getAbsolutePath() + File.separator + RESOURCE; - } - - public static String getModelDir(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), "ModelResource.bundle"), ""); - return file.getAbsolutePath(); - } - - public static String getLicensePath(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), "LicenseBag.bundle"), LICENSE_NAME); - return file.getAbsolutePath(); - } - - public static String getStickersPath(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), "StickerResource.bundle"), "stickers"); - return file.getAbsolutePath(); - } - - public static String getAnimojiPath(final Context context) { - File file = new File(new File(getResourcePath(context), "StickerResource.bundle"), "animoji"); - return file.getAbsolutePath(); - } - - public static String getGamePath(Context context) { - File file = new File(new File(getResourcePath(context), "StickerResource.bundle"), "game"); - return file.getAbsolutePath(); - } - - public static String getComposeMakeupComposerPath(@NonNull final Context context) { - return getResourcePath(context) + File.separator + "ComposeMakeup.bundle" + File.separator + "ComposeMakeup/composer"; - } - - public static String getComposePath(@NonNull final Context context) { - return getResourcePath(context) + File.separator + "ComposeMakeup.bundle" + File.separator + "ComposeMakeup/"; - } - - - public static File[] getFilterResources(@NonNull final Context context) { - return getResources(context, FILTER_RESOURCE); - } - - public static String getFaceModelPath(@NonNull final Context context) { - File file = new File(getResourcePath(context), FACE); - return file.getAbsolutePath(); - } - - public static String getPetFaceModelPath(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), PETFACE), ""); - return file.getAbsolutePath(); - } - - public static String getFaceExtaModelPath(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), FACEEXTA), ""); - return file.getAbsolutePath(); - } - - public static String getFaceAttriModelPath(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), FACEATTRI), ""); - return file.getAbsolutePath(); - } - - public static String getFaceVerifyModelPath(final Context context) { - File file = new File(new File(getResourcePath(context), FACEVERIFY), ""); - return file.getAbsolutePath(); - } - - public static String getSkeletonModelPath(@NonNull final Context context) { - File file = new File(new File(getResourcePath(context), SKELETON), ""); - return file.getAbsolutePath(); - } - - public static String getPortraitmattingModelPath(@NonNull final Context context) - { - File file = new File(new File(getResourcePath(context), PORTRAITMATTING), ""); - return file.getAbsolutePath(); - } - - public static String getHeadSegmentModelPath(@NonNull final Context context) - { - File file = new File(new File(getResourcePath(context), HEADSEGMENT), ""); - return file.getAbsolutePath(); - } - - - public static String getHairParsingModelPath(@NonNull final Context context) - { - File file = new File(new File(getResourcePath(context), HAIRPARSING), ""); - return file.getAbsolutePath(); - } - - public static String getHandModelPath(@NonNull final Context context,String path) { - File file = new File(new File(getResourcePath(context), path), ""); - return file.getAbsolutePath(); - } - -// public static String getLightClsModelPath(final Context context) { -// File file = new File(getResourcePath(context), LIGHTCLS); -// return file.getAbsolutePath(); -// } - - public static String getGeneralObjectDetect(final Context context) { - return new File(getResourcePath(context), GENERAL_OBJECT_DETECT).getAbsolutePath(); - } - - public static String getGeneralObjectCls(final Context context) { - return new File(getResourcePath(context), GENERAL_OBJECT_CLS).getAbsolutePath(); - } - - public static String getGeneralObjectTrack(final Context context) { - return new File(getResourcePath(context), GENERAL_OBJECT_TRACK).getAbsolutePath(); - } - - public static String getHumandistModelPath(final Context context) { - File file = new File(getResourcePath(context), HUMANDIST); - return file.getAbsolutePath(); - } - - - - public static File[] getResources(@NonNull final Context context, String type) { - File file = new File(new File(getResourcePath(context), type), ""); - if (file.exists() && file.isDirectory()) - return file.listFiles(); - return new File[0]; - } - - public static String getStickerPath(@NonNull final Context context,String name) { - return getStickersPath(context) + File.separator + name; - - } - - public static String getAnimojiPath(final Context context, String name) { - return getAnimojiPath(context) + File.separator + name; - } - - public static String getGamePath(Context context, String name) { - return getGamePath(context) + File.separator + name; - } - - public static boolean isResourceReady(@NonNull final Context context, int versionCode) { - - SharedPreferences preferences=context.getSharedPreferences("user", Context.MODE_PRIVATE); - boolean resourceReady = preferences.getBoolean("resource", false); - int preVersioncode = preferences.getInt("versionCode", 0); - - // Continue to check the version number if it was copied last time - if (resourceReady && versionCode == preVersioncode){ - return true; - } - return false; - } - - public static void setResourceReady(@NonNull final Context context, boolean isReady, int versionCode){ - SharedPreferences preferences=context.getSharedPreferences("user", Context.MODE_PRIVATE); - - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean("resource", isReady); - editor.putInt("versionCode", versionCode); - editor.commit(); - } - - public static String getDownloadedStickerDir(@NonNull final Context context){ - File file = new File(new File(getResourcePath(context), "download"), "sticker"); - if (!file.exists()) { - file.mkdirs(); - - } - return file.getAbsolutePath(); - - - } - - public static String getFilterResourcePathByName(@NonNull final Context context, String filterName){ - File file = new File(new File(getResourcePath(context), FILTER_RESOURCE), ""); - - return file.getAbsolutePath() + File.separator + filterName; - } - - - - -} diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt b/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt index 22785cf99..1d0b7d58c 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt @@ -11,7 +11,7 @@ project(agora-stream-encrypt) set(agora-lib-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora-rtc-sdk.so) link_libraries(${agora-lib-so}) -set(agora-ffmpeg-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora_ffmpeg.so) +set(agora-ffmpeg-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora-ffmpeg.so) link_libraries(${agora-ffmpeg-so}) set(agora-soundtouch-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora-soundtouch.so) diff --git a/Android/APIExample/app/.gitignore b/Android/APIExample/app/.gitignore index d21bc5656..3b466dd9a 100644 --- a/Android/APIExample/app/.gitignore +++ b/Android/APIExample/app/.gitignore @@ -20,10 +20,8 @@ androidTest/ Test/ authpack.java src/main/assets/beauty_agora -src/main/assets/beauty_bytedance src/main/assets/beauty_faceunity src/main/assets/beauty_sensetime/* -!src/main/assets/beauty_bytedance/PLACEHOLDER !src/main/assets/beauty_faceunity/PLACEHOLDER !src/main/assets/beauty_sensetime/PLACEHOLDER !src/main/assets/beauty_agora/PLACEHOLDER diff --git a/Android/APIExample/app/proguard-rules.pro b/Android/APIExample/app/proguard-rules.pro index 341bc81d4..c281f58bc 100644 --- a/Android/APIExample/app/proguard-rules.pro +++ b/Android/APIExample/app/proguard-rules.pro @@ -28,11 +28,6 @@ -keep class com.softsugar.**{*;} -keep class com.sensetime.**{*;} -# bytedance --keep class com.effectsar.**{*;} --keep class com.bes.**{*;} --keep class com.amazing.**{*;} - # ijkplayer -keep class tv.danmaku.ijk.media.player.** {*;} -keep class tv.danmaku.ijk.media.player.IjkMediaPlayer{*;} 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 071d4887f..7ab138995 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 @@ -61,7 +61,7 @@ public class SimpleExtension extends BaseFragment implements View.OnClickListene /** * The constant EXTENSION_VENDOR_NAME. */ - public static final String EXTENSION_VENDOR_NAME = "Agora"; // Provider name used for registering in agora-bytedance.cpp + public static final String EXTENSION_VENDOR_NAME = "Agora"; // Provider name used for registering in agora-simple-filter.cpp /** * The constant EXTENSION_VIDEO_FILTER_WATERMARK. */ @@ -187,7 +187,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ //Name of dynamic link library is provided by plug-in vendor, - //e.g. libagora-bytedance.so whose EXTENSION_NAME should be "agora-bytedance" + // e.g. libagora-simple-filter.so whose EXTENSION_NAME should be "agora-simple-filter" //and one or more plug-ins can be added config.addExtension(EXTENSION_NAME); config.mExtensionObserver = this; diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt index cb3f6a7b8..11462ac7c 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt @@ -492,6 +492,7 @@ class SenseTimeBeautyAPIImpl : SenseTimeBeautyAPI, IVideoFrameObserver { } apiReporter.reportFuncEvent("release", emptyMap(), emptyMap()) apiReporter.endDurationEvent("initialize-release", emptyMap()) + apiReporter.release() LogUtils.i(TAG, "release") isReleased = true @@ -855,9 +856,8 @@ class SenseTimeBeautyAPIImpl : SenseTimeBeautyAPI, IVideoFrameObserver { val buffer = videoFrame.buffer YuvConverter.setEnablePboOpt(true) YuvConverter.setEnableConvertPerLog(true) - val i420Buffer = buffer as? I420Buffer ?: buffer.toI420() - val width = i420Buffer.width - val height = i420Buffer.height + val width = buffer.width + val height = buffer.height val nv21Size = (width * height * 3.0f / 2.0f + 0.5f).toInt() if (nv21ByteBuffer == null || nv21ByteBuffer?.capacity() != nv21Size) { nv21ByteBuffer?.clear() @@ -865,19 +865,23 @@ class SenseTimeBeautyAPIImpl : SenseTimeBeautyAPI, IVideoFrameObserver { return null } val nv21ByteArray = ByteArray(nv21Size) + val i420Buffer = buffer as? I420Buffer ?: buffer.toI420() - YuvHelper.I420ToNV12( - i420Buffer.dataY, i420Buffer.strideY, - i420Buffer.dataV, i420Buffer.strideV, - i420Buffer.dataU, i420Buffer.strideU, - nv21ByteBuffer, width, height - ) - nv21ByteBuffer?.position(0) - nv21ByteBuffer?.get(nv21ByteArray) - if (buffer !is I420Buffer) { - i420Buffer.release() + try { + YuvHelper.I420ToNV12( + i420Buffer.dataY, i420Buffer.strideY, + i420Buffer.dataV, i420Buffer.strideV, + i420Buffer.dataU, i420Buffer.strideU, + nv21ByteBuffer, width, height + ) + nv21ByteBuffer?.position(0) + nv21ByteBuffer?.get(nv21ByteArray) + return nv21ByteArray + } finally { + if (buffer !is I420Buffer) { + i420Buffer.release() + } } - return nv21ByteArray } // IVideoFrameObserver implements @@ -927,4 +931,4 @@ class SenseTimeBeautyAPIImpl : SenseTimeBeautyAPI, IVideoFrameObserver { override fun getObservedFramePosition() = IVideoFrameObserver.POSITION_POST_CAPTURER -} \ No newline at end of file +} diff --git a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt index 2d5ce59e9..a520b1cc1 100644 --- a/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt @@ -2,8 +2,9 @@ package io.agora.beautyapi.sensetime.utils import io.agora.rtc2.RtcEngine import org.json.JSONObject -import java.util.concurrent.Executors import java.lang.ref.WeakReference +import java.util.concurrent.Executors +import java.util.concurrent.RejectedExecutionException enum class APIType(val value: Int) { KTV(1), // Karaoke @@ -54,7 +55,7 @@ class APIReporter( // Report regular scenario API fun reportFuncEvent(name: String, value: Map, ext: Map) { - executorService.submit { + submitReport { rtcEngineRef.get()?.let { val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.API.value, ApiEventKey.DESC to name) val labelMap = mapOf( @@ -95,7 +96,7 @@ class APIReporter( // Report custom information fun reportCustomEvent(name: String, ext: Map) { - executorService.submit { + submitReport { rtcEngineRef.get()?.let { val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.CUSTOM.value, ApiEventKey.DESC to name) val labelMap = mapOf(ApiEventKey.TIMESTAMP to getCurrentTs(), ApiEventKey.EXT to ext) @@ -114,10 +115,14 @@ class APIReporter( durationEventStartMap.clear() } + fun release() { + executorService.shutdown() + } + // ---------------------- private ---------------------- private fun configParameters() { - executorService.submit { + submitReport { rtcEngineRef.get()?.let { // it.setParameters("{\"rtc.qos_for_test_purpose\": true}") // Used for test environment // Data reporting @@ -133,7 +138,7 @@ class APIReporter( } private fun innerReportCostEvent(ts: Long, name: String, cost: Int, ext: Map) { - executorService.submit { + submitReport { rtcEngineRef.get()?.let { // writeLog("reportCostEvent: $name cost: $cost ms", Constants.LOG_LEVEL_INFO) val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.COST.value, ApiEventKey.DESC to name) @@ -145,6 +150,15 @@ class APIReporter( } } + private fun submitReport(report: () -> Unit) { + if (executorService.isShutdown) return + try { + executorService.submit(report) + } catch (_: RejectedExecutionException) { + // release() may race with an in-flight frame finishing its report. + } + } + private fun convertToJSONString(dictionary: Map): String? { return try { JSONObject(dictionary).toString() @@ -153,4 +167,4 @@ class APIReporter( null } } -} \ No newline at end of file +} diff --git a/Android/APIExample/app/src/main/res/values-zh/strings.xml b/Android/APIExample/app/src/main/res/values-zh/strings.xml index b0841f9dc..9ccee7ace 100644 --- a/Android/APIExample/app/src/main/res/values-zh/strings.xml +++ b/Android/APIExample/app/src/main/res/values-zh/strings.xml @@ -334,8 +334,6 @@ 相芯美颜 美体 相芯库没有集成!请按README集成后再编译运行 - 字节美颜 - 字节库没有集成!请按README集成后再编译运行 私有云 IP地址 diff --git a/Android/APIExample/app/src/main/res/values/strings.xml b/Android/APIExample/app/src/main/res/values/strings.xml index a906a928f..975e9346c 100644 --- a/Android/APIExample/app/src/main/res/values/strings.xml +++ b/Android/APIExample/app/src/main/res/values/strings.xml @@ -108,7 +108,7 @@ Multi Video Source Tracks Switch Video Sources Multiple Video Sources - Scree Sharing + Screen Sharing Custom Audio Sources Custom Audio Render Custom Video Renderer @@ -347,8 +347,6 @@ FaceUnityBeauty Body Beauty FaceUnity library is not integrated! Please read README before compiling - ByteDance Beauty - ByteDance library is not integrated! Please read README before compiling Private Cloud IP Address diff --git a/Android/APIExample/cloud_build.sh b/Android/APIExample/cloud_build.sh index d6c4f12ad..459d1d4bd 100755 --- a/Android/APIExample/cloud_build.sh +++ b/Android/APIExample/cloud_build.sh @@ -93,9 +93,6 @@ test -f assets/beauty_agora/beauty_material_functional/config.json || exit 1 curl -L -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -O "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/vender_faceunity_resources_apiexample.zip" unzip -o vender_faceunity_resources_apiexample.zip rm -f vender_faceunity_resources_apiexample.zip -#curl -L -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -O "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/vender_bytedance_resources_apiexample.zip" -#unzip -o vender_bytedance_resources_apiexample.zip -#rm -f vender_bytedance_resources_apiexample.zip curl -L -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -O "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/vender_sensetime_resources_apiexample.zip" unzip -o vender_sensetime_resources_apiexample.zip rm -f vender_sensetime_resources_apiexample.zip diff --git a/README.kr.md b/README.kr.md index b28ff6867..2d74c0acc 100644 --- a/README.kr.md +++ b/README.kr.md @@ -8,14 +8,14 @@ | 플랫폼 | 언어 | 프로젝트 위치 | SDK | |----------|-------------|------------------------------------------------------------|---------------------------------------------------------------------------| -| Android | Java | [/Android/APIExample](/Android/APIExample) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | -| Android | Kotlin | [/Android/APIExample-Compose](/Android/APIExample-Compose) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | -| Android | Java | [/Android/APIExample-Audio](/Android/APIExample-Audio) | [RTC Java Audio SDK](https://docs.agora.io/en/sdks?platform=android) | -| iOS | Swift | [/iOS/APIExample](/iOS/APIExample) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | -| iOS | Objective-C | [/iOS/APIExample-OC](/iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | -| iOS | Swift | [/iOS/APIExample-Audio](/iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://docs.agora.io/en/sdks?platform=ios) | -| macOS | Swift | [/macOS](/macOS) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=macos) | -| Windows | C++ | [/windows](/windows) | [RTC C++ Video SDK](https://docs.agora.io/en/sdks?platform=windows) | +| Android | Java | [Android/APIExample](Android/APIExample) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | +| Android | Kotlin | [Android/APIExample-Compose](Android/APIExample-Compose) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | +| Android | Java | [Android/APIExample-Audio](Android/APIExample-Audio) | [RTC Java Audio SDK](https://docs.agora.io/en/sdks?platform=android) | +| iOS | Swift | [iOS/APIExample](iOS/APIExample) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | +| iOS | Objective-C | [iOS/APIExample-OC](iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | +| iOS | Swift | [iOS/APIExample-Audio](iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://docs.agora.io/en/sdks?platform=ios) | +| macOS | Swift | [macOS](macOS) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=macos) | +| Windows | C++ | [windows](windows) | [RTC C++ Video SDK](https://docs.agora.io/en/sdks?platform=windows) | 각 플랫폼의 내용을 참조하여 프로젝트에 대해 자세히 알아볼 수 있습니다. @@ -33,4 +33,4 @@ ## 라이선스 -샘플 프로젝트는 MIT 라이선스를 따릅니다. \ No newline at end of file +샘플 프로젝트는 MIT 라이선스를 따릅니다. diff --git a/README.md b/README.md index 6d7940afd..9226e27fd 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ This repository contains sample projects for the Agora RTC Native SDK, including | Platform | Language | Project Location | SDK | |----------|-------------|------------------------------------------------------------|---------------------------------------------------------------------------| -| Android | Java | [/Android/APIExample](/Android/APIExample) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | -| Android | Kotlin | [/Android/APIExample-Compose](/Android/APIExample-Compose) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | -| Android | Java | [/Android/APIExample-Audio](/Android/APIExample-Audio) | [RTC Java Audio SDK](https://docs.agora.io/en/sdks?platform=android) | -| iOS | Swift | [/iOS/APIExample](/iOS/APIExample) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | -| iOS | Objective-C | [/iOS/APIExample-OC](/iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | -| iOS | Swift | [/iOS/APIExample-Audio](/iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://docs.agora.io/en/sdks?platform=ios) | -| macOS | Swift | [/macOS](/macOS) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=macos) | -| Windows | C++ | [/windows](/windows) | [RTC C++ Video SDK](https://docs.agora.io/en/sdks?platform=windows) | +| Android | Java | [Android/APIExample](Android/APIExample) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | +| Android | Kotlin | [Android/APIExample-Compose](Android/APIExample-Compose) | [RTC Java Video SDK](https://docs.agora.io/en/sdks?platform=android) | +| Android | Java | [Android/APIExample-Audio](Android/APIExample-Audio) | [RTC Java Audio SDK](https://docs.agora.io/en/sdks?platform=android) | +| iOS | Swift | [iOS/APIExample](iOS/APIExample) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | +| iOS | Objective-C | [iOS/APIExample-OC](iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | +| iOS | Swift | [iOS/APIExample-Audio](iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://docs.agora.io/en/sdks?platform=ios) | +| macOS | Swift | [macOS](macOS) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=macos) | +| Windows | C++ | [windows](windows) | [RTC C++ Video SDK](https://docs.agora.io/en/sdks?platform=windows) | You can refer to each individual platform to learn more about the projects. diff --git a/README.zh.md b/README.zh.md index 631e19de5..4cf227616 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,14 +8,14 @@ | 平台 | 语言 | 项目位置 | SDK | |---------|-------------|------------------------------------------------------------|-------------------------------------------------------------------------------| -| Android | Java | [/Android/APIExample](/Android/APIExample) | [RTC Java Video SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | -| Android | Kotlin | [/Android/APIExample-Compose](/Android/APIExample-Compose) | [RTC Java Video SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | -| Android | Java | [/Android/APIExample-Audio](/Android/APIExample-Audio) | [RTC Java Audio SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | -| iOS | Swift | [/iOS/APIExample](/iOS/APIExample) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | -| iOS | Objective-C | [/iOS/APIExample-OC](/iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | -| iOS | Swift | [/iOS/APIExample-Audio](/iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | -| macOS | Swift | [/macOS](/macOS) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/macos/resources) | -| Windows | C++ | [/windows](/windows) | [RTC C++ Video SDK](https://doc.shengwang.cn/doc/rtc/windows/resources) | +| Android | Java | [Android/APIExample](Android/APIExample) | [RTC Java Video SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | +| Android | Kotlin | [Android/APIExample-Compose](Android/APIExample-Compose) | [RTC Java Video SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | +| Android | Java | [Android/APIExample-Audio](Android/APIExample-Audio) | [RTC Java Audio SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | +| iOS | Swift | [iOS/APIExample](iOS/APIExample) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | +| iOS | Objective-C | [iOS/APIExample-OC](iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | +| iOS | Swift | [iOS/APIExample-Audio](iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | +| macOS | Swift | [macOS](macOS) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/macos/resources) | +| Windows | C++ | [windows](windows) | [RTC C++ Video SDK](https://doc.shengwang.cn/doc/rtc/windows/resources) | 你可以进入不同平台的项目进行试用或参考源代码。 diff --git a/docs/ai-engineering/case-maintenance-matrix.md b/docs/ai-engineering/case-maintenance-matrix.md index c149856f1..1b9162bb8 100644 --- a/docs/ai-engineering/case-maintenance-matrix.md +++ b/docs/ai-engineering/case-maintenance-matrix.md @@ -50,7 +50,7 @@ Audio, Compose, SwiftUI, and Objective-C variants are outside this matrix and do Seeded from the primary project `ARCHITECTURE.md` indexes and source trees, then re-verified against `dev/4.7.0` (`ee77cc85`). Paths are relative to the projects listed in Platform Units. -Inventory scope: the initial rows were derived from the `Android/APIExample/` case index and its counterparts in the other three primary projects. Features that exist only outside the Android primary project are not inventoried yet, including `ARKit`, `FaceCapture`, `MutliCamera`/`MultiCameraSourece`, `RawMediaData`, `CustomVideoSourceMediaIO`, `VideoChat`, and Windows-only dialogs such as `AudioProfile`, `AudioVolume`, `RegionConn`, `ReportInCall`, `RTMPinject`, and `MultiVideoSource`. Their absence from this matrix is an inventory gap, not a coverage claim. +Inventory scope: the initial rows were derived from the `Android/APIExample/` case index and its counterparts in the other three primary projects. Features that exist only outside the Android primary project are not inventoried yet, including `ARKit`, `MutliCamera`/`MultiCameraSourece`, `RawMediaData`, `CustomVideoSourceMediaIO`, `VideoChat`, and Windows-only dialogs such as `AudioProfile`, `AudioVolume`, `RegionConn`, `ReportInCall`, `RTMPinject`, and `MultiVideoSource`. Their absence from this matrix is an inventory gap, not a coverage claim. | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | | --- | --- | --- | --- | --- | --- | --- | --- | diff --git a/docs/ai-engineering/release-iteration-gate.md b/docs/ai-engineering/release-iteration-gate.md index ea75e6610..9ea833068 100644 --- a/docs/ai-engineering/release-iteration-gate.md +++ b/docs/ai-engineering/release-iteration-gate.md @@ -110,8 +110,7 @@ The validator enforces platform completeness, unique agent/run provenance, Contr python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ --matrix docs/ai-engineering/case-maintenance-matrix.md \ --feature "" \ - --target-sdk-version "4.6.2" \ - --platform-sdk-version "android=4.6.3" \ + --target-sdk-version "4.7.0" \ --run-dir /tmp/api-example-requirement python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ diff --git a/docs/ai-engineering/tools/README.md b/docs/ai-engineering/tools/README.md index bf5bf27fe..d6c17d0d9 100644 --- a/docs/ai-engineering/tools/README.md +++ b/docs/ai-engineering/tools/README.md @@ -18,14 +18,13 @@ Provider model names remain runtime inputs through `--model` or `CODEX_MODEL_STA python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ --matrix docs/ai-engineering/case-maintenance-matrix.md \ --feature "Join channel audio" \ - --target-sdk-version "4.6.2" \ - --platform-sdk-version "android=4.6.3" \ + --target-sdk-version "4.7.0" \ --run-dir /tmp/api-example-requirement ``` For a new feature outside the matrix backlog, add `--sdk-family "Full RTC" --key-api ""`; repeat `--key-api` for multiple APIs. -`--target-sdk-version` is a baseline expanded to Android, iOS, macOS, and Windows. Repeat `--platform-sdk-version platform=x.y.z` for any platform on a different SDK line. For example, the command above resolves to Android 4.6.3 and iOS/macOS/Windows 4.6.2. The final mapping is stored in the execution package and manifest; platform release cadence changes do not require edits to Python or the repository profile. +`--target-sdk-version` is a baseline expanded to Android, iOS, macOS, and Windows. The command above resolves every platform to 4.7.0. Repeat `--platform-sdk-version platform=x.y.z` for any platform on a different SDK line. The final mapping is stored in the execution package and manifest; platform release cadence changes do not require edits to Python or the repository profile. The default `docs/ai-engineering/repository-profile.json` defines repository-specific SDK version sources. Shared tools support Gradle property, CocoaPods package, SDK archive-name, and explicit external-injection sources without hard-coding a distribution in Python. An external-injection source keeps its platform SDK check `BLOCKED` because tracked project files cannot prove the injected binary version. `init` accepts `--repository-profile` for a checked-in alternative and binds its path and SHA-256 into the execution package, input snapshots, and acceptance manifest. @@ -85,8 +84,7 @@ python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ ```bash python3 docs/ai-engineering/tools/prepare_case_execution.py \ --feature "Join channel audio" \ - --target-sdk-version "4.6.2" \ - --platform-sdk-version "android=4.6.3" \ + --target-sdk-version "4.7.0" \ --repository-profile docs/ai-engineering/repository-profile.json python3 docs/ai-engineering/tools/validate_acceptance_manifest.py ``` diff --git a/iOS/.gitignore b/iOS/.gitignore index 2979ca3d9..4e01c5a78 100644 --- a/iOS/.gitignore +++ b/iOS/.gitignore @@ -16,7 +16,6 @@ Packaging.log *.app FULib/ SenseLib/ -ByteEffectLib/ # Xcode # diff --git a/iOS/APIExample-Audio/Podfile b/iOS/APIExample-Audio/Podfile index 2830cfbe6..bd9c7d606 100644 --- a/iOS/APIExample-Audio/Podfile +++ b/iOS/APIExample-Audio/Podfile @@ -12,5 +12,5 @@ target 'APIExample-Audio' do end pre_install do |installer| - # system("sh .download_script.sh 4.5.2 true") + # system("sh .download_script.sh 4.7.0 true") end diff --git a/iOS/APIExample-Audio/README.md b/iOS/APIExample-Audio/README.md index acefe6e3c..bed41bd59 100644 --- a/iOS/APIExample-Audio/README.md +++ b/iOS/APIExample-Audio/README.md @@ -23,18 +23,14 @@ This section shows you how to prepare, build, and run the sample application. ### Prepare Dependencies -Change directory into **iOS** folder, run following command to install project dependencies, +Change to the sample directory and install the project dependencies with CocoaPods. See the [official CocoaPods guide](https://guides.cocoapods.org/using/getting-started.html) for installation instructions. -use cocoapods - -[install cocoapods](http://t.zoukankan.com/lijiejoy-p-9680485.html) - - -``` +```bash +cd iOS/APIExample-Audio pod install ``` -Verify `APIExample.xcworkspace` has been properly generated. +Verify `APIExample-Audio.xcworkspace` has been properly generated. ### Obtain an App Id @@ -79,7 +75,7 @@ You are all set. Now connect your iPhone or iPad device and run the project. - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) - You can find full API documentation at [Document Center](https://docs.agora.io/en/) - If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) -- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues) ## License diff --git a/iOS/APIExample-OC/Podfile b/iOS/APIExample-OC/Podfile index 7d45b16f5..43e0bc065 100644 --- a/iOS/APIExample-OC/Podfile +++ b/iOS/APIExample-OC/Podfile @@ -26,5 +26,5 @@ target 'SimpleFilter' do end pre_install do |installer| - # system("sh .download_script.sh 4.5.2 true") + # system("sh .download_script.sh 4.7.0 true") end diff --git a/iOS/APIExample-OC/README.md b/iOS/APIExample-OC/README.md index e64199d83..160328e7a 100644 --- a/iOS/APIExample-OC/README.md +++ b/iOS/APIExample-OC/README.md @@ -23,14 +23,10 @@ This section shows you how to prepare, build, and run the sample application. ### Prepare Dependencies -Change directory into **iOS** folder, run following command to install project dependencies, +Change to the sample directory and install the project dependencies with CocoaPods. See the [official CocoaPods guide](https://guides.cocoapods.org/using/getting-started.html) for installation instructions. -use cocoapods - -[install cocoapods](http://t.zoukankan.com/lijiejoy-p-9680485.html) - - -``` +```bash +cd iOS/APIExample-OC pod install ``` @@ -79,7 +75,7 @@ You are all set. Now connect your iPhone or iPad device and run the project. - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) - You can find full API documentation at [Document Center](https://docs.agora.io/en/) - If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) -- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues) ## License diff --git a/iOS/APIExample-SwiftUI/Podfile b/iOS/APIExample-SwiftUI/Podfile index d8467f96f..a59f9d423 100644 --- a/iOS/APIExample-SwiftUI/Podfile +++ b/iOS/APIExample-SwiftUI/Podfile @@ -15,7 +15,6 @@ target 'APIExample-SwiftUI' do # pod 'ijkplayer', '~> 1.1.3' # pod 'SwiftLint', '~> 0.53.0' # pod 'senseLib', :path => 'sense.podspec' -# pod 'bytedEffect', :path => 'bytedEffect.podspec' # pod 'fuLib', :path => 'fu.podspec' end @@ -33,5 +32,5 @@ end #end pre_install do |installer| -# system("sh .download_script.sh 4.5.2 true") +# system("sh .download_script.sh 4.7.0 true") end diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m index 9b1423c67..f54c41e88 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/BeautyAPI/Render/SenseRender/SenseBeautyRender.m @@ -79,7 +79,10 @@ - (void)destroy { #if __has_include(Sensetime) [self.timer invalidate]; self.timer = nil; - [self reset]; + if (_videoProcessing != nil) { + [self reset]; + [_videoProcessing cleanup]; + } _videoProcessing = nil; #endif } diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h index 80da6c616..2dbcd0155 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)cleareStickers; +- (void)cleanup; + @end NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m index 8d0a81aa3..dfe69a25e 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m @@ -112,6 +112,23 @@ - (void)cleareStickers { #endif } +- (void)releaseOutputResources { +#if __has_include("st_mobile_common.h") + if (self.effectsProcess && (_outTexture || _outputPixelBuffer || _outputCVTexture)) { + [self.effectsProcess deleteTexture:&_outTexture + pixelBuffer:&_outputPixelBuffer + cvTexture:&_outputCVTexture]; + } +#endif + _outTexture = 0; + _outputPixelBuffer = NULL; + _outputCVTexture = NULL; +} + +- (void)cleanup { + [self releaseOutputResources]; +} + - (CVPixelBufferRef)videoProcessHandler:(CVPixelBufferRef)pixelBuffer { if (!pixelBuffer) return pixelBuffer; @@ -127,22 +144,24 @@ - (CVPixelBufferRef)videoProcessHandler:(CVPixelBufferRef)pixelBuffer { int width = (int)CVPixelBufferGetWidth(pixelBuffer); int heigh = (int)CVPixelBufferGetHeight(pixelBuffer); #if __has_include("st_mobile_common.h") - if (_outTexture) { + if (_outTexture && _outputPixelBuffer && _outputCVTexture) { int _cacheW = (int)CVPixelBufferGetWidth(_outputPixelBuffer); int _cacheH = (int)CVPixelBufferGetHeight(_outputPixelBuffer); if (_cacheH != heigh || _cacheW != width) { - GLuint testTexture = 0; //TODO: shengtao - [self.effectsProcess deleteTexture:&testTexture pixelBuffer:&_outputPixelBuffer cvTexture:&_outputCVTexture]; - _outTexture = 0; - _outputPixelBuffer = NULL; - _outputCVTexture = NULL; + [self releaseOutputResources]; } - } else { + } + if (!_outTexture || !_outputPixelBuffer || !_outputCVTexture) { [self.effectsProcess createGLObjectWith:width height:heigh texture:&_outTexture pixelBuffer:&_outputPixelBuffer cvTexture:&_outputCVTexture]; + if (!_outTexture || !_outputPixelBuffer || !_outputCVTexture) { + [self releaseOutputResources]; + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + return pixelBuffer; + } } st_mobile_human_action_t detectResult; memset(&detectResult, 0, sizeof(st_mobile_human_action_t)); @@ -168,7 +187,11 @@ - (CVPixelBufferRef)videoProcessHandler:(CVPixelBufferRef)pixelBuffer { CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); - return self->_outputPixelBuffer; + return self->_outputPixelBuffer ?: pixelBuffer; +} + +- (void)dealloc { + [self cleanup]; } - (void)getDeviceOrientation:(CMAccelerometerData *)accelerometerData { diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift index 798408906..392ba498c 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift @@ -11,7 +11,6 @@ import UIKit enum ThirdBeautifyType: String { case sense = "SenseBeautify" case fu = "FUBeautify" - case bytedEffect = "BytedEffect" } class ThirdBeautifyEntry: BaseViewController { diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index 185bb113a..21429b325 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -40,7 +40,7 @@ | ContentInspect | `Examples/Advanced/ContentInspect/ContentInspect.swift` | `enableContentInspect()`, `switchCamera()` | Moderate content in video stream | | MutliCamera | `Examples/Advanced/MutliCamera/MutliCamera.swift` | `enableMultiCamera()`, `startCameraCapture()`, `stopCameraCapture()` | Capture from front and back cameras simultaneously (iOS 13+) | | KtvCopyrightMusic | `Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.swift` | — | Links to KTV copyright music documentation | -| ThirdBeautify | `Examples/Advanced/ThirdBeautify/ThirdBeautify.swift` | `enableExtension()` | Third-party beauty SDK integration (ByteDance / FaceUnity / SenseTime) | +| ThirdBeautify | `Examples/Advanced/ThirdBeautify/ThirdBeautify.swift` | `enableExtension()` | Third-party beauty SDK integration (FaceUnity / SenseTime) | | ARKit | `Examples/Advanced/ARKit/ARKit.swift` | `setVideoFrameDelegate()`, `enableInstantMediaRendering()`, `startMediaRenderingTracing()` | Push ARKit face tracking frames as custom video source | | AudioRouterPlayer | `Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift` | `setEnableSpeakerphone()` | Control audio output routing with third-party player | | AudioWaveform | `Examples/Advanced/AudioWaveform/AudioWaveform.swift` | `setAudioProfile()`, `enableAudioVolumeIndication()` | Visualize audio waveform from volume callbacks | @@ -56,7 +56,6 @@ APIExample/ ├── Podfile # CocoaPods dependencies (AgoraRtcEngine_iOS, Floaty, AGEVideoLayout, etc.) ├── SimpleFilter/ # Optional C++ audio/video extension module ├── Agora-ScreenShare-Extension/ # ReplayKit broadcast extension for screen sharing -├── ByteEffectLib/ # Optional ByteDance beauty SDK resources ├── FULib/ # Optional FaceUnity beauty SDK resources ├── SenseLib/ # Optional SenseTime beauty SDK resources ├── libs/ # Local SDK frameworks (when not using CocoaPods) diff --git a/iOS/APIExample/Podfile b/iOS/APIExample/Podfile index 16fc000f6..790c29df8 100644 --- a/iOS/APIExample/Podfile +++ b/iOS/APIExample/Podfile @@ -38,5 +38,5 @@ target 'SimpleFilter' do end pre_install do |installer| -# system("sh .download_script.sh 4.5.2 true") +# system("sh .download_script.sh 4.7.0 true") end diff --git a/iOS/APIExample/README.md b/iOS/APIExample/README.md index eab043cf0..b4f7b1cf3 100644 --- a/iOS/APIExample/README.md +++ b/iOS/APIExample/README.md @@ -23,14 +23,10 @@ This section shows you how to prepare, build, and run the sample application. ### Prepare Dependencies -Change directory into **iOS** folder, run following command to install project dependencies, +Change to the sample directory and install the project dependencies with CocoaPods. See the [official CocoaPods guide](https://guides.cocoapods.org/using/getting-started.html) for installation instructions. -use cocoapods - -[install cocoapods](http://t.zoukankan.com/lijiejoy-p-9680485.html) - - -``` +```bash +cd iOS/APIExample pod install ``` @@ -96,7 +92,7 @@ The extracted bundle is ignored by Git. - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) - You can find full API documentation at [Document Center](https://docs.agora.io/en/) - If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) -- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues) ## License diff --git a/iOS/APIExample/bytedEffect.podspec b/iOS/APIExample/bytedEffect.podspec deleted file mode 100644 index 21cc4b4d9..000000000 --- a/iOS/APIExample/bytedEffect.podspec +++ /dev/null @@ -1,23 +0,0 @@ -Pod::Spec.new do |s| - s.name = "bytedEffect" - s.version = "4.5.2" - s.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} - s.homepage = 'https://github.com' - s.author = { "Agora Lab" => "developer@agora.io" } - s.summary = 'Demo for effect-sdk' - s.description = "iOS library for agora A/V communication, broadcasting and data channel service." - s.platform = :ios - s.frameworks = 'Accelerate','AssetsLibrary','AVFoundation','CoreGraphics','CoreImage','CoreMedia','CoreVideo','Foundation','QuartzCore','UIKit','CoreMotion','Accelerate','JavaScriptCore' - s.weak_frameworks = 'Metal','MetalPerformanceShaders', 'Photos', 'CoreML' - s.source = { :git => "" } - # s.source_files = "ByteEffectLib/**/*.h" - # s.public_header_files = "ByteEffectLib/**/*.h" - # s.header_mappings_dir = "ByteEffectLib/BytedEffectSDK/" - s.vendored_framework = "ByteEffectLib/*.framework" - s.vendored_libraries = "ByteEffectLib/*.a" - s.resource_bundles = {"ByteEffectLib" => "ByteEffectLib/**/*.bundle"} - s.platform = :ios, '9.0' - s.requires_arc = true - s.libraries = 'z', 'stdc++' - s.dependency 'SSZipArchive' -end diff --git a/iOS/APIExample/cloud_project.sh b/iOS/APIExample/cloud_project.sh index f18177c28..fb56e5d1f 100755 --- a/iOS/APIExample/cloud_project.sh +++ b/iOS/APIExample/cloud_project.sh @@ -7,11 +7,6 @@ if [ "$WORKSPACE" = "" ]; then fi # Download beauty resource -echo "start download bytedance resource : $bytedance_lib" -curl -L -O "$bytedance_lib" -unzip -o vender_bytedance_iOS.zip -rm -f vender_bytedance_iOS.zip - echo "start download sense resource : $sense_lib" curl -L -O "$sense_lib" unzip -o vender_sense_iOS.zip @@ -27,7 +22,6 @@ perl -i -pe "s#\# pod 'ijkplayer'# pod 'ijkplayer'#g" Podfile # Enable third-party beauty filters perl -i -pe "s#\#pod 'SenseLib'#pod 'SenseLib'#g" Podfile -perl -i -pe "s#\#pod 'bytedEffect'#pod 'bytedEffect'#g" Podfile perl -i -pe "s#\#pod 'fuLib'#pod 'fuLib'#g" Podfile pod install --repo-update || exit 1 diff --git a/macOS/Podfile b/macOS/Podfile index d68c0c1f9..39dace798 100644 --- a/macOS/Podfile +++ b/macOS/Podfile @@ -21,7 +21,7 @@ end post_install do |installer| - # system("sh .download_script.sh 4.4.0 true") + # system("sh .download_script.sh 4.7.0 true") installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' diff --git a/macOS/README.md b/macOS/README.md index e108cf69d..1d6b3d833 100644 --- a/macOS/README.md +++ b/macOS/README.md @@ -15,14 +15,10 @@ This section shows you how to prepare, build, and run the sample application. ### Prepare Dependencies -Change directory into **macOS** folder, run following command to install project dependencies, +Change to the `macOS` directory and install the project dependencies with CocoaPods. See the [official CocoaPods guide](https://guides.cocoapods.org/using/getting-started.html) for installation instructions. -use cocoapods - -[install cocoapods](http://t.zoukankan.com/lijiejoy-p-9680485.html) - - -``` +```bash +cd macOS pod install ``` @@ -90,7 +86,7 @@ The extracted bundle is ignored by Git. - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) - You can find full API documentation at [Document Center](https://docs.agora.io/en/) - If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) -- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues) ## License diff --git a/windows/README.md b/windows/README.md index 08a152301..d417f8405 100644 --- a/windows/README.md +++ b/windows/README.md @@ -164,7 +164,7 @@ The `Release` directory is ignored by Git. - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) - You can find full API documentation at [Document Center](https://docs.agora.io/en/) - If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) -- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Broadcasting/issues) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/API-Examples/issues) ## License diff --git a/windows/README.zh.md b/windows/README.zh.md index c6f030769..523840100 100644 --- a/windows/README.zh.md +++ b/windows/README.zh.md @@ -187,7 +187,7 @@ windows/APIExample/Release/beauty_agora/ - 如果你想了解声网的一些社区开发者维护的项目,可以查看[社区](https://github.com/AgoraIO-Community) - 完整的 API 文档见 [文档中心](https://docs.agora.io/cn/) - 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问 -- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/Basic-Video-Broadcasting/issues) +- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/API-Examples/issues) ## 代码许可 From b64b6d0a72730af35c43272a3cee8ea0dedc4f8b Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 8 Sep 2026 10:23:45 +0800 Subject: [PATCH 36/49] chore: update RTC SDK headers for 4.7.0 --- .../src/main/cpp/AgoraRtcKit/AgoraBase.h | 7 +- .../src/main/cpp/AgoraRtcKit/IAgoraLog.h | 3 + .../cpp/AgoraRtcKit/api/cpp/aosl_ref_class.h | 321 +++++++++++++++++- .../src/main/cpp/include/agora/AgoraBase.h | 7 +- .../src/main/cpp/include/agora/IAgoraLog.h | 3 + .../main/cpp/include/agora/IAgoraRtcEngine.h | 48 +-- 6 files changed, 352 insertions(+), 37 deletions(-) diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h index a087b259d..c8903c4f5 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h @@ -1211,9 +1211,10 @@ enum DEGRADATION_PREFERENCE { */ MAINTAIN_AUTO = -1, /** - * 0: Prefers to reduce the video frame rate while maintaining video resolution during video - * encoding under limited bandwidth. This degradation preference is suitable for scenarios where - * video quality is prioritized. + * 0: Deprecated. Use `MAINTAIN_RESOLUTION` instead. During video encoding under limited + * bandwidth, this option has the same behavior as `MAINTAIN_RESOLUTION`: The SDK reduces the video + * frame rate while maintaining video resolution. This degradation preference is suitable for + * scenarios where video quality is prioritized. */ MAINTAIN_QUALITY = 0, /** diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraLog.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraLog.h index 4bc586442..c65b546a0 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraLog.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraLog.h @@ -125,6 +125,9 @@ struct LogConfig { /** * The complete path of the log files. Agora recommends using the default log directory. If you need * to modify the default directory, ensure that the directory you specify exists and is writable. + * The custom path must include the log file name, for example, `/path/to/agorasdk.log`; if you only + * specify a log directory, append a path separator to the end of the directory, and the SDK + * generates `agorasdk.log` in that directory. * The default log directory is: * - Android: /storage/emulated/0/Android/data//files/agorasdk.log. * - iOS: App Sandbox/Library/caches/agorasdk.log. diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ref_class.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ref_class.h index dcd328e18..a29fb948d 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ref_class.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ref_class.h @@ -532,6 +532,28 @@ class aosl_ref_class { { return aosl_mpqp_run_data (qp, dq, ref (), f_name, f, len, data); } + + int exec (aosl_mpqp_t qp, const char *f_name, aosl_mpq_func_argv_t f, uintptr_t argc, ...) + { + va_list args; + int err; + + va_start (args, argc); + err = aosl_mpqp_exec_args (qp, ref (), f_name, f, argc, args); + va_end (args); + + return err; + } + + int exec_args (aosl_mpqp_t qp, const char *f_name, aosl_mpq_func_argv_t f, uintptr_t argc, va_list args) + { + return aosl_mpqp_exec_args (qp, ref (), f_name, f, argc, args); + } + + int exec_argv (aosl_mpqp_t qp, const char *f_name, aosl_mpq_func_argv_t f, uintptr_t argc, uintptr_t *argv) + { + return aosl_mpqp_exec_argv (qp, ref (), f_name, f, argc, argv); + } #endif /* __AOSL_MPQP_H__ */ #endif /* __AOSL_MPQ_H__ */ @@ -702,7 +724,6 @@ class aosl_ref_class { * - error C2672: XXX: no matching overloaded function found * - error C2783: XXX(YYY): could not deduce template argument for '__formal' * So, we use the return type style SFINAE here instead. - * -- Lionfore Hao Apr 15th, 2025 **/ typename std::enable_if()())>::value, int>::type queue (aosl_mpq_t tq, const char *f_name, __mpq_0arg_lambda_t&& task) @@ -723,7 +744,6 @@ class aosl_ref_class { * - error C2672: XXX: no matching overloaded function found * - error C2783: XXX(YYY): could not deduce template argument for '__formal' * So, we use the return type style SFINAE here instead. - * -- Lionfore Hao Apr 15th, 2025 **/ typename std::enable_if()())>::value, int>::type call (aosl_mpq_t q, const char *f_name, __mpq_0arg_lambda_t&& task, void *task_result = NULL) @@ -744,7 +764,6 @@ class aosl_ref_class { * - error C2672: XXX: no matching overloaded function found * - error C2783: XXX(YYY): could not deduce template argument for '__formal' * So, we use the return type style SFINAE here instead. - * -- Lionfore Hao Apr 15th, 2025 **/ typename std::enable_if()())>::value, int>::type run (aosl_mpq_t q, const char *f_name, __mpq_0arg_lambda_t&& task) @@ -765,7 +784,6 @@ class aosl_ref_class { * - error C2672: XXX: no matching overloaded function found * - error C2783: XXX(YYY): could not deduce template argument for '__formal' * So, we use the return type style SFINAE here instead. - * -- Lionfore Hao Apr 15th, 2025 **/ typename std::enable_if()())>::value, int>::type exec (aosl_mpq_t q, const char *f_name, __mpq_0arg_lambda_t&& task) @@ -828,6 +846,22 @@ class aosl_ref_class { return qid; } + /* __mpq_lambda_t: void (const aosl_ts_t &queued_ts, aosl_refobj_t robj) */ + template ()( + std::declval(), + std::declval() + ))>::value, int>::type = 0> + int exec (aosl_mpqp_t qp, const char *f_name, __mpq_lambda_t&& task) + { + __mpq_lambda_t *task_obj = new __mpq_lambda_t (std::move (task)); + int err = aosl_ref_t_oop::exec (qp, f_name, ____mpq_exec_f::type>, 1, task_obj); + if (err < 0) + delete task_obj; + + return err; + } + /* __mpq_0arg_lambda_t: void (void) */ template typename std::enable_if()())>::value, aosl_mpq_t>::type @@ -866,6 +900,26 @@ class aosl_ref_class { return qid; } + + /* __mpq_0arg_lambda_t: void (void) */ + template + /** + * Do not use the template parameter with default value style SFINAE for 0 argument lambda case, + * because the buggy MSVC compiler version 14.25.28610 will report: + * - error C2672: XXX: no matching overloaded function found + * - error C2783: XXX(YYY): could not deduce template argument for '__formal' + * So, we use the return type style SFINAE here instead. + **/ + typename std::enable_if()())>::value, int>::type + exec (aosl_mpqp_t qp, const char *f_name, __mpq_0arg_lambda_t&& task) + { + __mpq_0arg_lambda_t *task_obj = new __mpq_0arg_lambda_t (std::move (task)); + int err = aosl_ref_t_oop::exec (qp, f_name, ____mpq_exec_0arg_f::type>, 1, task_obj); + if (err < 0) + delete task_obj; + + return err; + } #endif /* __AOSL_MPQP_H__ */ /* MPQ with specified ref encapsulations */ @@ -1035,6 +1089,22 @@ class aosl_ref_class { return qid; } + /* __mpq_lambda_t: void (const aosl_ts_t &queued_ts, aosl_refobj_t robj) */ + template ()( + std::declval(), + std::declval() + ))>::value, int>::type = 0> + static int exec (aosl_mpqp_t qp, aosl_ref_t ref, const char *f_name, __mpq_lambda_t&& task) + { + __mpq_lambda_t *task_obj = new __mpq_lambda_t (std::move (task)); + int err = aosl_mpqp_exec (qp, ref, f_name, ____mpq_exec_f::type>, 1, task_obj); + if (err < 0) + delete task_obj; + + return err; + } + /* __mpq_0arg_lambda_t: void (void) */ template static typename std::enable_if()())>::value, aosl_mpq_t>::type @@ -1073,6 +1143,19 @@ class aosl_ref_class { return qid; } + + /* __mpq_0arg_lambda_t: void (void) */ + template + static typename std::enable_if()())>::value, int>::type + exec (aosl_mpqp_t qp, aosl_ref_t ref, const char *f_name, __mpq_0arg_lambda_t&& task) + { + __mpq_0arg_lambda_t *task_obj = new __mpq_0arg_lambda_t (std::move (task)); + int err = aosl_mpqp_exec (qp, ref, f_name, ____mpq_exec_0arg_f::type>, 1, task_obj); + if (err < 0) + delete task_obj; + + return err; + } #endif /* __AOSL_MPQP_H__ */ static void *call_result_var_addr (void) @@ -1101,7 +1184,6 @@ class aosl_ref_class { * done mpq id, due to the task object would be still in use if * the function has a done mpq id when queuing back to the done * mpq. - * -- Lionfore Hao Nov 19th, 2018 **/ delete task_obj; } @@ -1121,7 +1203,6 @@ class aosl_ref_class { * done mpq id, due to the task object would be still in use if * the function has a done mpq id when queuing back to the done * mpq. - * -- Lionfore Hao Nov 19th, 2018 **/ delete task_obj; } @@ -1204,7 +1285,48 @@ class aosl_ref_class { * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found * really, so change it anyway for now. - * -- Lionfore Hao Apr 15th, 2025 + * Returns 1 after invoking the lambda synchronously for an already-completed + * await object, 0 after registering it for future completion, or a negative + * value with aosl_errno set on failure, exactly like aosl_async_await(). + **/ + typename std::enable_if()(std::declval()))>::value, int>::type + await (aosl_await_t awt, const char *f_name, __async_resume_lambda_t&& task, int flags = 0) + { + __async_resume_lambda_t *resume_f = new __async_resume_lambda_t (std::move (task)); + int err = aosl_async_await (awt, flags, ref (), f_name, ____async_resume_f::type>, 1, resume_f); + if (err < 0) + delete resume_f; + + return err; + } + + /* __async_resume_lambda_t: void (int free_only) */ + template + /** + * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but + * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found + * really, so change it anyway for now. + * Returns 1 after invoking the lambda synchronously for an already-completed + * await object, 0 after registering it for future completion, or a negative + * value with aosl_errno set on failure, exactly like aosl_async_await(). + **/ + static typename std::enable_if()(std::declval()))>::value, int>::type + await (aosl_await_t awt, aosl_ref_t ref, const char *f_name, __async_resume_lambda_t&& task, int flags = 0) + { + __async_resume_lambda_t *resume_f = new __async_resume_lambda_t (std::move (task)); + int err = aosl_async_await (awt, flags, ref, f_name, ____async_resume_f::type>, 1, resume_f); + if (err < 0) + delete resume_f; + + return err; + } + + /* __async_resume_lambda_t: void (int free_only) */ + template + /** + * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but + * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found + * really, so change it anyway for now. **/ typename std::enable_if()(std::declval()))>::value, int>::type resume (aosl_stack_id_t stack_id, const char *f_name, __async_resume_lambda_t&& task) @@ -1223,7 +1345,6 @@ class aosl_ref_class { * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found * really, so change it anyway for now. - * -- Lionfore Hao Apr 15th, 2025 **/ static typename std::enable_if()(std::declval()))>::value, int>::type resume (aosl_stack_id_t stack_id, aosl_ref_t ref, const char *f_name, __async_resume_lambda_t&& task) @@ -1243,7 +1364,6 @@ class aosl_ref_class { * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found * really, so change it anyway for now. - * -- Lionfore Hao Apr 15th, 2025 **/ static typename std::enable_if()(std::declval()))>::value, void>::type ____async_resume_f (int free_only, uintptr_t argc, uintptr_t argv []) @@ -1870,6 +1990,28 @@ class aosl_ref_class { { return refoop->run_data (qp, dq, f_name, f, len, data); } + + int exec (aosl_mpqp_t qp, const char *f_name, aosl_mpq_func_argv_t f, uintptr_t argc, ...) + { + va_list args; + int err; + + va_start (args, argc); + err = refoop->exec_args (qp, f_name, f, argc, args); + va_end (args); + + return err; + } + + int exec_args (aosl_mpqp_t qp, const char *f_name, aosl_mpq_func_argv_t f, uintptr_t argc, va_list args) + { + return refoop->exec_args (qp, f_name, f, argc, args); + } + + int exec_argv (aosl_mpqp_t qp, const char *f_name, aosl_mpq_func_argv_t f, uintptr_t argc, uintptr_t *argv) + { + return refoop->exec_argv (qp, f_name, f, argc, argv); + } #endif /* __AOSL_MPQP_H__ */ #endif /* __AOSL_MPQ_H__ */ @@ -2059,6 +2201,54 @@ class aosl_ref_class { return refoop->exec (q, f_name, std::move (task)); } +#if (__cplusplus >= 202002) || (defined (_MSVC_LANG) && _MSVC_LANG >= 202002) + private: + template + static auto ____co_async (bool doneback, __mpq_co_async_lambda_t&& task, + __exec_issue_lambda_t&& exec_issue, __queue_issue_lambda_t&& queue_issue); + + public: + /** + * Submit a zero-argument callable through this object's ref and return a + * typed C++20 awaiter. The callable's return value becomes the value of + * co_await; a void callable produces an awaiter without result storage. + * + * @q selects the target MPQ. @doneback requests the coroutine-aware exec and + * resume path and defaults to true. That path is used only when the caller is + * itself running on an MPQ; execution is immediate when q is that MPQ and + * otherwise the suspended coroutine resumes on its issuing MPQ after the exec + * operation and asynchronous work attached to its resume domain finish. + * + * When the caller is not an MPQ thread, or when @doneback is false, co_async() + * instead creates an independent multi-thread await object and submits the + * callable with queue(), without a done MPQ. Completing the queue callback + * completes that await object synchronously. A coroutine that has already + * suspended therefore resumes in q's MPQ context. If completion occurs before + * await_suspend(), await_ready() instead continues inline in the issuing thread. + * Setting @doneback to false selects this completion path; it does not suppress + * the continuation or guarantee that a context switch will occur. + * + * The queue callback and returned awaiter own independent references to the + * multi-thread await object, so the callback may finish before co_async() + * returns. A synchronous queue submission failure completes the await object + * in free-only mode. Arguments should be carried explicitly by the callable, + * normally through lambda captures. + * + * The callable executes only in the normal exec phase. If this ref enters the + * free-only path before execution, including on the queue path, a non-void + * callable produces no value and its await_resume() aborts; that path cannot + * be converted into a value by the callable because the callable is skipped. + * Errors detected during normal execution can instead be represented by + * returning an error-bearing value type. The void specialization carries no + * value and therefore does not distinguish normal execution from free-only + * cleanup. Include aosl_async_class.h to use this C++20 API. + */ + template + auto co_async (aosl_mpq_t q, const char *f_name, + __mpq_co_async_lambda_t&& task, bool doneback = true); +#endif /* C++20 */ + #ifdef __AOSL_MPQP_H__ /* MPQP encapsulations */ /* __mpq_lambda_t: void (const aosl_ts_t &queued_ts, aosl_refobj_t robj) */ @@ -2094,6 +2284,17 @@ class aosl_ref_class { return refoop->run (qp, f_name, std::move (task)); } + /* __mpq_lambda_t: void (const aosl_ts_t &queued_ts, aosl_refobj_t robj) */ + template ()( + std::declval(), + std::declval() + ))>::value, int>::type = 0> + int exec (aosl_mpqp_t qp, const char *f_name, __mpq_lambda_t&& task) + { + return refoop->exec (qp, f_name, std::move (task)); + } + /* __mpq_0arg_lambda_t: void (void) */ template typename std::enable_if()())>::value, aosl_mpq_t>::type @@ -2117,6 +2318,31 @@ class aosl_ref_class { { return refoop->run (qp, f_name, std::move (task)); } + + /* __mpq_0arg_lambda_t: void (void) */ + template + typename std::enable_if()())>::value, int>::type + exec (aosl_mpqp_t qp, const char *f_name, __mpq_0arg_lambda_t&& task) + { + return refoop->exec (qp, f_name, std::move (task)); + } + +#if (__cplusplus >= 202002) || (defined (_MSVC_LANG) && _MSVC_LANG >= 202002) + /** + * Pool-targeted form of co_async(). The pool selects the concrete execution + * MPQ. With @doneback enabled from an MPQ thread, the exec/resume path runs the + * callable on that selected MPQ and returns a suspended continuation to the + * issuing MPQ after the connected resume domain completes. From a non-MPQ + * thread, or with @doneback disabled, the queue/manual-await path runs the + * callable on the selected MPQ and resumes an already-suspended continuation + * there. Completion before await_suspend() remains inline on the issuing thread. + * All other completion, ownership and free-only rules are the same as the + * aosl_mpq_t overload. + */ + template + auto co_async (aosl_mpqp_t qp, const char *f_name, + __mpqp_co_async_lambda_t&& task, bool doneback = true); +#endif /* C++20 */ #endif /* __AOSL_MPQP_H__ */ /* MPQ with specified ref encapsulations */ @@ -2196,6 +2422,19 @@ class aosl_ref_class { return aosl_ref_t_oop::exec (q, ref, f_name, std::move (task)); } +#if (__cplusplus >= 202002) || (defined (_MSVC_LANG) && _MSVC_LANG >= 202002) + /** + * Static form of co_async() for an explicitly supplied ref. AOSL_REF_INVALID + * disables ref-based free-only protection; otherwise destruction of ref before + * normal execution skips the zero-argument callable. The @doneback parameter, + * result, suspension, failure and ownership semantics are identical to the + * member co_async(). Include aosl_async_class.h to use this C++20 API. + */ + template + static auto co_async (aosl_mpq_t q, aosl_ref_t ref, const char *f_name, + __mpq_co_async_lambda_t&& task, bool doneback = true); +#endif /* C++20 */ + #ifdef __AOSL_MPQP_H__ /* MPQP with specified ref encapsulations */ /* __mpq_lambda_t: void (const aosl_ts_t &queued_ts, aosl_refobj_t robj) */ @@ -2231,6 +2470,17 @@ class aosl_ref_class { return aosl_ref_t_oop::run (qp, ref, f_name, std::move (task)); } + /* __mpq_lambda_t: void (const aosl_ts_t &queued_ts, aosl_refobj_t robj) */ + template ()( + std::declval(), + std::declval() + ))>::value, int>::type = 0> + static int exec (aosl_mpqp_t qp, aosl_ref_t ref, const char *f_name, __mpq_lambda_t&& task) + { + return aosl_ref_t_oop::exec (qp, ref, f_name, std::move (task)); + } + /* __mpq_0arg_lambda_t: void (void) */ template static typename std::enable_if()())>::value, aosl_mpq_t>::type @@ -2254,6 +2504,25 @@ class aosl_ref_class { { return aosl_ref_t_oop::run (qp, ref, f_name, std::move (task)); } + + /* __mpq_0arg_lambda_t: void (void) */ + template + static typename std::enable_if()())>::value, int>::type + exec (aosl_mpqp_t qp, aosl_ref_t ref, const char *f_name, __mpq_0arg_lambda_t&& task) + { + return aosl_ref_t_oop::exec (qp, ref, f_name, std::move (task)); + } + +#if (__cplusplus >= 202002) || (defined (_MSVC_LANG) && _MSVC_LANG >= 202002) + /** + * Static pool-targeted co_async() using an explicitly supplied ref. Pool + * selection and @doneback semantics match the member aosl_mpqp_t overload; + * AOSL_REF_INVALID disables ref-based free-only protection. + */ + template + static auto co_async (aosl_mpqp_t qp, aosl_ref_t ref, const char *f_name, + __mpqp_co_async_lambda_t&& task, bool doneback = true); +#endif /* C++20 */ #endif /* __AOSL_MPQP_H__ */ #endif /* __AOSL_MPQ_H__ */ @@ -2287,7 +2556,38 @@ class aosl_ref_class { * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found * really, so change it anyway for now. - * -- Lionfore Hao Apr 15th, 2025 + * Returns 1 after invoking the lambda synchronously for an already-completed + * await object, 0 after registering it for future completion, or a negative + * value with aosl_errno set on failure, exactly like aosl_async_await(). + **/ + typename std::enable_if()(std::declval()))>::value, int>::type + await (aosl_await_t awt, const char *f_name, __async_resume_lambda_t&& task, int flags = 0) + { + return refoop->await (awt, f_name, std::move (task), flags); + } + + /* __async_resume_lambda_t: void (int free_only) */ + template + /** + * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but + * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found + * really, so change it anyway for now. + * Returns 1 after invoking the lambda synchronously for an already-completed + * await object, 0 after registering it for future completion, or a negative + * value with aosl_errno set on failure, exactly like aosl_async_await(). + **/ + static typename std::enable_if()(std::declval()))>::value, int>::type + await (aosl_await_t awt, aosl_ref_t ref, const char *f_name, __async_resume_lambda_t&& task, int flags = 0) + { + return aosl_ref_t_oop::await (awt, ref, f_name, std::move (task), flags); + } + + /* __async_resume_lambda_t: void (int free_only) */ + template + /** + * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but + * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found + * really, so change it anyway for now. **/ typename std::enable_if()(std::declval()))>::value, int>::type resume (aosl_stack_id_t stack_id, const char *f_name, __async_resume_lambda_t&& task) @@ -2301,7 +2601,6 @@ class aosl_ref_class { * Do not know why this function needs to be changed to the return type style SFINAE, the lambda has one argument, but * the buggy MSVC compiler version 14.25.28610 also reports the error C2672: XXX: no matching overloaded function found * really, so change it anyway for now. - * -- Lionfore Hao Apr 15th, 2025 **/ static typename std::enable_if()(std::declval()))>::value, int>::type resume (aosl_stack_id_t stack_id, aosl_ref_t ref, const char *f_name, __async_resume_lambda_t&& task) diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraBase.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraBase.h index a087b259d..c8903c4f5 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraBase.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraBase.h @@ -1211,9 +1211,10 @@ enum DEGRADATION_PREFERENCE { */ MAINTAIN_AUTO = -1, /** - * 0: Prefers to reduce the video frame rate while maintaining video resolution during video - * encoding under limited bandwidth. This degradation preference is suitable for scenarios where - * video quality is prioritized. + * 0: Deprecated. Use `MAINTAIN_RESOLUTION` instead. During video encoding under limited + * bandwidth, this option has the same behavior as `MAINTAIN_RESOLUTION`: The SDK reduces the video + * frame rate while maintaining video resolution. This degradation preference is suitable for + * scenarios where video quality is prioritized. */ MAINTAIN_QUALITY = 0, /** diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraLog.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraLog.h index 4bc586442..c65b546a0 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraLog.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraLog.h @@ -125,6 +125,9 @@ struct LogConfig { /** * The complete path of the log files. Agora recommends using the default log directory. If you need * to modify the default directory, ensure that the directory you specify exists and is writable. + * The custom path must include the log file name, for example, `/path/to/agorasdk.log`; if you only + * specify a log directory, append a path separator to the end of the directory, and the SDK + * generates `agorasdk.log` in that directory. * The default log directory is: * - Android: /storage/emulated/0/Android/data//files/agorasdk.log. * - iOS: App Sandbox/Library/caches/agorasdk.log. diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngine.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngine.h index 1ffa8b060..d32b28401 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngine.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngine.h @@ -4304,7 +4304,8 @@ class IMetadataObserver { * This callback is triggered when the SDK is ready to send metadata. * * @note Ensure that the size of the metadata does not exceed the value set in the - * `getMaxMetadataSize` callback. + * `getMaxMetadataSize` callback. If the size exceeds this value, the SDK truncates the excess + * part. * * @param source_type Video data type. See `VIDEO_SOURCE_TYPE`. * @param metadata The metadata that the user wants to send. See `Metadata`. @@ -9922,13 +9923,16 @@ class IRtcEngine : public agora::base::IEngineBase { * Call timing: You can call this method either before or after joining the channel, with the * following differences: * - Call this method before joining a channel, and then call `joinChannel(const char* token, const - * char* channelId, uid_t uid, const ChannelMediaOptions& options)` to join a channel - * and set `publishScreenTrack` or `publishSecondaryScreenTrack` to `true` to start screen sharing. - * - Call this method after joining a channel, and then call `updateChannelMediaOptions` to join a - * channel and set `publishScreenTrack` or `publishSecondaryScreenTrack` to `true` to start screen - * sharing. + * char* channelId, uid_t uid, const ChannelMediaOptions& options)` to join a channel and set + * `publishScreenTrack` to `true` to start screen sharing. + * - Call this method after joining a channel, and then call `updateChannelMediaOptions` and set + * `publishScreenTrack` to `true` to start screen sharing. * - * @note This method is for Windows and macOS only. + * @note + * - This method is for Windows and macOS only. + * - This method creates only the primary screen video track. To specify another screen video source + * by `sourceType`, use `startScreenCapture(VIDEO_SOURCE_TYPE sourceType, const + * ScreenCaptureConfiguration& config)` instead. * * @param displayId The display ID of the screen to be shared.Note: For the Windows platform, if you * need to simultaneously share two screens (main screen and secondary screen), you can set @@ -9968,11 +9972,10 @@ class IRtcEngine : public agora::base::IEngineBase { * You can call this method either before or after joining the channel, with the following * differences: * - Call this method before joining a channel, and then call `joinChannel(const char* token, const - * char* channelId, uid_t uid, const ChannelMediaOptions& options)` to join a channel - * and set `publishScreenTrack` or `publishSecondaryScreenTrack` to `true` to start screen sharing. - * - Call this method after joining a channel, and then call `updateChannelMediaOptions` to join a - * channel and set `publishScreenTrack` or `publishSecondaryScreenTrack` to `true` to start screen - * sharing. + * char* channelId, uid_t uid, const ChannelMediaOptions& options)` to join a channel and set + * `publishScreenTrack` to `true` to start screen sharing. + * - Call this method after joining a channel, and then call `updateChannelMediaOptions` and set + * `publishScreenTrack` to `true` to start screen sharing. * * @note This method applies to Windows only. * @@ -10083,10 +10086,9 @@ class IRtcEngine : public agora::base::IEngineBase { * following differences: * - Call this method before joining a channel, and then call `joinChannel(const char* token, const * char* channelId, uid_t uid, const ChannelMediaOptions& options)` to join a channel - * and set `publishScreenTrack` or `publishSecondaryScreenTrack` to `true` to start screen sharing. - * - Call this method after joining a channel, and then call `updateChannelMediaOptions` to join a - * channel and set `publishScreenTrack` or `publishSecondaryScreenTrack` to `true` to start screen - * sharing. + * and set `publishScreenTrack` to `true` to start screen sharing. + * - Call this method after joining a channel, and then call `updateChannelMediaOptions` and set + * `publishScreenTrack` to `true` to start screen sharing. * * @note * The window sharing feature of the Agora SDK relies on WGC (Windows Graphics Capture) or GDI @@ -10095,6 +10097,9 @@ class IRtcEngine : public agora::base::IEngineBase { * start window sharing on a device with a system earlier than Windows 10 2004. See * `ScreenCaptureParameters`. * This method applies to the macOS and Windows only. + * This method creates only the primary screen video track. To specify another screen video source + * by `sourceType`, use `startScreenCapture(VIDEO_SOURCE_TYPE sourceType, const + * ScreenCaptureConfiguration& config)` instead. * * @param windowId The ID of the window to be shared. * @param regionRect (Optional) Sets the relative location of the region to the screen. If you do @@ -10804,10 +10809,13 @@ class IRtcEngine : public agora::base::IEngineBase { * Call timing: You can call this method either before or after joining the channel, with the * following differences: * - Call this method first and then call `joinChannel(const char* token, const char* channelId, - * uid_t uid, const ChannelMediaOptions& options)` to join channel and set - * `publishScreenCaptureVideo` to `true` to start screen sharing. - * - Call this method after joining a channel, then call `updateChannelMediaOptions` and set - * `publishScreenCaptureVideo` to `true` to start screen sharing. + * uid_t uid, const ChannelMediaOptions& options)` to join a channel, or call this method after + * joining a channel and then call `updateChannelMediaOptions`. + * - Set the publishing option corresponding to `sourceType` to `true` to start screen sharing: + * - `VIDEO_SOURCE_SCREEN` or `VIDEO_SOURCE_SCREEN_PRIMARY`: `publishScreenTrack` + * - `VIDEO_SOURCE_SCREEN_SECONDARY`: `publishSecondaryScreenTrack` + * - `VIDEO_SOURCE_SCREEN_THIRD`: `publishThirdScreenTrack` + * - `VIDEO_SOURCE_SCREEN_FOURTH`: `publishFourthScreenTrack` * * @note * - If you start screen capture by calling this method, you need to call From 989d3eb4ac580b71c72019f20f8b3d411356bea7 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 8 Sep 2026 10:43:28 +0800 Subject: [PATCH 37/49] fix(android): correct FFmpeg library name --- .../APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt b/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt index 1d0b7d58c..22785cf99 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/CMakeLists.txt @@ -11,7 +11,7 @@ project(agora-stream-encrypt) set(agora-lib-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora-rtc-sdk.so) link_libraries(${agora-lib-so}) -set(agora-ffmpeg-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora-ffmpeg.so) +set(agora-ffmpeg-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora_ffmpeg.so) link_libraries(${agora-ffmpeg-so}) set(agora-soundtouch-so ${PROJECT_SOURCE_DIR}/../agoraLibs/${CMAKE_ANDROID_ARCH_ABI}/libagora-soundtouch.so) From 282815b584d05eadca89bdfac88965d236580942 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 8 Sep 2026 12:02:25 +0800 Subject: [PATCH 38/49] ci: align Agora APIExample package contents --- .github/ci/build/build_ios.sh | 14 ++++++----- .github/ci/build/build_mac.sh | 14 ++++++----- .github/ci/build/build_windows.bat | 37 ++++++++++++++++++------------ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/.github/ci/build/build_ios.sh b/.github/ci/build/build_ios.sh index 234d96c30..604a31e78 100644 --- a/.github/ci/build/build_ios.sh +++ b/.github/ci/build/build_ios.sh @@ -127,13 +127,15 @@ fi python3 ./.github/ci/build/modify_podfile.py ./$unzip_name/samples/${ios_direction}/Podfile $sdk_url_flag || exit 1 -echo "start compress" -7za a -tzip result.zip -r $unzip_name > log.txt -echo "start move to" -echo $WORKSPACE/Agora_with${ios_direction}_${BUILD_NUMBER}_$zip_name -mv result.zip $WORKSPACE/Agora_with${ios_direction}_${BUILD_NUMBER}_$zip_name +if [ "$compress_apiexample" != true ]; then + echo "start compress" + 7za a -tzip result.zip -r $unzip_name > log.txt + echo "start move to" + echo $WORKSPACE/Agora_with${ios_direction}_${BUILD_NUMBER}_$zip_name + mv result.zip $WORKSPACE/Agora_with${ios_direction}_${BUILD_NUMBER}_$zip_name +fi -if [ $compress_apiexample = true ]; then +if [ "$compress_apiexample" = true ]; then echo "Using version for package: $API_EXAMPLES_SDK_VERSION" cp -rf ./iOS/${ios_direction} $global_dir/ diff --git a/.github/ci/build/build_mac.sh b/.github/ci/build/build_mac.sh index fc4b3f56a..e9ef06451 100644 --- a/.github/ci/build/build_mac.sh +++ b/.github/ci/build/build_mac.sh @@ -119,13 +119,15 @@ fi python3 ./.github/ci/build/modify_podfile.py ./$unzip_name/samples/APIExample/Podfile $sdk_url_flag -echo "start compress" -7za a -tzip result.zip -r $unzip_name > log.txt -echo "start move to" -echo $WORKSPACE/Agora_APIExample_mac_${BUILD_NUMBER}_$zip_name -mv result.zip $WORKSPACE/Agora_APIExample_mac_${BUILD_NUMBER}_$zip_name +if [ "$compress_apiexample" != true ]; then + echo "start compress" + 7za a -tzip result.zip -r $unzip_name > log.txt + echo "start move to" + echo $WORKSPACE/Agora_APIExample_mac_${BUILD_NUMBER}_$zip_name + mv result.zip $WORKSPACE/Agora_APIExample_mac_${BUILD_NUMBER}_$zip_name +fi -if [ $compress_apiexample = true ]; then +if [ "$compress_apiexample" = true ]; then # Use BRANCH_VERSION for the package name (already validated to match SDK version) sdk_version="${BRANCH_VERSION}" echo "Using version for package: $sdk_version" diff --git a/.github/ci/build/build_windows.bat b/.github/ci/build/build_windows.bat index e6de61237..ce71979fe 100644 --- a/.github/ci/build/build_windows.bat +++ b/.github/ci/build/build_windows.bat @@ -80,33 +80,40 @@ echo compile_project: %compile_project% REM Package APIExample code (only when compress_apiexample=true) REM Run before compile so package content is not affected by compile -set result_zip=APIExample_result.zip -set des_path=%WORKSPACE%\Agora_Native_SDK_for_Windows_v%SDK_VER%_APIExample_%BUILD_NUMBER%.zip +set "result_zip=APIExample_result.zip" +set "des_path=%WORKSPACE%\Agora_Native_SDK_for_Windows_v%SDK_VER%_APIExample_%BUILD_NUMBER%.zip" if "%compress_apiexample%"=="true" ( echo "Packaging APIExample code..." - - REM Compress windows\APIExample (code only) to zip - echo "Compressing APIExample code package..." - del /F /Q %result_zip% 2>nul - 7z a -tzip %result_zip% -r windows\APIExample + + REM Archive tracked source so reused workspaces cannot leak build output or + REM local configuration. Dependencies are intentionally excluded for Agora. + del /F /Q "%result_zip%" 2>nul + git archive --format=zip --output="%result_zip%" HEAD windows/APIExample + if errorlevel 1 ( + echo git archive failed! + exit /b 1 + ) + + REM Preserve an SDK URL injected by this job. + 7z u -tzip "%result_zip%" -r windows\APIExample\install.ps1 >nul if errorlevel 1 ( - echo 7z compression failed! + echo Failed to update install.ps1 in APIExample package! exit /b 1 ) - + REM Copy to WORKSPACE with new naming format echo "Copying %result_zip% to %des_path%" - copy %result_zip% %des_path% + copy /Y "%result_zip%" "%des_path%" if errorlevel 1 ( echo copy failed! exit /b 1 ) - + REM Clean up temporary zip in repo root - del /F %result_zip% - + del /F /Q "%result_zip%" + echo "Complete: APIExample code package created" - dir %WORKSPACE%\ + dir "%WORKSPACE%\" ) else ( echo "Skipping APIExample code packaging (compress_apiexample=false)" ) @@ -123,4 +130,4 @@ if "%compile_project%"=="true" ( cd ..\.. ) else ( echo "Skipping project compilation (compile_project=false)" -) \ No newline at end of file +) From da9c615fb3046686059a72da2fb0b0e3f1d58867 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 8 Sep 2026 14:44:00 +0800 Subject: [PATCH 39/49] chore: streamline repository git hooks --- .git-hooks/check-commit-message.sh | 58 +++++--- .git-hooks/install-hooks.sh | 212 +++++++++------------------- .git-hooks/post-commit | 23 --- .git-hooks/pre-commit | 30 ---- .git-hooks/pre-commit-dispatcher.sh | 38 +++++ .git-hooks/run-gitleaks.sh | 45 +++--- .gitleaks.toml | 27 +--- .pre-commit-config.yaml | 10 +- AGENTS.md | 6 +- HOOKS-GUIDE.md | 80 +++++------ 10 files changed, 205 insertions(+), 324 deletions(-) delete mode 100755 .git-hooks/post-commit delete mode 100755 .git-hooks/pre-commit create mode 100755 .git-hooks/pre-commit-dispatcher.sh diff --git a/.git-hooks/check-commit-message.sh b/.git-hooks/check-commit-message.sh index 8d0f57a6d..69fc54cca 100755 --- a/.git-hooks/check-commit-message.sh +++ b/.git-hooks/check-commit-message.sh @@ -1,22 +1,38 @@ -#!/bin/bash - -commit_msg_file=$1 -commit_msg=$(cat "$commit_msg_file") - - -if perl -e ' - binmode(STDIN, ":utf8"); - $/ = undef; - $text = <>; - if ($text =~ /[\x{4e00}-\x{9fff}]/) { - exit(1); - } else { - exit(0); - }' < "$commit_msg_file" -then - exit 0 -else - echo "Error: Commit message contains Chinese characters." - echo "Please use English only in commit messages." +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 1 || ! -f "$1" ]]; then + echo "Error: expected a commit message file." >&2 exit 1 -fi \ No newline at end of file +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required to validate commit messages." >&2 + exit 1 +fi + +python3 - "$1" <<'PY' +import pathlib +import sys + + +han_ranges = ( + (0x3400, 0x4DBF), + (0x4E00, 0x9FFF), + (0xF900, 0xFAFF), + (0x20000, 0x2EBEF), + (0x30000, 0x323AF), +) + +try: + message = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") +except (OSError, UnicodeError) as error: + print(f"Error: cannot read commit message as UTF-8: {error}", file=sys.stderr) + raise SystemExit(1) + +if any(start <= ord(character) <= end for character in message for start, end in han_ranges): + print("Error: commit message contains Chinese characters.", file=sys.stderr) + print("Please use English only in commit messages.", file=sys.stderr) + raise SystemExit(1) +PY diff --git a/.git-hooks/install-hooks.sh b/.git-hooks/install-hooks.sh index 184c45018..d1490b5ef 100755 --- a/.git-hooks/install-hooks.sh +++ b/.git-hooks/install-hooks.sh @@ -1,165 +1,87 @@ -#!/bin/bash - -# Functions for colored text output -print_green() { - echo -e "\033[0;32m$1\033[0m" -} - -print_yellow() { - echo -e "\033[0;33m$1\033[0m" -} - -print_red() { - echo -e "\033[0;31m$1\033[0m" -} - -# Function to add executable permissions -ensure_executable() { - if [ -f "$1" ] && [ ! -x "$1" ]; then - chmod +x "$1" - print_green "Added executable permission to $1" - fi -} - -# Ensure script runs from project root directory -if [ ! -d ".git" ]; then - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - cd "$(dirname "$SCRIPT_DIR")" || { print_red "Cannot find project root directory"; exit 1; } - - if [ ! -d ".git" ]; then - print_red "Please run this script from the project root directory" - exit 1 - fi -fi +#!/usr/bin/env bash -# Check if pre-commit is installed -if ! command -v pre-commit &> /dev/null; then - print_yellow "pre-commit not found, attempting to install..." - if command -v pip3 &> /dev/null; then - pip3 install pre-commit - elif command -v pip &> /dev/null; then - pip install pre-commit - else - print_red "pip not found, please install Python and pip first, then run this script again" - exit 1 - fi - - if [ $? -ne 0 ]; then - print_red "Failed to install pre-commit, please install manually: pip install pre-commit" - exit 1 - fi - print_green "pre-commit installed successfully!" -else - print_green "pre-commit is already installed!" -fi +set -euo pipefail -# Check if gitleaks is installed (required) -if ! command -v gitleaks &> /dev/null; then - print_red "❌ gitleaks not found - this is a required security tool!" - print_yellow "Installation guide: https://github.com/gitleaks/gitleaks#installing" - - # Attempt automatic installation (based on OS) - if [[ "$OSTYPE" == "darwin"* ]]; then - print_yellow "Detected macOS, attempting to install gitleaks via Homebrew..." - if command -v brew &> /dev/null; then - brew install gitleaks - if [ $? -eq 0 ]; then - print_green "gitleaks installed successfully!" - else - print_red "Cannot automatically install gitleaks, please install manually" - exit 1 - fi - else - print_red "Homebrew not found, please install Homebrew or install gitleaks manually" - exit 1 - fi - else - print_red "Please install gitleaks manually and try again" - exit 1 - fi -else - print_green "gitleaks is installed!" -fi +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Check required files and directories -if [ ! -d ".git-hooks" ]; then - print_red "Cannot find .git-hooks directory, please ensure you're in the correct project" +if ! repo_root="$(git -C "$script_dir" rev-parse --show-toplevel 2>/dev/null)"; then + echo "Error: .git-hooks must be inside a Git repository." >&2 exit 1 fi -if [ ! -f ".gitleaks.toml" ]; then - print_red "Cannot find .gitleaks.toml configuration file, please ensure it exists" - exit 1 +cd "$repo_root" + +has_error=false + +hooks_path="$(git config --path --get core.hooksPath || true)" + +if ! command -v pre-commit >/dev/null 2>&1; then + echo "Error: pre-commit is required. Install it from https://pre-commit.com/#install" >&2 + has_error=true fi -if [ ! -f ".git-hooks/check-commit-message.sh" ]; then - print_red "Cannot find .git-hooks/check-commit-message.sh file, please ensure it exists" - exit 1 +if ! command -v gitleaks >/dev/null 2>&1; then + echo "Error: gitleaks is required. Install it from https://github.com/gitleaks/gitleaks#installing" >&2 + has_error=true fi -# Ensure all scripts have executable permissions -print_yellow "Granting executable permissions to hook scripts..." -ensure_executable ".git-hooks/check-commit-message.sh" -ensure_executable ".git-hooks/post-commit" -ensure_executable ".git-hooks/pre-commit" - -# Install pre-commit hook -print_yellow "Installing pre-commit hook..." -pre-commit install -if [ $? -ne 0 ]; then - print_red "Failed to install pre-commit hook!" - exit 1 +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 is required to validate commit messages." >&2 + has_error=true fi -print_green "pre-commit hook installed successfully!" -# Install commit-msg hook -print_yellow "Installing commit-msg hook..." -pre-commit install --hook-type commit-msg -if [ $? -ne 0 ]; then - print_red "Failed to install commit-msg hook!" +required_files=( + ".pre-commit-config.yaml" + ".gitleaks.toml" + ".git-hooks/run-gitleaks.sh" + ".git-hooks/check-commit-message.sh" + ".git-hooks/pre-commit-dispatcher.sh" +) + +for required_file in "${required_files[@]}"; do + if [[ ! -f "$required_file" ]]; then + echo "Error: required file not found: $required_file" >&2 + has_error=true + fi +done + +if [[ "$has_error" == true ]]; then exit 1 fi -print_green "pre-commit commit-msg hook installed successfully!" - -# Copy and set up custom hooks -print_yellow "Setting up custom hooks..." -# Copy commit-msg hook -cp .git-hooks/check-commit-message.sh .git/hooks/commit-msg -chmod +x .git/hooks/commit-msg - -# Copy post-commit hook (if exists) -if [ -f ".git-hooks/post-commit" ]; then - cp .git-hooks/post-commit .git/hooks/post-commit - chmod +x .git/hooks/post-commit -fi -# Copy pre-commit hook (if exists) -if [ -f ".git-hooks/pre-commit" ]; then - # Backup pre-commit hook - if [ -f ".git/hooks/pre-commit" ]; then - cp .git/hooks/pre-commit .git/hooks/pre-commit.bak +if [[ -z "$hooks_path" ]]; then + pre-commit install --hook-type pre-commit --hook-type commit-msg +else + if [[ "$hooks_path" == /* ]]; then + hooks_dir="$hooks_path" + else + hooks_dir="$repo_root/$hooks_path" fi - - cp .git-hooks/pre-commit .git/hooks/pre-commit.custom - chmod +x .git/hooks/pre-commit.custom - - # Add custom pre-commit to existing hook chain - if [ -f ".git/hooks/pre-commit" ]; then - HOOK_CONTENT=$(cat .git/hooks/pre-commit) - if ! grep -q "pre-commit.custom" .git/hooks/pre-commit; then - echo -e "\n# Run custom pre-commit hook\n.git/hooks/pre-commit.custom || exit 1" >> .git/hooks/pre-commit - chmod +x .git/hooks/pre-commit + + dispatcher_source=".git-hooks/pre-commit-dispatcher.sh" + dispatcher_marker="api-examples-pre-commit-dispatcher" + + for hook_type in pre-commit commit-msg; do + hook_target="$hooks_dir/$hook_type" + if [[ -e "$hook_target" || -L "$hook_target" ]]; then + if ! grep -Fq "$dispatcher_marker" "$hook_target" 2>/dev/null; then + echo "Error: refusing to replace existing hook: $hook_target" >&2 + echo "Make that hook invoke pre-commit, then run this script again." >&2 + has_error=true + fi fi - else - echo -e "#!/bin/bash\n\n# Run custom pre-commit hook\n.git/hooks/pre-commit.custom" > .git/hooks/pre-commit - chmod +x .git/hooks/pre-commit + done + + if [[ "$has_error" == true ]]; then + exit 1 fi -fi -pre-commit clean && pre-commit install && pre-commit install --hook-type commit-msg + mkdir -p "$hooks_dir" + install -m 0755 "$dispatcher_source" "$hooks_dir/pre-commit" + install -m 0755 "$dispatcher_source" "$hooks_dir/commit-msg" + echo "Installed pre-commit dispatchers in configured hook path: $hooks_dir" +fi -print_green "================================================================" -print_green "🎉 Git hooks setup complete! Your repository now has:" -print_green " - Sensitive information leak detection using gitleaks" -print_green " - Chinese character detection in commit messages" -print_green "================================================================" +echo "Git hooks installed successfully:" +echo " - staged changes are scanned for sensitive information" +echo " - commit messages containing Chinese characters are rejected" diff --git a/.git-hooks/post-commit b/.git-hooks/post-commit deleted file mode 100755 index 1feac10e8..000000000 --- a/.git-hooks/post-commit +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Check if required hooks are installed -if [ ! -f ".git/hooks/commit-msg" ] || [ ! -x ".git/hooks/commit-msg" ]; then - echo "============================================================" - echo "⚠️ Note: Git hooks for checking commit messages are not installed." - echo "Please run the following commands to install:" - echo "" - echo " 1. Install pre-commit:" - echo " pip install pre-commit" - echo "" - echo " 2. Install hooks:" - echo " .git-hooks/install-hooks.sh" - echo "" - echo "These hooks will help detect sensitive information leaks and" - echo "Chinese characters in commit messages." - echo "============================================================" -fi - -# Ensure the hook itself is executable -if [ -f ".git-hooks/check-commit-message.sh" ] && [ ! -x ".git-hooks/check-commit-message.sh" ]; then - chmod +x .git-hooks/check-commit-message.sh -fi \ No newline at end of file diff --git a/.git-hooks/pre-commit b/.git-hooks/pre-commit deleted file mode 100755 index e3ccc78e3..000000000 --- a/.git-hooks/pre-commit +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -# Check if gitleaks is configured -if ! command -v gitleaks &> /dev/null; then - echo "============================================================" - echo "❌ ERROR: Gitleaks not detected." - echo "This is a required tool to prevent sensitive information leaks." - echo "" - echo "Please install gitleaks first:" - echo " macOS: brew install gitleaks" - echo " Other: https://github.com/gitleaks/gitleaks#installing" - echo "" - echo "After installation, run: ./.git-hooks/install-hooks.sh" - echo "============================================================" - exit 1 -fi - -# Check for sensitive information -if [ -f ".gitleaks.toml" ]; then - gitleaks detect --source . --config .gitleaks.toml - if [ $? -ne 0 ]; then - echo "❌ Gitleaks detected sensitive information. Commit rejected." - echo "Please review the output above and remove sensitive information." - exit 1 - fi -else - echo "⚠️ No .gitleaks.toml configuration file found, skipping sensitive information check." -fi - -exit 0 \ No newline at end of file diff --git a/.git-hooks/pre-commit-dispatcher.sh b/.git-hooks/pre-commit-dispatcher.sh new file mode 100755 index 000000000..70f8fcae6 --- /dev/null +++ b/.git-hooks/pre-commit-dispatcher.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# api-examples-pre-commit-dispatcher + +set -euo pipefail + +if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + exit 0 +fi + +config_file="$repo_root/.pre-commit-config.yaml" +if [[ ! -f "$config_file" ]]; then + exit 0 +fi + +if ! command -v pre-commit >/dev/null 2>&1; then + echo "Error: pre-commit is required by $config_file" >&2 + echo "Install it from https://pre-commit.com/#install" >&2 + exit 1 +fi + +cd "$repo_root" + +case "$(basename "$0")" in + pre-commit) + exec pre-commit run --config "$config_file" --hook-stage pre-commit + ;; + commit-msg) + if [[ $# -ne 1 ]]; then + echo "Error: commit-msg hook expected a commit message file." >&2 + exit 1 + fi + exec pre-commit run --config "$config_file" --hook-stage commit-msg --commit-msg-filename "$1" + ;; + *) + echo "Error: unsupported hook name: $(basename "$0")" >&2 + exit 1 + ;; +esac diff --git a/.git-hooks/run-gitleaks.sh b/.git-hooks/run-gitleaks.sh index 114e979d1..bcbb21a0f 100755 --- a/.git-hooks/run-gitleaks.sh +++ b/.git-hooks/run-gitleaks.sh @@ -1,35 +1,22 @@ -#!/bin/bash +#!/usr/bin/env bash -# Try to find gitleaks in common locations -GITLEAKS_CMD="" +set -euo pipefail -# Check if gitleaks is in PATH -if command -v gitleaks &> /dev/null; then - GITLEAKS_CMD="gitleaks" -# Check Homebrew locations (Intel Mac) -elif [ -f "/usr/local/bin/gitleaks" ]; then - GITLEAKS_CMD="/usr/local/bin/gitleaks" -# Check Homebrew locations (Apple Silicon Mac) -elif [ -f "/opt/homebrew/bin/gitleaks" ]; then - GITLEAKS_CMD="/opt/homebrew/bin/gitleaks" -# Check if installed via other package managers -elif [ -f "$HOME/.local/bin/gitleaks" ]; then - GITLEAKS_CMD="$HOME/.local/bin/gitleaks" +if ! command -v gitleaks >/dev/null 2>&1; then + echo "Error: gitleaks is required to scan staged changes." >&2 + echo "Install it from https://github.com/gitleaks/gitleaks#installing" >&2 + exit 1 fi -# If gitleaks not found, exit with error -if [ -z "$GITLEAKS_CMD" ]; then - echo "============================================================" - echo "❌ ERROR: gitleaks not found!" - echo "" - echo "Please install gitleaks:" - echo " macOS: brew install gitleaks" - echo " Linux: https://github.com/gitleaks/gitleaks#installing" - echo "" - echo "After installation, run: ./.git-hooks/install-hooks.sh" - echo "============================================================" - exit 1 +# Gitleaks renamed "protect" to "git" in v8.19. Support both CLI forms. +if gitleaks git --help >/dev/null 2>&1; then + exec gitleaks git --pre-commit --staged "$@" +fi + +if gitleaks protect --help >/dev/null 2>&1; then + exec gitleaks protect --staged "$@" fi -# Run gitleaks with provided arguments -exec "$GITLEAKS_CMD" "$@" +echo "Error: the installed gitleaks version does not support staged scans." >&2 +echo "Upgrade gitleaks and try again." >&2 +exit 1 diff --git a/.gitleaks.toml b/.gitleaks.toml index 856063fee..b515f4b0c 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -33,18 +33,6 @@ title = "gitleaks config" [extend] useDefault = true -[[rules]] -id = "chinese-characters" -description = "Detecting Chinese characters" -regex = '''[\p{Han}]+''' -tags = ["chinese"] - -[[rules]] -id = "chinese-comments" -description = "Detect Chinese comments" -regex = '''(//|#|/\*|\*).*[\p{Han}]+''' -tags = ["chinese", "comments"] - [[rules]] id = "agora-app-id-pattern" description = "Agora App ID Pattern" @@ -129,17 +117,6 @@ paths = [ '''gitleaks.toml''', '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''', '''(go.mod|go.sum)$''', - '''iOS/.*\.strings''', - '''iOS/.*\.lproj/.*''', - '''iOS/Scenes/ConvoAI/.*''', - '''.*\.strings$''', - '''.*\.strings''', - '''.*\/zh-Hans\.lproj\/.*''', - '''.*\/zh-Hant\.lproj\/.*''', - '''.*\/zh\.lproj\/.*''', '''iOS/Pods/.*''', - '''.*\.bundle''', - '''README\.md''', - '''.*\.md''', - '''Android/.*/res/values(-zh)?/(strings|arrays)\.xml''' -] \ No newline at end of file + '''.*\.bundle''' +] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e81c1c34..0cf6a08a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,14 +2,14 @@ repos: - repo: local hooks: - id: gitleaks - name: Detect hardcoded secrets - description: Ensures no secrets are committed - entry: .git-hooks/run-gitleaks.sh protect - args: ["--config=.gitleaks.toml", "--staged", "--verbose"] + name: Scan staged changes for sensitive information + description: Rejects staged changes containing sensitive information + entry: .git-hooks/run-gitleaks.sh + args: ["--config=.gitleaks.toml", "--verbose"] language: system pass_filenames: false stages: [pre-commit] - + - id: check-commit-message name: Check commit message for Chinese characters description: Ensures commit messages do not contain Chinese characters diff --git a/AGENTS.md b/AGENTS.md index 51d0ce421..8c09b3d7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,11 +55,11 @@ Supporting documents: ## Git Hooks -This repository enforces two rules via Git hooks: -- No sensitive information (API keys, tokens) in committed code. +When installed, the repository's local Git hooks enforce two rules: +- No sensitive information (API keys, tokens) in staged changes. - Commit messages must be in English only (no Chinese characters). -Run `.git-hooks/install-hooks.sh` once after cloning to activate the hooks. +Install `pre-commit`, Gitleaks, and Python 3, then run `.git-hooks/install-hooks.sh` once after cloning. The script validates prerequisites but does not install them. See `HOOKS-GUIDE.md` for details and troubleshooting. ## Sensitive Configuration diff --git a/HOOKS-GUIDE.md b/HOOKS-GUIDE.md index a0a79aba3..8e39e4af4 100644 --- a/HOOKS-GUIDE.md +++ b/HOOKS-GUIDE.md @@ -1,75 +1,69 @@ # Git Hook Installation Guide -This project uses Git hooks to prevent sensitive information leaks and check commit messages. After cloning the code for the first time, please follow these steps to install the hooks: +This repository uses pre-commit to run two local checks: -## One-click installation +- Gitleaks scans staged changes for sensitive information. +- Commit messages containing Chinese characters are rejected. -Execute the following command to install all necessary Git hooks with one click: +Chinese content in source files and documentation is allowed. It is not part of the commit-message rule. -```bash -./.git-hooks/install-hooks.sh -``` - -## Hook Features - -After installation, the following features will be enabled: +## Prerequisites -1. **Sensitive Information Detection** (Based on Gitleaks) +Install these tools before installing the hooks: -- Detect sensitive information such as API keys and tokens in the code +- [pre-commit](https://pre-commit.com/#install) +- [Gitleaks](https://github.com/gitleaks/gitleaks#installing) +- Python 3 -- Block code submissions containing sensitive information +For example, on macOS with Homebrew: -2. **Commit Message Inspection** - -- Detect and block commit messages containing Chinese characters +```bash +brew install pre-commit gitleaks python +``` -- Ensure commit messages use only English -## Manual Installation +The repository installation script checks these dependencies but does not install or update them. -If the automatic installation fails, you can manually follow these steps: +## Install The Hooks -1. Install pre-commit: +Run this command once after cloning the repository: ```bash - -pip install pre-commit - +./.git-hooks/install-hooks.sh ``` -2. Install the pre-commit hook: +The script installs both the `pre-commit` and `commit-msg` hook types from `.pre-commit-config.yaml`. It supports normal clones, Git worktrees, and custom hook paths. -```bash +## Custom Hook Paths + +Git ignores its default hook directory when `core.hooksPath` is configured. The installation script detects this setting and installs repository-aware dispatchers into that directory. Existing hooks with other names, such as `post-commit` and `pre-push`, are preserved. -pre-commit install +Inspect the active value and its source with: +```bash +git config --show-origin --get core.hooksPath ``` -3. Install the commit-msg hook: - ```bash - pre-commit install --hook-type commit-msg - cp .git-hooks/check-commit-message.sh .git/hooks/commit-msg - chmod +x .git/hooks/commit-msg - ``` +The dispatchers run pre-commit only when the current repository contains `.pre-commit-config.yaml`. This allows a shared hook directory to serve multiple repositories without hardcoded repository paths. -## Frequently Asked Questions +The installer never changes `core.hooksPath`. It also refuses to overwrite an existing `pre-commit` or `commit-msg` hook that it does not manage. If either name is already used, update that hook to invoke pre-commit before rerunning the installer. -**Q: How to temporarily skip hook checks?** +## Usage -A: In special cases, you can use the `--no-verify` parameter to skip checks: +The hooks run automatically during `git commit`: -```bash +- The pre-commit hook scans only the content staged for the commit. +- The commit-msg hook checks the proposed commit message. -git commit -m "Your commit message" --no-verify +To run the staged sensitive-information check manually: +```bash +pre-commit run gitleaks ``` -However, use this parameter with caution to avoid leaking sensitive information. - -**Q: How to allow Chinese content in specific files?** +Git allows hooks to be bypassed with `git commit --no-verify`. Use that option only when explicitly approved; bypassing the hook also bypasses the sensitive-information scan. -A: Modify the `allowlist` section in the `.gitleaks.toml` file and add the corresponding file paths. +## Troubleshooting -**Q: Getting a "Chinese characters detected" prompt when committing?** +If a valid sample value triggers Gitleaks, add the narrowest possible rule-specific allowlist entry to `.gitleaks.toml`. Do not allowlist an entire documentation or source directory, because doing so would hide real credentials in that directory. -A: Commit messages must not contain Chinese characters. Please write commit messages in English only. \ No newline at end of file +If a commit message is rejected, rewrite it in English. The commit-message check requires UTF-8 input and Python 3. From 0bd5372fccb1a8fec3773a513d3b2ffc22cb7775 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 8 Sep 2026 22:41:41 +0800 Subject: [PATCH 40/49] refactor: streamline AI engineering assets and retire Azure CI Consolidate cross-platform guidance and project skills, fix lifecycle and API templates, and enforce complete AI asset validation in repository policy. Document isolated Python setup and CI ownership; remove retired Azure pipelines and their dedicated scripts. --- .../api-example-release-iteration/SKILL.md | 124 -- .../skills/cross-platform-api-change/SKILL.md | 91 + .github/ci/README.md | 21 + .github/ci/policy/requirements.txt | 1 + .../policy/tests/test_validate_ai_assets.py | 143 ++ .github/ci/policy/validate_ai_assets.py | 207 ++ .github/pull_request_template.md | 50 + .github/workflows/ai-engineering-checks.yml | 71 - .github/workflows/compile.yml | 161 ++ .github/workflows/create-tag.yml | 20 +- .github/workflows/mobile-ci.yml | 49 - .github/workflows/repository-policy.yml | 207 ++ .gitignore | 1 + .gitleaks.toml | 10 +- AGENTS.md | 25 +- .../.agents/skills/upsert-case/SKILL.md | 4 +- Android/APIExample-Audio/ci.env.py | 31 - .../.agents/skills/review-case/SKILL.md | 4 +- .../.agents/skills/upsert-case/SKILL.md | 8 +- .../references/composable-template.kt | 12 +- Android/APIExample-Compose/AGENTS.md | 3 +- Android/APIExample-Compose/ARCHITECTURE.md | 14 +- .../.agents/skills/review-case/SKILL.md | 5 +- .../.agents/skills/upsert-case/SKILL.md | 5 +- .../references/fragment-template.java | 2 + Android/APIExample/AGENTS.md | 2 +- Android/APIExample/ci.env.py | 30 - Android/build-template/build-android.yml | 41 - HOOKS-GUIDE.md | 6 + azure-pipelines.yml | 57 - cicd/build-template/build-ios.yml | 25 - cicd/build-template/build-mac.yml | 41 - cicd/build-template/github-release.yml | 3 - cicd/scripts/ios_build.sh | 32 - cicd/scripts/keycenter.py | 52 - cicd/scripts/mac_build.sh | 73 - .../ai-engineering/case-maintenance-matrix.md | 126 -- docs/ai-engineering/knowledge-index.md | 220 --- docs/ai-engineering/pilot-runs/README.md | 27 - docs/ai-engineering/release-iteration-gate.md | 163 -- docs/ai-engineering/release-known-issues.md | 21 - docs/ai-engineering/repository-profile.json | 34 - docs/ai-engineering/role-routing.json | 36 - docs/ai-engineering/templates/README.md | 16 - .../acceptance-manifest-template.json | 285 --- .../templates/release-dry-run-template.md | 131 -- docs/ai-engineering/tools/README.md | 104 - .../tools/generate_case_backlog.py | 229 --- .../tools/generate_case_backlog_test.py | 165 -- .../ai-engineering/tools/lint_agent_assets.py | 344 ---- .../tools/lint_agent_assets_test.py | 353 ---- .../tools/orchestrate_case_execution.py | 1676 ----------------- .../tools/orchestrate_case_execution_test.py | 1358 ------------- .../tools/prepare_case_execution.py | 511 ----- .../tools/prepare_case_execution_test.py | 453 ----- .../tools/validate_acceptance_manifest.py | 1516 --------------- .../validate_acceptance_manifest_test.py | 960 ---------- docs/ai/README.md | 59 + docs/ai/change-contract-template.md | 85 + docs/ai/project-matrix.md | 40 + docs/ai/release-risks.md | 17 + .../.agents/skills/query-cases/SKILL.md | 1 - .../.agents/skills/review-case/SKILL.md | 11 +- .../.agents/skills/upsert-case/SKILL.md | 30 +- iOS/APIExample-Audio/ARCHITECTURE.md | 14 +- .../.agents/skills/query-cases/SKILL.md | 1 - .../.agents/skills/review-case/SKILL.md | 11 +- .../.agents/skills/upsert-case/SKILL.md | 32 +- iOS/APIExample-OC/ARCHITECTURE.md | 17 +- .../.agents/skills/query-cases/SKILL.md | 1 - .../.agents/skills/review-case/SKILL.md | 21 +- .../.agents/skills/upsert-case/SKILL.md | 37 +- iOS/APIExample-SwiftUI/AGENTS.md | 3 +- iOS/APIExample-SwiftUI/ARCHITECTURE.md | 8 +- .../.agents/skills/query-cases/SKILL.md | 1 - .../.agents/skills/review-case/SKILL.md | 10 +- .../.agents/skills/upsert-case/SKILL.md | 21 +- iOS/APIExample/ARCHITECTURE.md | 14 +- macOS/.agents/skills/review-case/SKILL.md | 447 +---- .../references/correct-lifecycle.swift | 28 +- .../references/correct-thread-safety.swift | 3 +- .../references/incorrect-lifecycle.swift | 5 +- macOS/.agents/skills/upsert-case/SKILL.md | 34 +- .../references/example-template.swift | 104 +- .../references/lifecycle-pattern.swift | 26 +- .../references/thread-safety-pattern.swift | 3 +- macOS/AGENTS.md | 10 +- macOS/ARCHITECTURE.md | 55 +- windows/.agents/skills/review-case/SKILL.md | 544 +----- .../references/correct-lifecycle.cpp | 29 +- .../references/correct-thread-safety.cpp | 6 +- windows/.agents/skills/upsert-case/SKILL.md | 17 +- .../references/event-handler-pattern.cpp | 5 +- .../references/example-template.cpp | 137 +- .../upsert-case/references/example-template.h | 45 +- .../references/lifecycle-pattern.cpp | 36 +- .../references/message-map-pattern.cpp | 22 +- windows/AGENTS.md | 19 +- windows/ARCHITECTURE.md | 70 +- windows/cicd/script/downloadsdk1.ps1 | 18 - windows/cicd/templates/build-Windows.yml | 53 - 101 files changed, 1877 insertions(+), 10632 deletions(-) delete mode 100644 .agents/skills/api-example-release-iteration/SKILL.md create mode 100644 .agents/skills/cross-platform-api-change/SKILL.md create mode 100644 .github/ci/README.md create mode 100644 .github/ci/policy/requirements.txt create mode 100644 .github/ci/policy/tests/test_validate_ai_assets.py create mode 100644 .github/ci/policy/validate_ai_assets.py create mode 100644 .github/pull_request_template.md delete mode 100644 .github/workflows/ai-engineering-checks.yml create mode 100644 .github/workflows/compile.yml delete mode 100644 .github/workflows/mobile-ci.yml create mode 100644 .github/workflows/repository-policy.yml delete mode 100644 Android/APIExample-Audio/ci.env.py delete mode 100644 Android/APIExample/ci.env.py delete mode 100644 Android/build-template/build-android.yml delete mode 100644 azure-pipelines.yml delete mode 100644 cicd/build-template/build-ios.yml delete mode 100644 cicd/build-template/build-mac.yml delete mode 100644 cicd/build-template/github-release.yml delete mode 100755 cicd/scripts/ios_build.sh delete mode 100644 cicd/scripts/keycenter.py delete mode 100755 cicd/scripts/mac_build.sh delete mode 100644 docs/ai-engineering/case-maintenance-matrix.md delete mode 100644 docs/ai-engineering/knowledge-index.md delete mode 100644 docs/ai-engineering/pilot-runs/README.md delete mode 100644 docs/ai-engineering/release-iteration-gate.md delete mode 100644 docs/ai-engineering/release-known-issues.md delete mode 100644 docs/ai-engineering/repository-profile.json delete mode 100644 docs/ai-engineering/role-routing.json delete mode 100644 docs/ai-engineering/templates/README.md delete mode 100644 docs/ai-engineering/templates/acceptance-manifest-template.json delete mode 100644 docs/ai-engineering/templates/release-dry-run-template.md delete mode 100644 docs/ai-engineering/tools/README.md delete mode 100644 docs/ai-engineering/tools/generate_case_backlog.py delete mode 100644 docs/ai-engineering/tools/generate_case_backlog_test.py delete mode 100644 docs/ai-engineering/tools/lint_agent_assets.py delete mode 100644 docs/ai-engineering/tools/lint_agent_assets_test.py delete mode 100644 docs/ai-engineering/tools/orchestrate_case_execution.py delete mode 100644 docs/ai-engineering/tools/orchestrate_case_execution_test.py delete mode 100644 docs/ai-engineering/tools/prepare_case_execution.py delete mode 100644 docs/ai-engineering/tools/prepare_case_execution_test.py delete mode 100644 docs/ai-engineering/tools/validate_acceptance_manifest.py delete mode 100644 docs/ai-engineering/tools/validate_acceptance_manifest_test.py create mode 100644 docs/ai/README.md create mode 100644 docs/ai/change-contract-template.md create mode 100644 docs/ai/project-matrix.md create mode 100644 docs/ai/release-risks.md delete mode 100644 windows/cicd/script/downloadsdk1.ps1 delete mode 100644 windows/cicd/templates/build-Windows.yml diff --git a/.agents/skills/api-example-release-iteration/SKILL.md b/.agents/skills/api-example-release-iteration/SKILL.md deleted file mode 100644 index 40825c60c..000000000 --- a/.agents/skills/api-example-release-iteration/SKILL.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -name: api-example-release-iteration -description: Use when turning an RTC API Examples product requirement into aligned Android, iOS, macOS, and Windows changes with attributed implementation and independent repository acceptance ---- - -# API Example Release Iteration - -Use this skill for one product requirement across all official platform families. The Lead coordinates a shared Contract, attributed platform delivery, independent platform Verification, and final cross-platform acceptance. - -## Required Inputs - -Identify before editing: - -- Product scenario and key RTC SDK APIs. -- New case, existing case update, docs-only, or SDK-version work. -- Reference case for behavior and parity. -- Verification budget and available platform hosts. -- Target RTC SDK version for each platform. Platforms may remain on different release lines. - -Android, iOS, macOS, and Windows are required by default. Their targets are fixed to `Android/APIExample/`, `iOS/APIExample/`, `macOS/`, and `windows/`. Audio, Compose, SwiftUI, and Objective-C variants are outside this workflow; marking a primary platform not required needs an explicit waiver reason. - -## Source Route - -Read root `AGENTS.md`, the repository profile, this knowledge index and gate, then `/AGENTS.md`, `/AGENTS.md`, `/ARCHITECTURE.md`, and the available `query-cases`, `upsert-case`, and `review-case` skills. Every project keeps its skills under `.agents/skills/`. Read `docs/ai-engineering/release-known-issues.md` only for release, SDK version, CI, signing, packaging, license, or third-party asset work. - -## Agent Topology - -```text -Lead - Contract - Android Implementation -> Android Verification - iOS Implementation -> iOS Verification - macOS Implementation -> macOS Verification - Windows Implementation -> Windows Verification - Cross-platform acceptance -``` - -This is one Lead plus nine independent `codex exec` runs. They are replayable requirement-workflow runs, not parent-managed Codex subagent threads. Contract executes once. In a shared checkout, platform Implementation agents run one at a time so each repository delta belongs to one run; platform Verification agents run concurrently after their corresponding Implementation passes. - -| Role Type | Responsibility | Profile | -| --- | --- | --- | -| Contract | Shared behavior, APIs, non-goals, reference, platform targets and allowed files | `standard` | -| Implementation | Query and modify exactly one platform target | `deep` | -| Verification | Findings-first review, parity, UX, and strongest valid platform checks | `review` | - -Implementation cannot approve its own platform. Every accepted artifact needs a distinct `agent_id`, Codex run provenance, model/profile, and input snapshot hash. - -## Execution - -Initialize one requirement: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ - --matrix docs/ai-engineering/case-maintenance-matrix.md \ - --feature "" \ - --target-sdk-version "4.7.0" \ - --platform-sdk-version "android=4.7.0" \ - --run-dir /tmp/api-example-requirement -``` - -`--target-sdk-version` supplies the baseline for all four platforms. Repeat `--platform-sdk-version platform=x.y.z` only for platforms whose target differs. The execution package and manifest store the resolved four-platform mapping, so staggered platform releases never require script or repository-profile changes. - -The default `docs/ai-engineering/repository-profile.json` supplies repository-specific SDK package names and version sources. Use `--repository-profile` only for a checked-in alternative profile. The execution package, input snapshots, and acceptance manifest record its repository-relative path and SHA-256; the standalone validator recomputes release checks from that binding. - -For a new requirement not yet actionable in the matrix, also pass `--sdk-family ""` and repeat `--key-api ""` as needed. - -Dispatch the shared Contract: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement \ - --phase contract \ - --model "" -``` - -Dispatch all platform agents by omitting `--platform`. Implementation runs are serialized in the shared checkout for delta attribution; Verification runs fan out concurrently: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement \ - --phase implementation \ - --model "" - -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement \ - --phase verification \ - --model "" -``` - -Use `--platform android|ios|macos|windows` for a single platform and `--retry` to replace a prior `FAIL` or `BLOCKED` artifact. Retrying Implementation invalidates that platform's old Verification and carries all attempts into one cumulative net repository delta. Each run has an independent 900-second default timeout, JSONL command log, host provenance, and target-project working directory so nested `AGENTS.md` files load automatically. - -Assemble only after reviewing all required platform artifacts: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py assemble \ - --run-dir /tmp/api-example-requirement \ - --matrix docs/ai-engineering/case-maintenance-matrix.md \ - --final-status "" \ - --cross-platform-result "" \ - --cross-platform-evidence "" -``` - -## Platform Rules - -- Contract preserves the fixed primary target project and defines allowed files for each platform. -- The dispatcher runs platform roles from that target project. In a shared checkout it refreshes the package/dependency snapshot, completes and reconciles one Implementation before starting the next, derives that role's cumulative net repository delta, and rejects changes outside Contract paths. -- Each Implementation agent follows only its platform/project instructions. -- Platform source files, dependencies, and build scripts are never shared across roots. -- Verification covers code review, reference parity, discoverability, UX, and valid checks on the current verification host. Executed PASS/FAIL commands must bind to the Codex JSONL command event and exit code, Contract working directory, and recorded host platform. -- Contract and Verification runs may create ignored build output but must not modify tracked or untracked repository content. -- A required platform `FAIL` or `BLOCKED` forces the whole requirement to `BLOCKED`. -- Final non-`BLOCKED` acceptance requires all required platform review, parity, and build results to pass, plus cross-platform acceptance `PASS`. - -## Host Mismatch - -On macOS, Windows Verification may perform repository-local static review only. It must not download Windows SDK archives, emulate Windows, cross-compile, or use a substitute compiler as build evidence. Keep Windows Verification and the requirement `BLOCKED` until the Verification role is retried on a real Windows host with MSBuild evidence. - -## Matrix And Release - -Each platform Implementation may propose zero or more matrix updates. `DONE` requires final `PASS`, platform parity/build `PASS`, and no skipped checks. Release data is limited to repository SDK-version consistency. An externally injected SDK without a trustworthy tracked version source keeps its platform SDK check and final acceptance `BLOCKED`; build success is not version evidence. Jenkins packaging, QA validation, artifact distribution, and external website publication are outside this workflow and must not be recorded as manifest gates. - -## Final Output - -Report the shared Contract, each platform's files and Verification result, declared platform differences, SDK-version checks, manifest validation, and final status. Do not describe the workflow as fully autonomous: the Lead still advances phases, handles retries, and owns final cross-platform judgment. diff --git a/.agents/skills/cross-platform-api-change/SKILL.md b/.agents/skills/cross-platform-api-change/SKILL.md new file mode 100644 index 000000000..86b23a3c6 --- /dev/null +++ b/.agents/skills/cross-platform-api-change/SKILL.md @@ -0,0 +1,91 @@ +--- +name: cross-platform-api-change +description: Coordinate an RTC API Examples change across Android, iOS, macOS, and Windows when behavior, SDK usage, dependencies, CI, or packaging may affect more than one project. +--- + +# Cross-Platform API Change + +Use one written contract to keep every affected implementation aligned. The contract is +the source of truth for behavior; an existing platform implementation is only a reference. + +## Required Context + +1. Read the root `AGENTS.md`. +2. Read `docs/ai/README.md`, `docs/ai/change-contract-template.md`, and + `docs/ai/project-matrix.md`. +3. For SDK, dependency, CI, packaging, signing, or external-asset changes, also read + `docs/ai/release-risks.md`. +4. Read the platform and project `AGENTS.md` and `ARCHITECTURE.md` files for every project + that may be affected. + +Treat the current source tree as the case inventory. Do not rely on a separately maintained +case catalog. + +## Workflow + +### 1. Establish The Contract + +Before editing, copy `docs/ai/change-contract-template.md` into the issue, pull request, or +task notes and complete it. Define observable behavior, SDK operations, inputs and defaults, +callbacks, success and failure signals, lifecycle cleanup, non-goals, and the reference +implementation or documentation. + +Separate shared behavior from platform-specific API and UI details. If the requirement +changes, update the contract first and then re-evaluate scope and existing work. + +### 2. Decide Every Project + +Review every row in `docs/ai/project-matrix.md`. Mark each project `REQUIRED`, +`REVIEW_ONLY`, or `NOT_APPLICABLE`. Include a reason for every project that is not required. +Do not assume that Android Views covers Compose, iOS UIKit covers SwiftUI or Objective-C, +or a full-SDK project covers its audio-only counterpart. + +### 3. Implement From The Same Contract + +One coordinator owns the contract and cross-platform decisions. Split implementation by +top-level platform only when parallel work is explicitly available, and give every owner +the same contract, its project scope, allowed paths, reference, and validation requirements. +Platform owners must not redefine shared behavior. + +Implementations may differ because of SDK types, UI frameworks, permissions, or operating +system behavior. Record each intentional difference and its reason in the contract. + +### 4. Verify Each Required Project + +For every required project: + +- compare the diff with the shared contract; +- verify SDK arguments, defaults, callbacks, lifecycle, threading, errors, registration, + resources, localization, and cleanup as applicable; +- run the strongest valid build or static check documented by that project; +- record changed files, commands, results, blockers, and intentional differences. + +Keep scope decisions and validation status separate. Record each planned validation as one +of: + +- `PASS`: the command or behavioral check completed and met its acceptance criteria; +- `FAIL`: the check ran and failed; +- `BLOCKED`: the check cannot run yet; record the blocker, remaining command, and the + condition that will unblock it; +- `NOT_RUN`: the check was not attempted; record why. + +Do not convert `BLOCKED` or `NOT_RUN` into a pass. They are valid handoff states, but they +leave the overall change incomplete. + +Compilation is necessary but does not prove behavioral parity. Use an independent review +pass when available. + +### 5. Close Cross-Platform Acceptance + +The change is complete only when every project has an explicit scope decision, every +required implementation satisfies the contract, and every required validation is `PASS`. +A change with `FAIL`, `BLOCKED`, or `NOT_RUN` validation may be handed off with clear next +steps, but must not be reported as complete. Copy the final scope, differences, and +validation evidence into the pull request using `.github/pull_request_template.md`. + +Before handoff, confirm that SDK sources match `docs/ai/project-matrix.md` and that no real +App ID, certificate, token, signing asset, private key, or private download URL is staged. + +Do not commit task transcripts, agent identities, generated case catalogs, prompt snapshots, +or run logs. Preserve durable product behavior and verification evidence in the issue or +pull request. diff --git a/.github/ci/README.md b/.github/ci/README.md new file mode 100644 index 000000000..e0ea41117 --- /dev/null +++ b/.github/ci/README.md @@ -0,0 +1,21 @@ +# CI Implementation + +GitHub Actions entry points live in `.github/workflows/`. This directory contains their +policy helpers and the release packaging scripts loaded by Jenkins. + +| Path | Caller and responsibility | Validation | +| --- | --- | --- | +| `Jenkinsfile_bitbucket.groovy` | External Jenkins shared library loads the release pipeline. | Verify job configuration and release evidence in the release ticket. | +| `build/` | Jenkins platform Groovy entry points call packaging scripts, which use each project's `cloud_build.*` / `cloud_project.*` adapters. IPA entry points also depend on external Jenkins configuration. | Run the signing-preflight unit tests below; actual signing, private SDK injection and artifact verification belong to Jenkins. | +| `policy/` | `repository-policy.yml` runs AI asset validation and its regression tests; `.git-hooks/` supplies the local credential and commit-message checks. | Follow [AI validation](../../docs/ai/README.md) for venv setup and policy checks. | + +After creating the venv described above, run the packaging helper tests from the repository root: + +```bash +.venv/bin/python -m unittest discover --start-directory .github/ci/build/tests --pattern 'test_check_ios_signing_assets.py' +``` + +Keep the Jenkins entry point and `build/` paths stable: external jobs and existing scripts +refer to them directly. A lack of repository-local callers does not establish that an +external entry point is unused. GitHub compilation uses public SDK dependencies and +produces compile evidence; Jenkins owns release artifacts and signing evidence. diff --git a/.github/ci/policy/requirements.txt b/.github/ci/policy/requirements.txt new file mode 100644 index 000000000..8392d5414 --- /dev/null +++ b/.github/ci/policy/requirements.txt @@ -0,0 +1 @@ +PyYAML==6.0.2 diff --git a/.github/ci/policy/tests/test_validate_ai_assets.py b/.github/ci/policy/tests/test_validate_ai_assets.py new file mode 100644 index 000000000..81275812c --- /dev/null +++ b/.github/ci/policy/tests/test_validate_ai_assets.py @@ -0,0 +1,143 @@ +import contextlib +import importlib.util +import io +import shutil +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + + +MODULE_PATH = Path(__file__).resolve().parents[1] / "validate_ai_assets.py" +SPEC = importlib.util.spec_from_file_location("validate_ai_assets", MODULE_PATH) +if SPEC is None or SPEC.loader is None: + raise RuntimeError(f"Unable to load {MODULE_PATH}") +VALIDATOR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(VALIDATOR) + + +class ValidateSkillFrontmatterTest(unittest.TestCase): + def setUp(self) -> None: + self.temporary_directory = tempfile.TemporaryDirectory() + self.original_root = VALIDATOR.ROOT + VALIDATOR.ROOT = Path(self.temporary_directory.name) + + def tearDown(self) -> None: + VALIDATOR.ROOT = self.original_root + self.temporary_directory.cleanup() + + def validate(self, frontmatter: str) -> list[str]: + skill_path = VALIDATOR.ROOT / ".agents" / "skills" / "sample-skill" / "SKILL.md" + skill_path.parent.mkdir(parents=True) + skill_path.write_text(f"---\n{frontmatter}\n---\n\n# Sample\n", encoding="utf-8") + errors: list[str] = [] + VALIDATOR.validate_skill(skill_path, errors) + return errors + + def test_accepts_valid_yaml_frontmatter(self) -> None: + errors = self.validate("name: sample-skill\ndescription: >\n A valid description.\nlicense: MIT") + self.assertEqual([], errors) + + def test_rejects_malformed_yaml_description(self) -> None: + errors = self.validate("name: sample-skill\ndescription: [unterminated") + self.assertTrue(any("invalid YAML frontmatter" in error for error in errors)) + + def test_rejects_null_description(self) -> None: + errors = self.validate("name: sample-skill\ndescription: null") + self.assertTrue(any("description must be a non-empty string" in error for error in errors)) + + def test_rejects_missing_description_value(self) -> None: + errors = self.validate("name: sample-skill\ndescription:\nlicense: MIT") + self.assertTrue(any("description must be a non-empty string" in error for error in errors)) + + +class AssetIntegrityTest(unittest.TestCase): + """Exercise the real asset tree, including failures that metadata tests miss.""" + + def setUp(self) -> None: + self.temporary_directory = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary_directory.cleanup) + self.root = Path(self.temporary_directory.name).resolve() + source_root = MODULE_PATH.parents[3] + directories = {".", "Android", "iOS", *(path for _, path in VALIDATOR.PROJECTS)} + for directory in directories: + source = source_root / directory + target = self.root / directory + target.mkdir(parents=True, exist_ok=True) + for filename in ("AGENTS.md", "CLAUDE.md", "ARCHITECTURE.md", "README.md"): + if (source / filename).is_file(): + shutil.copy2(source / filename, target / filename) + if (source / ".agents").is_dir(): + shutil.copytree(source / ".agents", target / ".agents") + shutil.copytree(source_root / "docs" / "ai", self.root / "docs" / "ai") + shutil.copytree(source_root / ".github", self.root / ".github", + ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) + + def validate(self) -> tuple[int, str]: + output = io.StringIO() + with patch.object(VALIDATOR, "ROOT", self.root), contextlib.redirect_stdout(output), contextlib.redirect_stderr(output): + result = VALIDATOR.main() + return result, output.getvalue() + + def assert_missing_file_rejected(self, relative_path: str) -> None: + target = self.root / relative_path + contents = target.read_bytes() + target.unlink() + try: + result, output = self.validate() + self.assertEqual(1, result, output) + self.assertIn(f"missing required AI entry file: {relative_path}", output) + finally: + target.write_bytes(contents) + + def test_current_repository_assets_pass(self) -> None: + result, output = self.validate() + self.assertEqual(0, result, output) + + def test_missing_project_skills_fail_even_if_directories_remain(self) -> None: + for role in ("query-cases", "upsert-case", "review-case"): + with self.subTest(role=role): + self.assert_missing_file_rejected(f"Android/APIExample/.agents/skills/{role}/SKILL.md") + + def test_missing_root_and_project_adapters_fail(self) -> None: + for filename in ("CLAUDE.md", "iOS/APIExample-SwiftUI/CLAUDE.md"): + with self.subTest(filename=filename): + self.assert_missing_file_rejected(filename) + + def test_missing_platform_architecture_fails(self) -> None: + for filename in ("Android/ARCHITECTURE.md", "iOS/ARCHITECTURE.md"): + with self.subTest(filename=filename): + self.assert_missing_file_rejected(filename) + + def test_directory_cannot_replace_required_entry_file(self) -> None: + target = self.root / "AGENTS.md" + target.unlink() + target.mkdir() + result, output = self.validate() + self.assertEqual(1, result, output) + self.assertIn("missing required AI entry file: AGENTS.md", output) + + def test_adapter_must_import_its_project_guide(self) -> None: + target = self.root / "Android/APIExample/CLAUDE.md" + target.write_text("# Adapter\n\nSee @MISSING.md\n", encoding="utf-8") + result, output = self.validate() + self.assertEqual(1, result, output) + self.assertIn("adapter must import the same-directory @AGENTS.md", output) + + def test_missing_referenced_template_fails(self) -> None: + target = self.root / "Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java" + target.unlink() + result, output = self.validate() + self.assertEqual(1, result, output) + self.assertIn("missing reference: references/fragment-template.java", output) + + def test_invalid_project_skill_yaml_fails(self) -> None: + target = self.root / "Android/APIExample/.agents/skills/upsert-case/SKILL.md" + target.write_text("---\nname: upsert-case\ndescription: [unterminated\n---\n", encoding="utf-8") + result, output = self.validate() + self.assertEqual(1, result, output) + self.assertIn("invalid YAML frontmatter", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/ci/policy/validate_ai_assets.py b/.github/ci/policy/validate_ai_assets.py new file mode 100644 index 000000000..49742a826 --- /dev/null +++ b/.github/ci/policy/validate_ai_assets.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Validate durable AI guidance and cross-file references.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from urllib.parse import unquote + +import yaml + + +ROOT = Path(__file__).resolve().parents[3] +PROJECTS = ( + ("Android Views full", "Android/APIExample"), + ("Android Views audio", "Android/APIExample-Audio"), + ("Android Compose", "Android/APIExample-Compose"), + ("iOS UIKit full", "iOS/APIExample"), + ("iOS UIKit audio", "iOS/APIExample-Audio"), + ("iOS SwiftUI", "iOS/APIExample-SwiftUI"), + ("iOS Objective-C", "iOS/APIExample-OC"), + ("macOS AppKit", "macOS"), + ("Windows MFC", "windows"), +) +ENTRY_FILES = ( + "AGENTS.md", + ".agents/skills/cross-platform-api-change/SKILL.md", + ".github/pull_request_template.md", + "docs/ai/README.md", + "docs/ai/change-contract-template.md", + "docs/ai/project-matrix.md", + "docs/ai/release-risks.md", +) +PLATFORMS = ("Android", "iOS", "macOS", "windows") +PROJECT_SKILLS = ("query-cases", "upsert-case", "review-case") +TABLE_FILES = ( + "docs/ai/project-matrix.md", + "docs/ai/change-contract-template.md", + ".github/pull_request_template.md", +) +ALLOWED_FRONTMATTER_KEYS = {"name", "description", "license", "allowed-tools", "metadata"} +MARKDOWN_LINK = re.compile(r"(? set[str]: + required = set(ENTRY_FILES) + guide_directories = {".", *PLATFORMS, *(path for _, path in PROJECTS)} + for directory in guide_directories: + for filename in ("AGENTS.md", "CLAUDE.md"): + required.add((Path(directory) / filename).as_posix()) + if directory != ".": + required.add(f"{directory}/ARCHITECTURE.md") + for _, project in PROJECTS: + for skill in PROJECT_SKILLS: + required.add(f"{project}/.agents/skills/{skill}/SKILL.md") + return required + + +def relative(path: Path) -> str: + return path.relative_to(ROOT).as_posix() + + +def validate_skill(path: Path, errors: list[str]) -> None: + text = path.read_text(encoding="utf-8") + if not text.startswith("---\n"): + errors.append(f"{relative(path)}: missing YAML frontmatter") + return + + end = text.find("\n---\n", 4) + if end < 0: + errors.append(f"{relative(path)}: unterminated YAML frontmatter") + return + + frontmatter = text[4:end] + try: + data = yaml.safe_load(frontmatter) + except yaml.YAMLError as error: + problem = getattr(error, "problem", None) or str(error).splitlines()[0] + errors.append(f"{relative(path)}: invalid YAML frontmatter: {problem}") + return + + if not isinstance(data, dict): + errors.append(f"{relative(path)}: YAML frontmatter must be a mapping") + return + + unexpected = {str(key) for key in data if key not in ALLOWED_FRONTMATTER_KEYS} + if unexpected: + errors.append(f"{relative(path)}: unsupported frontmatter keys: {', '.join(sorted(unexpected))}") + + if "name" not in data: + errors.append(f"{relative(path)}: missing skill name") + elif not isinstance(data["name"], str) or not data["name"].strip(): + errors.append(f"{relative(path)}: skill name must be a non-empty string") + else: + name = data["name"].strip() + if name != path.parent.name: + errors.append(f"{relative(path)}: skill name '{name}' does not match directory '{path.parent.name}'") + if len(name) > 64 or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name): + errors.append(f"{relative(path)}: skill name must be lowercase hyphen-case and at most 64 characters") + + if "description" not in data: + errors.append(f"{relative(path)}: missing skill description") + elif not isinstance(data["description"], str) or not data["description"].strip(): + errors.append(f"{relative(path)}: skill description must be a non-empty string") + else: + description = data["description"].strip() + if len(description) > 1024: + errors.append(f"{relative(path)}: skill description must contain at most 1024 characters") + + +def resolve_reference(source: Path, target: str) -> Path | None: + target = unquote(target.strip()) + if target.startswith("<") and target.endswith(">"): + target = target[1:-1] + target = target.split("#", 1)[0].split("?", 1)[0] + if not target or target.startswith(("http://", "https://", "mailto:")): + return None + if target.startswith(("docs/ai/", ".github/")): + return ROOT / target + return source.parent / target + + +def validate_references(path: Path, errors: list[str]) -> None: + text = path.read_text(encoding="utf-8") + if path.name == "CLAUDE.md" and not AGENTS_IMPORT.search(text): + errors.append(f"{relative(path)}: adapter must import the same-directory @AGENTS.md") + if "docs/ai-engineering" in text or ".agent/skills" in text: + errors.append(f"{relative(path)}: contains a retired AI asset path") + if re.search(r"(?i)\[TODO:[^]]*\]||\bTODO:", text): + errors.append(f"{relative(path)}: contains an unfinished TODO scaffold") + + targets = [match.group(1).split(maxsplit=1)[0] for match in MARKDOWN_LINK.finditer(text)] + targets.extend(match.group(1).strip().rstrip(".,;:") for match in CODE_PATH.finditer(text)) + for target in targets: + resolved = resolve_reference(path, target) + if resolved is None: + continue + resolved = resolved.resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + errors.append(f"{relative(path)}: reference leaves the repository: {target}") + continue + if not resolved.exists(): + errors.append(f"{relative(path)}: missing reference: {target}") + + +def validate_project_tables(errors: list[str]) -> None: + expected = [project for project, _ in PROJECTS] + for filename in TABLE_FILES: + lines = (ROOT / filename).read_text(encoding="utf-8").splitlines() + header_index = next( + (index for index, line in enumerate(lines) if re.match(r"^\|\s*Project\s*\|", line)), + None, + ) + if header_index is None: + errors.append(f"{filename}: missing project table") + continue + rows = [] + for line in lines[header_index + 2 :]: + if not line.startswith("|"): + break + rows.append(line.strip("|").split("|", 1)[0].strip()) + if rows != expected: + errors.append(f"{filename}: project table must contain the canonical nine rows in order") + + +def main() -> int: + errors: list[str] = [] + for filename in sorted(required_entry_files()): + if not (ROOT / filename).is_file(): + errors.append(f"missing required AI entry file: {filename}") + + skill_files = sorted(path for path in ROOT.glob("**/.agents/skills/*/SKILL.md") if path.is_file()) + if not skill_files: + errors.append("no skills found under .agents/skills") + for path in skill_files: + validate_skill(path, errors) + + markdown_files = {ROOT / filename for filename in ENTRY_FILES if filename.endswith(".md")} + markdown_files.update(ROOT.glob("**/AGENTS.md")) + markdown_files.update(ROOT.glob("**/CLAUDE.md")) + markdown_files.update(ROOT.glob("**/ARCHITECTURE.md")) + markdown_files.update(ROOT.glob("docs/ai/**/*.md")) + markdown_files.update(ROOT.glob("**/.agents/**/*.md")) + for path in sorted(markdown_files): + if path.is_file(): + validate_references(path, errors) + + if all((ROOT / filename).is_file() for filename in TABLE_FILES): + validate_project_tables(errors) + + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + print(f"AI asset validation failed with {len(errors)} error(s).", file=sys.stderr) + return 1 + + print(f"AI asset validation passed: {len(skill_files)} skills, 9 projects, no broken references.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..fc1e8bf8c --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,50 @@ + + +## Change Contract + +- Requirement or issue: +- Shared behavior: +- SDK/API changes: +- Non-goals: +- Reference implementation or documentation: + +## Project Scope + +Use `REQUIRED`, `REVIEW_ONLY`, or `NOT_APPLICABLE` and explain every non-required decision. +For documentation or repository-only work, one shared reason may cover all unaffected rows. + +| Project | Decision | Change or reason | +| --- | --- | --- | +| Android Views full | | | +| Android Views audio | | | +| Android Compose | | | +| iOS UIKit full | | | +| iOS UIKit audio | | | +| iOS SwiftUI | | | +| iOS Objective-C | | | +| macOS AppKit | | | +| Windows MFC | | | + +## Cross-Platform Review + +- Intentional platform differences and reasons: +- Lifecycle, threading, registration, resource, or permission considerations: +- Remaining blockers or risks: + +## Validation + +Use `PASS`, `FAIL`, `BLOCKED`, or `NOT_RUN`. Scope and validation are separate: a documented +blocker is useful handoff information, but it does not make a required check pass. + +| Project | Command or behavioral check | Status | Evidence, blocker, or reason | Remaining command and unblock condition | +| --- | --- | --- | --- | --- | +| | | | | | + +## Repository Checks + +- [ ] The implementation matches the shared contract across all required projects. +- [ ] Every required validation for each `REQUIRED` project is `PASS`; otherwise this change remains incomplete. +- [ ] SDK versions and dependency sources match `docs/ai/project-matrix.md`. +- [ ] Gitleaks passed and no real App ID, certificate, token, private key, signing asset, or private URL is included. +- [ ] Commit messages are in English. +- [ ] Release packaging changes were checked separately from compile-only CI when applicable. diff --git a/.github/workflows/ai-engineering-checks.yml b/.github/workflows/ai-engineering-checks.yml deleted file mode 100644 index dd66c6597..000000000 --- a/.github/workflows/ai-engineering-checks.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: AI Engineering Checks - -# Guards the AI engineering assets (AGENTS.md routing, SKILL.md bundles) and the -# orchestration tooling under docs/ai-engineering/tools/. These checks are file-only -# and run on every host family used by verification; platform builds remain in Azure. - -on: - push: - branches: - - main - - master - - 'dev/**' - paths: - - '**/AGENTS.md' - - '**/ARCHITECTURE.md' - - '**/SKILL.md' - - '**/.agents/**' - - 'docs/ai-engineering/**' - - '.github/workflows/ai-engineering-checks.yml' - pull_request: - paths: - - '**/AGENTS.md' - - '**/ARCHITECTURE.md' - - '**/SKILL.md' - - '**/.agents/**' - - 'docs/ai-engineering/**' - - '.github/workflows/ai-engineering-checks.yml' - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ai-engineering-checks-${{ github.ref }} - cancel-in-progress: true - -jobs: - checks: - name: Tooling checks (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - os: - - ubuntu-latest - - macos-latest - - windows-latest - env: - PYTHONDONTWRITEBYTECODE: '1' - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - # The tooling uses str.removeprefix, so 3.9 is the floor. Pin 3.11 for stability. - python-version: '3.11' - - - name: Lint AI engineering assets - run: python docs/ai-engineering/tools/lint_agent_assets.py - - - name: Run AI engineering tooling tests - run: > - python -m unittest - docs/ai-engineering/tools/lint_agent_assets_test.py - docs/ai-engineering/tools/validate_acceptance_manifest_test.py - docs/ai-engineering/tools/generate_case_backlog_test.py - docs/ai-engineering/tools/prepare_case_execution_test.py - docs/ai-engineering/tools/orchestrate_case_execution_test.py diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml new file mode 100644 index 000000000..f2449ac44 --- /dev/null +++ b/.github/workflows/compile.yml @@ -0,0 +1,161 @@ +name: Compile CI + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: compile-${{ github.ref }} + cancel-in-progress: true + +env: + PLACEHOLDER_APP_ID: "00000000000000000000000000000000" + +jobs: + android: + name: Android (${{ matrix.name }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - name: Full + project: Android/APIExample + - name: Audio + project: Android/APIExample-Audio + - name: Compose + project: Android/APIExample-Compose + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + cache: gradle + cache-dependency-path: | + ${{ matrix.project }}/**/*.gradle* + ${{ matrix.project }}/gradle/wrapper/gradle-wrapper.properties + - uses: android-actions/setup-android@v3 + - name: Configure compile-only App ID + working-directory: ${{ matrix.project }} + run: printf 'AGORA_APP_ID=%s\nAGORA_APP_CERT=\n' "$PLACEHOLDER_APP_ID" > local.properties + - name: Compile + working-directory: ${{ matrix.project }} + run: ./gradlew --no-daemon --stacktrace :app:assembleDebug + + ios: + name: iOS (${{ matrix.name }}) + runs-on: macos-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - name: Full + project: iOS/APIExample + workspace: APIExample.xcworkspace + scheme: APIExample + key_center: APIExample/Common/KeyCenter.swift + rtc_pod: AgoraRtcEngine_iOS + - name: Audio + project: iOS/APIExample-Audio + workspace: APIExample-Audio.xcworkspace + scheme: APIExample-Audio + key_center: APIExample-Audio/Common/KeyCenter.swift + rtc_pod: AgoraAudio_iOS + - name: SwiftUI + project: iOS/APIExample-SwiftUI + workspace: APIExample-SwiftUI.xcworkspace + scheme: APIExample-SwiftUI + key_center: APIExample-SwiftUI/Common/KeyCenter.swift + rtc_pod: AgoraRtcEngine_iOS + - name: Objective-C + project: iOS/APIExample-OC + workspace: APIExample-OC.xcworkspace + scheme: APIExample-OC + key_center: APIExample-OC/Common/KeyCenter.m + rtc_pod: AgoraRtcEngine_iOS + steps: + - uses: actions/checkout@v4 + - name: Select the published RTC pod + working-directory: ${{ matrix.project }} + env: + RTC_POD: ${{ matrix.rtc_pod }} + run: | + if grep -Eq "^[[:space:]]*pod 'sdk'" Podfile; then + rtc_version="$(grep -E "pod '$RTC_POD'" Podfile | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)" + test -n "$rtc_version" + perl -0pi -e "s/^[ \\t]*pod 'sdk', :path => 'sdk\\.podspec'/ pod '$RTC_POD', '$rtc_version'/m" Podfile + fi + - name: Configure compile-only App ID + working-directory: ${{ matrix.project }} + env: + KEY_CENTER: ${{ matrix.key_center }} + run: | + perl -0pi -e 's/<#YOUR AppId#>/"$ENV{PLACEHOLDER_APP_ID}"/g; s/<#APPID#>/"$ENV{PLACEHOLDER_APP_ID}"/g; s/<#YOUR APPID#>/@"$ENV{PLACEHOLDER_APP_ID}"/g' "$KEY_CENTER" + ! grep -Eq '<#[^#]+#>' "$KEY_CENTER" + - name: Install CocoaPods dependencies + working-directory: ${{ matrix.project }} + run: pod install --repo-update + - name: Compile + working-directory: ${{ matrix.project }} + env: + WORKSPACE_NAME: ${{ matrix.workspace }} + SCHEME: ${{ matrix.scheme }} + run: | + xcodebuild \ + -workspace "$WORKSPACE_NAME" \ + -scheme "$SCHEME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath "$RUNNER_TEMP/ios-${{ matrix.name }}" \ + CODE_SIGNING_ALLOWED=NO \ + build + + macos: + name: macOS + runs-on: macos-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - name: Configure compile-only App ID + working-directory: macOS + run: | + perl -0pi -e 's/<#YOUR AppId#>/"$ENV{PLACEHOLDER_APP_ID}"/g' APIExample/Common/KeyCenter.swift + ! grep -Eq '<#[^#]+#>' APIExample/Common/KeyCenter.swift + - name: Install CocoaPods dependencies + working-directory: macOS + run: pod install --repo-update + - name: Compile + working-directory: macOS + run: | + xcodebuild \ + -workspace APIExample.xcworkspace \ + -scheme APIExample \ + -configuration Debug \ + -derivedDataPath "$RUNNER_TEMP/macos" \ + CODE_SIGNING_ALLOWED=NO \ + build + + windows: + name: Windows + runs-on: windows-2022 + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + - uses: microsoft/setup-msbuild@v2 + - name: Install public dependencies + working-directory: windows/APIExample + shell: pwsh + run: ./install.ps1 + - name: Compile + working-directory: windows/APIExample + shell: pwsh + run: msbuild APIExample.sln -m /p:Configuration=Release /p:Platform=Win32 diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index 9519427e1..1eae39dfd 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -34,18 +34,28 @@ jobs: ref: main fetch-depth: 0 + - name: Validate tag name + env: + TAG: ${{ inputs.tag }} + run: git check-ref-format "refs/tags/$TAG" + - name: Push tag if: inputs.action == 'push' + env: + TAG: ${{ inputs.tag }} + TAG_MESSAGE: ${{ inputs.message }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - if [ -n "${{ inputs.message }}" ]; then - git tag -a "${{ inputs.tag }}" -m "${{ inputs.message }}" + if [ -n "$TAG_MESSAGE" ]; then + git tag -a -m "$TAG_MESSAGE" -- "$TAG" else - git tag "${{ inputs.tag }}" + git tag -- "$TAG" fi - git push origin "${{ inputs.tag }}" + git push origin "refs/tags/$TAG:refs/tags/$TAG" - name: Delete tag if: inputs.action == 'delete' - run: git push origin --delete "${{ inputs.tag }}" \ No newline at end of file + env: + TAG: ${{ inputs.tag }} + run: git push origin ":refs/tags/$TAG" diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml deleted file mode 100644 index 48a5cb8eb..000000000 --- a/.github/workflows/mobile-ci.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Mobile CI - -on: - pull_request: - branches: - - main - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: mobile-ci-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - android-static: - name: Android Static Checks - runs-on: ubuntu-latest - defaults: - run: - working-directory: Android/APIExample - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: "17" - cache: gradle - cache-dependency-path: Android/APIExample/**/*.gradle* - - uses: android-actions/setup-android@v3 - - name: Configure Android build - run: printf 'AGORA_APP_ID=00000000000000000000000000000000\n' > local.properties - - name: Validate Gradle configuration - run: ./gradlew :app:tasks --all - - name: Validate Android XML resources - run: python3 -c 'import pathlib, xml.etree.ElementTree as ET; [ET.parse(path) for path in pathlib.Path("app/src/main").rglob("*.xml")]' - - ios-static: - name: iOS Static Checks - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - name: Validate Xcode projects - run: plutil -lint iOS/APIExample/APIExample.xcodeproj/project.pbxproj macOS/APIExample.xcodeproj/project.pbxproj - - name: Parse Swift sources - run: find iOS/APIExample/APIExample macOS/APIExample -name '*.swift' ! -name 'KeyCenter.swift' -print0 | xargs -0 xcrun swiftc -frontend -parse - - name: Validate localizations - run: find iOS/APIExample/APIExample macOS/APIExample -name '*.strings' -print0 | xargs -0 plutil -lint diff --git a/.github/workflows/repository-policy.yml b/.github/workflows/repository-policy.yml new file mode 100644 index 000000000..6f0e92878 --- /dev/null +++ b/.github/workflows/repository-policy.yml @@ -0,0 +1,207 @@ +name: Repository Policy + +on: + push: + branches: + - main + - 'dev/**' + pull_request: + +permissions: + contents: read + +concurrency: + group: repository-policy-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + gitleaks: + name: Sensitive information + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve commits to scan + id: commits + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PUSH_HEAD_SHA: ${{ github.sha }} + run: | + set -euo pipefail + + if [[ "$EVENT_NAME" == "pull_request" ]]; then + base_sha="$PR_BASE_SHA" + head_sha="$PR_HEAD_SHA" + else + base_sha="$BEFORE_SHA" + head_sha="$PUSH_HEAD_SHA" + fi + + if [[ ! "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "Invalid head commit: $head_sha" >&2 + exit 1 + fi + git cat-file -e "${head_sha}^{commit}" + + zero_sha="0000000000000000000000000000000000000000" + if [[ "$base_sha" == "$zero_sha" || ! "$base_sha" =~ ^[0-9a-f]{40}$ ]] || ! git cat-file -e "${base_sha}^{commit}" 2>/dev/null; then + git fetch --no-tags origin "+refs/heads/$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH" + base_sha="$(git merge-base "$head_sha" "refs/remotes/origin/$DEFAULT_BRANCH" || true)" + fi + + if [[ -n "$base_sha" ]]; then + revision="$base_sha..$head_sha" + else + revision="$head_sha" + fi + echo "revision=$revision" >> "$GITHUB_OUTPUT" + + - name: Scan commits with Gitleaks + uses: docker://ghcr.io/gitleaks/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f + with: + args: git --redact --verbose --config=.gitleaks.toml --log-opts=${{ steps.commits.outputs.revision }} . + + - name: Create Gitleaks regression fixtures + run: | + set -euo pipefail + mkdir -p .gitleaks-fixtures/positive .gitleaks-fixtures/negative + + sample_id="0123456789abcdef" + sample_id="${sample_id}fedcba9876543210" + zero_id="0000000000000000" + zero_id="${zero_id}${zero_id}" + + printf 'static let AppId: String = "%s"\n' "$sample_id" > .gitleaks-fixtures/positive/swift-app-id.swift + printf 'static NSString * const APPID = @"%s";\n' "$sample_id" > .gitleaks-fixtures/positive/objc-app-id.m + printf 'static NSString * const Certificate = @"%s";\n' "$sample_id" > .gitleaks-fixtures/positive/objc-certificate.m + printf '#define APP_ID "%s"\n' "$sample_id" > .gitleaks-fixtures/positive/windows-app-id.h + printf 'AGORA_APP_CERT=%s\n' "$sample_id" > .gitleaks-fixtures/positive/android-certificate.properties + printf 'static let AppId: String = "%s"\n' "$zero_id" > .gitleaks-fixtures/negative/zero-app-id.swift + + - name: Scan representative credential formats + id: credential-formats + continue-on-error: true + uses: docker://ghcr.io/gitleaks/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f + with: + args: dir --no-banner --redact --config=.gitleaks.toml --report-format=json --report-path=.gitleaks-positive.json .gitleaks-fixtures/positive + + - name: Verify representative credentials were detected + env: + SCAN_OUTCOME: ${{ steps.credential-formats.outcome }} + run: | + python3 - <<'PY' + import json + import os + import sys + from pathlib import Path + + expected = { + "swift-app-id.swift", + "objc-app-id.m", + "objc-certificate.m", + "windows-app-id.h", + "android-certificate.properties", + } + report_path = Path(".gitleaks-positive.json") + findings = json.loads(report_path.read_text()) if report_path.exists() else [] + detected = {Path(item["File"]).name for item in findings} + missing = expected - detected + if os.environ["SCAN_OUTCOME"] != "failure" or missing: + print(f"Gitleaks regression failed; missing detections: {sorted(missing)}", file=sys.stderr) + raise SystemExit(1) + PY + + - name: Verify placeholder App ID is allowed + uses: docker://ghcr.io/gitleaks/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f + with: + args: dir --no-banner --redact --config=.gitleaks.toml .gitleaks-fixtures/negative + + commit-messages: + name: Commit messages + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve and validate commit messages + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PUSH_HEAD_SHA: ${{ github.sha }} + run: | + set -euo pipefail + + if [[ "$EVENT_NAME" == "pull_request" ]]; then + base_sha="$PR_BASE_SHA" + head_sha="$PR_HEAD_SHA" + else + base_sha="$BEFORE_SHA" + head_sha="$PUSH_HEAD_SHA" + fi + + if [[ ! "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then + echo "Invalid head commit: $head_sha" >&2 + exit 1 + fi + git cat-file -e "${head_sha}^{commit}" + + zero_sha="0000000000000000000000000000000000000000" + if [[ "$base_sha" == "$zero_sha" || ! "$base_sha" =~ ^[0-9a-f]{40}$ ]] || ! git cat-file -e "${base_sha}^{commit}" 2>/dev/null; then + git fetch --no-tags origin "+refs/heads/$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH" + base_sha="$(git merge-base "$head_sha" "refs/remotes/origin/$DEFAULT_BRANCH" || true)" + fi + + if [[ -n "$base_sha" ]]; then + revision="$base_sha..$head_sha" + else + revision="$head_sha" + fi + + message_file="$RUNNER_TEMP/commit-message.txt" + failed=0 + while IFS= read -r commit_sha; do + git show -s --format=%B "$commit_sha" > "$message_file" + if ! .git-hooks/check-commit-message.sh "$message_file"; then + echo "Commit $commit_sha has an invalid message." >&2 + failed=1 + fi + done < <(git rev-list --reverse "$revision") + exit "$failed" + + ai-assets: + name: AI asset integrity + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: .github/ci/policy/requirements.txt + + - name: Install AI asset validation dependencies + run: python3 -m pip install --requirement .github/ci/policy/requirements.txt + + - name: Test AI asset validator + run: python3 -m unittest discover --start-directory .github/ci/policy/tests --pattern 'test_validate_ai_assets.py' + + - name: Validate AI assets + run: python3 .github/ci/policy/validate_ai_assets.py diff --git a/.gitignore b/.gitignore index ea918dca3..e6cd698e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ xcuserdata .DS_Store +.venv/ AgoraRtcKit.framework */libs /sdk diff --git a/.gitleaks.toml b/.gitleaks.toml index b515f4b0c..4a15002b9 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -36,14 +36,14 @@ useDefault = true [[rules]] id = "agora-app-id-pattern" description = "Agora App ID Pattern" -regex = '''(?i)(AGORA_APP_ID|AG_APP_ID|static\s+let\s+AppId:\s+String|static\s+let\s+AG_APP_ID:\s+String)(\s*=\s*)(?:['"]?([0-9a-zA-Z]{1,32})['"]?)''' -secretGroup = 3 +regex = '''(?i)(?:AGORA_APP_ID|AG_APP_ID|APP_?ID)(?:\s*:\s*String)?\s*(?:=|\s)\s*@?['"]?([0-9a-zA-Z]{32})''' +secretGroup = 1 [[rules]] id = "agora-app-certificate-pattern" description = "Agora App Certificate Pattern" -regex = '''(?i)(AGORA_APP_CERTIFICATE|AG_APP_CERTIFICATE|static\s+let\s+Certificate:\s+String\?|static\s+let\s+AG_APP_CERTIFICATE:\s+String)(\s*=\s*)(?:['"]?([0-9a-zA-Z]{1,32})['"]?)''' -secretGroup = 3 +regex = '''(?i)(?:AGORA_APP_CERTIFICATE|AG_APP_CERTIFICATE|AGORA_APP_CERT|AG_APP_CERT|Certificate)(?:\s*:\s*String\??)?\s*=\s*@?['"]?([0-9a-zA-Z]{32})''' +secretGroup = 1 [[rules]] id = "basic-auth-key" @@ -112,7 +112,7 @@ regex = '''(?i)sk-(live|test|proj)-[0-9a-zA-Z]{24,48}''' [allowlist] description = "global allow lists" -regexes = ['''219-09-9999''', '''078-05-1120''', '''(9[0-9]{2}|666)-\d{2}-\d{4}'''] +regexes = ['''^0{32}$''', '''219-09-9999''', '''078-05-1120''', '''(9[0-9]{2}|666)-\d{2}-\d{4}'''] paths = [ '''gitleaks.toml''', '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''', diff --git a/AGENTS.md b/AGENTS.md index 8c09b3d7b..950c690ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,14 +27,14 @@ Each platform directory contains its own `AGENTS.md` with platform-specific rule ## AI Engineering Workflow -For AI-assisted version iteration, product-to-implementation routing, multi-agent acceptance, or automatic API example generation, use the repository workflow in `.agents/skills/api-example-release-iteration/SKILL.md`. +For a requirement that may affect more than one project, use +`.agents/skills/cross-platform-api-change/SKILL.md`. It requires one shared behavior contract, +an explicit decision for all nine projects, per-project validation, and a final +cross-platform comparison. -Supporting documents: -- `docs/ai-engineering/knowledge-index.md` - repository knowledge map for agents. -- `docs/ai-engineering/release-iteration-gate.md` - product, architecture, review, test, and UX acceptance gates. -- `docs/ai-engineering/role-routing.json` - shared Contract and independent platform role types with logical Codex profiles. -- `docs/ai-engineering/repository-profile.json` - repository-specific SDK package names and version sources used by the shared tools. -- `docs/ai-engineering/release-known-issues.md` - repository release-risk reference. +Durable workflow documentation lives in `docs/ai/`. Start with `docs/ai/README.md`; use +`docs/ai/change-contract-template.md` before implementation and record the final scope and +evidence in `.github/pull_request_template.md`. ## Cross-Platform Rules @@ -42,7 +42,7 @@ Supporting documents: 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. +5. SDK event/delegate callbacks may arrive on a background thread — follow the selected project's threading rules and keep UI or state updates framework-safe. ## Repository-Level Files @@ -51,7 +51,10 @@ Supporting documents: | `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 | +| `.github/workflows/compile.yml` | Post-release compile-only checks using a placeholder App ID | +| `.github/workflows/repository-policy.yml` | Remote sensitive-information, commit-message, and AI asset checks | +| `.github/ci/build/` | Jenkins release packaging entry points | +| `.github/ci/README.md` | CI ownership, entry points, and local verification | ## Git Hooks @@ -71,6 +74,8 @@ Never commit a real App ID, App Certificate, or token. The mechanism differs by | Android | `local.properties` at each project root, key `AGORA_APP_ID`, read by `app/build.gradle`(`.kts`) into `BuildConfig.AGORA_APP_ID` | No — `local.properties` is git-ignored | | iOS | `/Common/KeyCenter.swift`, or `KeyCenter.h` / `KeyCenter.m` for `APIExample-OC` | **Yes** | | macOS | `APIExample/Common/KeyCenter.swift` | **Yes** | -| Windows | `APIExample/APIExample/CConfig.h` and `CConfig.cpp` | **Yes** | +| Windows | `windows/APIExample/APIExample/stdafx.h` for the compile-time `APP_ID`; `CConfig.cpp` also reads `AppID.ini` beside the built executable when the placeholder remains | `stdafx.h`: **Yes**; generated `AppID.ini`: **No** | The iOS, macOS, and Windows credential files are tracked placeholder files, not git-ignored. Editing one to build locally leaves a real credential in a tracked file, so never stage it. The safety net is the gitleaks pre-commit hook (see Git Hooks above), not `.gitignore` — do not rely on git ignoring these paths. + +GitHub Actions compilation uses an all-zero 32-character placeholder App ID and does not run RTC sessions. It must not use repository secrets, App Certificates, or tokens. The compile workflow runs automatically only after changes reach `main`, when the referenced SDK version is available from public package repositories. diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md index a81c131d3..bfc8c3491 100644 --- a/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -24,13 +24,13 @@ Use this skill to add or update a case in `APIExample-Audio/`. It owns the full 3. `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**` 4. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml` 5. `APIExample-Audio/app/src/main/res/values/strings.xml` -6. `APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java` +6. [fragment-template.java](references/fragment-template.java) ## Procedure 1. Run `query-cases` first when the case index, group placement, or nearby examples are unknown. 2. Create or update the Fragment, layout, strings, nav entries, and `ARCHITECTURE.md` together so the case stays fully registered. -3. Use `APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java` as the reference for engine lifecycle, usage reporting, private-cloud setup, and voice-only guardrails. +3. Use the reference template above for engine lifecycle, usage reporting, private-cloud setup, and voice-only guardrails. 4. Keep `@Example`, action ID, destination ID, string resources, and audio-only constraints aligned across the implementation. ## Verify diff --git a/Android/APIExample-Audio/ci.env.py b/Android/APIExample-Audio/ci.env.py deleted file mode 100644 index 645cf0b1a..000000000 --- a/Android/APIExample-Audio/ci.env.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -from pathlib import Path - - -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 - - -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__": - main() diff --git a/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md index f2e2498e3..5e1d495b2 100644 --- a/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md +++ b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md @@ -32,7 +32,7 @@ Use this skill after a Compose case has been created or modified in `APIExample- - Run `./gradlew assembleDebug` from `APIExample-Compose/` - Confirm `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` are aligned -- Confirm engine lifecycle and cleanup are owned by `DisposableEffect(lifecycleOwner)`, and `rememberSaveable` / `remember` boundaries still match current project practice +- Confirm the effect cleans up the engine it captured, its keys match engine ownership, and live session state resets with engine recreation. A constant `Unit` key also disposes on leaving composition; do not reject it solely for being constant. ## Out of scope @@ -41,6 +41,6 @@ Use this skill after a Compose case has been created or modified in `APIExample- ## Never -- Never approve `DisposableEffect(Unit)` for case cleanup +- Never approve effect-key changes that destroy an engine while the case continues to reuse that same remembered instance - Never approve `RtcEngine` stored in `rememberSaveable` - Never skip the build command diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md index 7224860c6..f9f4fd1c4 100644 --- a/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md @@ -20,14 +20,14 @@ Use this skill to add or update a case in `APIExample-Compose/`. It owns the ful 3. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt` 4. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/**` 5. `APIExample-Compose/app/src/main/res/values/strings.xml` -6. `APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt` +6. [composable-template.kt](references/composable-template.kt) ## Procedure 1. Run `query-cases` first when the target list placement or the closest nearby examples are unclear. 2. Create or update the Composable source, `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` together as one change set. 3. When the case title is user-facing, update both `res/values/strings.xml` and `res/values-zh/strings.xml` to keep locale coverage aligned with current project practice. -4. Use `APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt` for Compose state, lifecycle, permission, and registration patterns. +4. Use the reference template above for Compose state, lifecycle, permission, and registration patterns. 5. Keep the public stateful Composable, the private preview/view split, and the `Examples.kt` plus string registration alignment consistent. 6. Treat this skill as the current source of truth for case-creation closure even if older sections in `ARCHITECTURE.md` still describe a smaller file set; update `ARCHITECTURE.md` as part of the same change. @@ -35,7 +35,7 @@ Use this skill to add or update a case in `APIExample-Compose/`. It owns the ful - Run `./gradlew assembleDebug` from `APIExample-Compose/` - Confirm the edited case updates `Examples.kt`, `strings.xml`, `values-zh/strings.xml` when needed, and `ARCHITECTURE.md` -- Confirm the state model matches current project practice: `rememberSaveable` for UI/session state shown in the canonical Compose samples, and `remember` for `RtcEngine` and other non-serializable objects +- Use `rememberSaveable` for user inputs, and keep joined status/assigned UID in `remember` with the current engine; a recreated engine starts without an RTC session. Keep `RtcEngine` and other non-serializable objects out of saved state. - Confirm the public Composable stays stateful and the preview stays on the private view function ## Out of scope @@ -46,5 +46,5 @@ Use this skill to add or update a case in `APIExample-Compose/`. It owns the ful ## Never - Never use `rememberSaveable` for `RtcEngine` -- Never use `DisposableEffect(Unit)` for teardown +- Never key teardown to a changing object unless engine creation follows the same ownership changes; `DisposableEffect(rtcEngine)` pairs cleanup with a remembered engine - Never hardcode `mAreaCode` diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt index 613c9c251..250d5a3f6 100644 --- a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.tooling.preview.Preview import io.agora.api.example.compose.BuildConfig import io.agora.api.example.compose.data.SettingPreferences @@ -26,10 +25,9 @@ import io.agora.rtc2.RtcStats @Composable fun ExampleCase() { val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current var channelName by rememberSaveable { mutableStateOf("") } - var isJoined by rememberSaveable { mutableStateOf(false) } - var localUid by rememberSaveable { mutableIntStateOf(0) } + var isJoined by remember { mutableStateOf(false) } + var localUid by remember { mutableIntStateOf(0) } val rtcEngine = remember { RtcEngine.create(RtcEngineConfig().apply { @@ -54,11 +52,9 @@ fun ExampleCase() { }) } - DisposableEffect(lifecycleOwner) { + DisposableEffect(rtcEngine) { onDispose { - if (isJoined) { - rtcEngine.leaveChannel() - } + rtcEngine.leaveChannel() RtcEngine.destroy() } } diff --git a/Android/APIExample-Compose/AGENTS.md b/Android/APIExample-Compose/AGENTS.md index 39b131843..0102f3284 100644 --- a/Android/APIExample-Compose/AGENTS.md +++ b/Android/APIExample-Compose/AGENTS.md @@ -20,7 +20,8 @@ See [README.md — Obtain an App Id](README.md#obtain-an-app-id). - 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. +- Keep one remembered `RtcEngine` per case and pair it with `DisposableEffect(rtcEngine) { onDispose { } }`. The effect must clean up the exact engine it owns. `onDispose` runs when the effect leaves composition or a key changes; `Unit` is valid when ownership is constant. Use a lifecycle-owner key only when engine creation and teardown both follow that owner's lifetime. +- Save user inputs with `rememberSaveable`; keep live session state such as joined status and assigned UID with the current engine so it resets when that engine is recreated. - 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). diff --git a/Android/APIExample-Compose/ARCHITECTURE.md b/Android/APIExample-Compose/ARCHITECTURE.md index d732ef645..41db317fc 100644 --- a/Android/APIExample-Compose/ARCHITECTURE.md +++ b/Android/APIExample-Compose/ARCHITECTURE.md @@ -166,13 +166,13 @@ MyNewCase() ← public, stateful: owns RtcEngine, state, per val rtcEngine = remember { RtcEngine.create(RtcEngineConfig().apply { mContext = context - mAppId = AgoraConfig.getAppId() + mAppId = BuildConfig.AGORA_APP_ID mEventHandler = object : IRtcEngineEventHandler() { … } }) } -DisposableEffect(lifecycleOwner) { // key must be lifecycleOwner, not Unit +DisposableEffect(rtcEngine) { // cleanup belongs to this engine onDispose { - if (isJoined) rtcEngine.leaveChannel() + rtcEngine.leaveChannel() RtcEngine.destroy() } } @@ -190,10 +190,14 @@ permissionLauncher.launch(arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.per ``` **State rules:** -- `rememberSaveable` — values that must survive rotation (channelName, isJoined, uid) -- `remember` — objects that must not be recreated (RtcEngine, collections) +- `rememberSaveable` — user inputs that should survive recreation, such as channelName +- `remember` — the engine, non-serializable objects, and live session state (isJoined, assigned uid); a new engine starts without a session - `IRtcEngineEventHandler` callbacks can mutate Compose state directly — the snapshot system is thread-safe +`onDispose` runs when its effect leaves composition, even with a constant `Unit` key. +Keys control effect replacement. If a lifecycle owner becomes a key, engine creation must +follow the same ownership changes so the UI never reuses an engine that was just destroyed. + ## Token Flow ```kotlin diff --git a/Android/APIExample/.agents/skills/review-case/SKILL.md b/Android/APIExample/.agents/skills/review-case/SKILL.md index f851a1e74..4eb431f07 100644 --- a/Android/APIExample/.agents/skills/review-case/SKILL.md +++ b/Android/APIExample/.agents/skills/review-case/SKILL.md @@ -22,7 +22,7 @@ Use this skill after a case has been created or modified in `APIExample/`. It ch ## Procedure -1. Audit lifecycle, permission, threading, and registration rules. +1. Audit lifecycle, permission, threading, and registration rules. Trace the Handler's Looper when assessing teardown: the BaseFragment handler runs on the main thread, including posted destroy calls. Verify destruction cannot overlap the next engine initialization. 2. Check `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md` for closure. 3. Run the minimum build verification command. 4. Report findings first, then verification results, then explicit unverified items. @@ -40,6 +40,7 @@ Use this skill after a case has been created or modified in `APIExample/`. It ch ## Never -- Never approve direct `RtcEngine.destroy()` on the main thread +- Never describe `handler.post(RtcEngine::destroy)` as moving work off the main thread +- Never approve destruction inside an SDK callback or asynchronous teardown that races the next engine initialization - Never approve missing `leaveChannel()` before destroy - Never skip the build command diff --git a/Android/APIExample/.agents/skills/upsert-case/SKILL.md b/Android/APIExample/.agents/skills/upsert-case/SKILL.md index 255a6e65b..219efc5df 100644 --- a/Android/APIExample/.agents/skills/upsert-case/SKILL.md +++ b/Android/APIExample/.agents/skills/upsert-case/SKILL.md @@ -22,7 +22,7 @@ Use this skill to add a new case or update an existing case in `APIExample/`. It 3. `APIExample/app/src/main/java/io/agora/api/example/examples/**` 4. `APIExample/app/src/main/res/navigation/nav_graph.xml` 5. `APIExample/app/src/main/res/values/strings.xml` -6. `APIExample/.agents/skills/upsert-case/references/fragment-template.java` +6. [fragment-template.java](references/fragment-template.java) ## Procedure @@ -47,5 +47,6 @@ Use this skill to add a new case or update an existing case in `APIExample/`. It - Never omit `setParameters(...)` - Never skip the `getPrivateCloudConfig()` null-check -- Never call `RtcEngine.destroy()` directly on the main thread +- Never call `RtcEngine.destroy()` inside an SDK callback or let pending destruction overlap the next engine initialization +- Never assume `handler.post` runs destruction on a worker; BaseFragment's handler uses the main Looper - Never stop at “it compiles” without updating registration and `ARCHITECTURE.md` diff --git a/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java index 2d029e12d..27eb9a868 100644 --- a/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java +++ b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java @@ -40,6 +40,8 @@ public void onDestroy() { if (engine != null) { engine.leaveChannel(); } + // BaseFragment's handler targets the main Looper. This defers work; it does not + // move destruction to a worker. Finish teardown before initializing another case. handler.post(RtcEngine::destroy); engine = null; } diff --git a/Android/APIExample/AGENTS.md b/Android/APIExample/AGENTS.md index e142a46a1..2dd3ead3f 100644 --- a/Android/APIExample/AGENTS.md +++ b/Android/APIExample/AGENTS.md @@ -30,7 +30,7 @@ Both are `false` by default. Do not enable unless the feature explicitly require - 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). +- Existing cases defer `RtcEngine.destroy()` with `handler.post(RtcEngine::destroy)`. This handler uses the main Looper: posting defers work but does not move it off the UI thread. Never destroy from an SDK callback, and finish destruction before creating the next engine. If moving destruction to a worker, serialize teardown and subsequent initialization explicitly. - 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. diff --git a/Android/APIExample/ci.env.py b/Android/APIExample/ci.env.py deleted file mode 100644 index 872df41ae..000000000 --- a/Android/APIExample/ci.env.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -from pathlib import Path - - -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 - -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__": - main() diff --git a/Android/build-template/build-android.yml b/Android/build-template/build-android.yml deleted file mode 100644 index 5ee783a80..000000000 --- a/Android/build-template/build-android.yml +++ /dev/null @@ -1,41 +0,0 @@ -parameters: - displayName: '' - project: '' - module: '' - name: '' - -jobs: - - - job: ${{ parameters.name }}_Build - displayName: ${{ parameters.name }} - pool: - vmImage: 'ubuntu-latest' - variables: - - group: AgoraKeys - steps: - - script: cd ${{ parameters.project }} && ls && python ci.env.py && echo $(agora.appId) - env: - AGORA_APP_ID: $(agora.appId) - - - task: Gradle@2 - inputs: - workingDirectory: ${{ parameters.project }}/${{ parameters.module }} - gradleWrapperFile: ${{ parameters.project }}/gradlew - options: '--stacktrace' - jdkVersionOption: '1.11' - gradleOptions: '-Xmx3072m' - publishJUnitResults: false - testResultsFiles: '**/TEST-*.xml' - tasks: 'assembleDebug' - - - task: CopyFiles@2 - inputs: - Contents: '**/*.apk' - TargetFolder: '$(Build.ArtifactStagingDirectory)' - - - script: cd $(Build.ArtifactStagingDirectory) && zip -r ${{ parameters.name }}.zip . - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)/${{ parameters.name }}.zip' - ArtifactName: ${{ parameters.name }} diff --git a/HOOKS-GUIDE.md b/HOOKS-GUIDE.md index 8e39e4af4..bd40b5f28 100644 --- a/HOOKS-GUIDE.md +++ b/HOOKS-GUIDE.md @@ -7,6 +7,12 @@ This repository uses pre-commit to run two local checks: Chinese content in source files and documentation is allowed. It is not part of the commit-message rule. +GitHub Actions repeats both policies for every pull request and for pushes to `main` and `dev/**`; +see `.github/workflows/repository-policy.yml`. A repository administrator must add its +`Sensitive information`, `Commit messages`, and `AI asset integrity` jobs to branch protection +before they become required merge checks. The AI asset check runs in CI; run it locally using +the virtual-environment commands in [docs/ai/README.md](docs/ai/README.md). + ## Prerequisites Install these tools before installing the hooks: diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 721b9d380..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Xcode -# Build, test, and archive an Xcode workspace on macOS. -# Add steps that install certificates, test, sign, and distribute an app, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/xcode - -trigger: -- master -- dev/* -#- dev/cjw - -pool: - vmImage: 'macos-latest' - -jobs: -- template: cicd/build-template/build-ios.yml - parameters: - displayName: 'APIExampleiOS' - workingDirectory: 'iOS/APIExample' - project: 'APIExample' - scheme: 'APIExample' - -#- template: cicd/build-template/build-mac.yml -#parameters: -#displayName: 'APIExampleMac' -#workingDirectory: 'iOS/APIExample' -#project: 'APIExample' -#scheme: 'APIExample' - -- template: cicd/build-template/build-ios.yml - parameters: - displayName: 'APIExampleiOS_Audio' - workingDirectory: 'iOS/APIExample-Audio' - project: 'APIExample-Audio' - scheme: 'APIExample-Audio' - -- template: ./Android/build-template/build-android.yml - parameters: - project: 'Android/APIExample' - module: 'app' - name: 'APIExampleAndroid' - -- template: ./Android/build-template/build-android.yml - parameters: - project: 'Android/APIExample-Audio' - module: 'app' - name: 'APIExampleAndroid_Audio' - -- template: windows/cicd/templates/build-Windows.yml - parameters: - displayName: 'ApiExampleWindows' - workingDirectory: 'windows/APIExample' - scheme: 'APIExample' - solutionName: 'APIExample.sln' - sdkVersion: '3_0_1' - Machine: Win32 - release: 'Release' - diff --git a/cicd/build-template/build-ios.yml b/cicd/build-template/build-ios.yml deleted file mode 100644 index 9a9ab7a62..000000000 --- a/cicd/build-template/build-ios.yml +++ /dev/null @@ -1,25 +0,0 @@ -parameters: - displayName: '' - workingDirectory: '' - project: '' - scheme: '' - -jobs: - - job: ${{ parameters.displayName }}Build - displayName: ${{ parameters.displayName }} - - pool: - vmImage: 'macOS-latest' - - variables: - - group: AgoraKeys - - steps: - - script: cd 'cicd/scripts' && ls && python keycenter.py && ls - displayName: 'Generate KeyCenter' - env: - AGORA_APP_ID: $(agora.appId) - File_Directory: '../../${{ parameters.workingDirectory }}/${{ parameters.project }}/Common' - - - script: cd 'cicd/scripts' && chmod +x ios_build.sh && ./ios_build.sh ../../${{ parameters.workingDirectory }} ${{ parameters.project }} ${{ parameters.scheme }} - displayName: 'Build without code signing' diff --git a/cicd/build-template/build-mac.yml b/cicd/build-template/build-mac.yml deleted file mode 100644 index 53c3ba822..000000000 --- a/cicd/build-template/build-mac.yml +++ /dev/null @@ -1,41 +0,0 @@ -parameters: - displayName: '' - workingDirectory: '' - project: '' - scheme: '' - -jobs: - - job: ${{ parameters.displayName }}Build - displayName: ${{ parameters.displayName }} - - pool: - vmImage: 'macOS-latest' - - variables: - - group: AgoraKeys - - steps: - - script: cd 'cicd/scripts' && ls && python keycenter.py && ls - env: - AGORA_APP_ID: $(agora.appId) - File_Directory: '../../${{ parameters.workingDirectory }}/${{ parameters.project }}/Common' - - - task: InstallAppleCertificate@2 - inputs: - certSecureFile: 'App.p12' - certPwd: $(agora.password) - - - task: InstallAppleProvisioningProfile@1 - inputs: - provProfileSecureFile: 'App.mobileprovision' - - - script: cd 'cicd/scripts' && chmod +x ios_build.sh && ./mac_build.sh ../../${{ parameters.workingDirectory }} ${{ parameters.project }} ${{ parameters.scheme }} - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: ${{ parameters.workingDirectory }}/app - ArtifactName: ${{ parameters.displayName }} - - - template: github-release.yml - parameters: - displayName: ${{ parameters.displayName }} diff --git a/cicd/build-template/github-release.yml b/cicd/build-template/github-release.yml deleted file mode 100644 index 46e5c7aa9..000000000 --- a/cicd/build-template/github-release.yml +++ /dev/null @@ -1,3 +0,0 @@ -parameters: - displayName: '' - diff --git a/cicd/scripts/ios_build.sh b/cicd/scripts/ios_build.sh deleted file mode 100755 index 56d939c10..000000000 --- a/cicd/scripts/ios_build.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -WORKING_PATH="$1" -APP_PROJECT="$2" -APP_TARGET="$3" -MODE=Release - -echo "WORKING_PATH: ${WORKING_PATH}" -echo "APP_TARGET: ${APP_TARGET}" - -cd "${WORKING_PATH}" -pwd - -if [[ -f Podfile ]]; then - pod install - project_args=(-workspace "${APP_PROJECT}.xcworkspace") -else - project_args=(-project "${APP_PROJECT}.xcodeproj") -fi - -xcodebuild \ - "${project_args[@]}" \ - -scheme "${APP_TARGET}" \ - -configuration "${MODE}" \ - -destination 'generic/platform=iOS' \ - CODE_SIGNING_ALLOWED=NO \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGN_IDENTITY= \ - DEVELOPMENT_TEAM= \ - PROVISIONING_PROFILE_SPECIFIER= \ - clean build diff --git a/cicd/scripts/keycenter.py b/cicd/scripts/keycenter.py deleted file mode 100644 index e6d912403..000000000 --- a/cicd/scripts/keycenter.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/python -# -*- coding: UTF-8 -*- -import re -import os - -def main(): - appId = "" - if "AGORA_APP_ID" in os.environ: - appId = os.environ["AGORA_APP_ID"] - token = "" - - fileDirectory = "" - if "File_Directory" in os.environ: - fileDirectory = os.environ["File_Directory"] - - # KeyCenter.swift - KeyCenterPath = fileDirectory + "/KeyCenter.swift" - print("KeyCenterPath: %s" %KeyCenterPath) - - try: - f = open(KeyCenterPath, 'r+') - content = f.read() - appString = "\"" + appId + "\"" - tokenString = "\"" + token + "\"" - contentNew = re.sub(r'<#YOUR APPID#>', appString, content) - contentNew = re.sub(r'<#YOUR AppId#>', appString, contentNew) - contentNew = re.sub(r'<#YOUR Certificate#>', tokenString, contentNew) - f.seek(0) - f.write(contentNew) - f.truncate() - except IOError: - print("Swift File is not accessible.") - - # KeyCenter.m - KeyCenterPath = fileDirectory + "/KeyCenter.m" - - try: - f = open(KeyCenterPath, 'r+') - content = f.read() - appString = "@\"" + appId + "\"" - tokenString = "@\"" + token + "\"" - contentNew = re.sub(r'<#Your App Id#>', appString, content) - contentNew = re.sub(r'<#YOUR APPID#>', appString, contentNew) - contentNew = re.sub(r'<#Temp Access Token#>', tokenString, contentNew) - f.seek(0) - f.write(contentNew) - f.truncate() - except IOError: - print("OC File is not accessible.") - -if __name__ == "__main__": - main() diff --git a/cicd/scripts/mac_build.sh b/cicd/scripts/mac_build.sh deleted file mode 100755 index 97b405bf4..000000000 --- a/cicd/scripts/mac_build.sh +++ /dev/null @@ -1,73 +0,0 @@ -WORKING_PATH=$1 -APP_Project=$2 -APP_TARGET=$3 -MODE=Release - -echo "WORKING_PATH: ${WORKING_PATH}" -echo "APP_TARGET: ${APP_TARGET}" - -cd ${WORKING_PATH} -echo `pwd` - -rm -f *.ipa -rm -rf *.app -rm -f *.zip -rm -rf dSYMs -rm -rf *.dSYM -rm -f *dSYMs.zip -rm -rf *.xcarchive - -Export_Plist_File=ExportOptions.plist - -BUILD_DATE=`date +%Y-%m-%d-%H.%M.%S` -ArchivePath=${APP_TARGET}-${BUILD_DATE}.xcarchive -PBXPROJ_PATH="${APP_Project}.xcodeproj/project.pbxproj" -echo PBXPROJ_PATH: $PBXPROJ_PATH - -# 主项目工程配置 -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF72448758C00B599B3:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03D13BF82448758C00B599B3:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH - -# 屏幕共享Extension -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB825205B80007D4FDD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:0339BEB925205B80007D4FDD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH - -# SimpleFilter -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1726AFFFA6002E1373:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8B10BE1826AFFFA6002E1373:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH - - -TARGET_FILE="" -if [ ! -f "Podfile" ];then -TARGET_FILE="${APP_Project}.xcodeproj" -xcodebuild clean -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -xcodebuild -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive -else -pod install -TARGET_FILE="${APP_Project}.xcworkspace" -xcodebuild clean -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -xcodebuild archive -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} -destination 'generic/platform=macOS' -fi - -xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . - -mkdir app -mv *.ipa app && mv *.xcarchive app diff --git a/docs/ai-engineering/case-maintenance-matrix.md b/docs/ai-engineering/case-maintenance-matrix.md deleted file mode 100644 index 1b9162bb8..000000000 --- a/docs/ai-engineering/case-maintenance-matrix.md +++ /dev/null @@ -1,126 +0,0 @@ -# API Examples Case Maintenance Matrix - -This matrix is the planning artifact for parity work across the four primary APIExample projects. It does not replace platform `AGENTS.md`, project `ARCHITECTURE.md`, or project `.agents/skills/*`; every row must be verified against the target project before implementation. - -## How To Use - -1. Start from the product request and identify the feature name, SDK family, and key SDK APIs. -2. Find the matching row in this matrix. -3. For each target platform unit, verify the path with the project `query-cases` skill when present. -4. Treat each of the four primary projects as an independent delivery/verification unit inside one requirement-level acceptance manifest. -5. Update this matrix when a case is added, moved, renamed, or confirmed not applicable in a primary project. - -## Status Values - -| Status | Meaning | -| --- | --- | -| `DONE(path)` | Inventory says the case is implemented and expected to be registered at the listed path. Verify before editing. For new or changed parity work, use `DONE` only after the required reference, review, and build gates pass. | -| `HIDDEN(path)` | Source is retained at the listed path, but the case is intentionally not registered in the user-visible list. | -| `PARTIAL(note)` | Exists but parity, registration, docs, verification, runtime smoke, or API coverage needs review. | -| `MISSING` | Required for parity but no implementation is currently recorded here. | -| `N/A(reason)` | Not applicable because the SDK family or platform does not support it. | -| `UNKNOWN` | Not inventoried yet; run the project case query before making decisions. | - -## Active Parity Acceptance Rule - -For new or changed parity work, do not move a cell to `DONE` from compilation alone. - -The platform execution unit must record: - -- Reference Contract: source implementation, UI resources, registration, SDK overloads, enum/option values, defaults, limits, and expected success signals. -- Parity Checklist: final implementation matches the reference contract; similar cases are used only for framework patterns. -- Build Result: the target project compile/build command passes, or the exact blocker is recorded. - -If runtime/device behavior is required but unavailable, use `PARTIAL(...)` and note the pending smoke check instead of `DONE(...)`. - -## Platform Units - -Only these primary projects participate in repository-level parity planning and execution: - -| Unit | Project | Primary Source | -| --- | --- | --- | -| Android full | `Android/APIExample/` | `Android/APIExample/ARCHITECTURE.md` | -| iOS UIKit | `iOS/APIExample/` | `iOS/APIExample/ARCHITECTURE.md` | -| macOS | `macOS/` | `macOS/ARCHITECTURE.md` | -| Windows | `windows/` | `windows/ARCHITECTURE.md` | - -Audio, Compose, SwiftUI, and Objective-C variants are outside this matrix and do not create backlog or acceptance units. - -## Pilot Matrix - -Seeded from the primary project `ARCHITECTURE.md` indexes and source trees, then re-verified against `dev/4.7.0` (`ee77cc85`). Paths are relative to the projects listed in Platform Units. - -Inventory scope: the initial rows were derived from the `Android/APIExample/` case index and its counterparts in the other three primary projects. Features that exist only outside the Android primary project are not inventoried yet, including `ARKit`, `MutliCamera`/`MultiCameraSourece`, `RawMediaData`, `CustomVideoSourceMediaIO`, `VideoChat`, and Windows-only dialogs such as `AudioProfile`, `AudioVolume`, `RegionConn`, `ReportInCall`, `RTMPinject`, and `MultiVideoSource`. Their absence from this matrix is an inventory gap, not a coverage claim. - -| Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | -| --- | --- | --- | --- | --- | --- | --- | --- | -| Join channel video | Full RTC | `joinChannel`, `setupLocalVideo`, `setupRemoteVideo` | `DONE(basic/JoinChannelVideo.java)` | `DONE(Basic/JoinChannelVideo/)` | `DONE(Basic/JoinChannelVideo/)` | `PARTIAL(Basic/JoinChannelVideoByToken)` | Windows records the token variant only; non-token parity remains unverified. | -| Join channel video with token | Full RTC | `joinChannel` with token, local and remote video setup | `DONE(basic/JoinChannelVideoByToken.java)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideo(Token)/)` | `DONE(Basic/JoinChannelVideoByToken/)` | Verify token-generation behavior per platform before changing auth flow. | -| Join channel audio | Full RTC | `joinChannel`, `setAudioProfile`, audio routing or volume APIs | `DONE(basic/JoinChannelAudio.java)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | -| Live streaming role switch | Full RTC | `setClientRole`, `joinChannel`, streaming role configuration | `DONE(advanced/LiveStreaming.java)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Advanced/LiveStreaming/)` | `DONE(Basic/LiveBroadcasting/)` | Names differ by platform; keep product scenario wording stable. | -| RTMP or CDN streaming | Full RTC | `startRtmpStreamWithTranscoding`, `startRtmpStreamWithoutTranscoding`, `stopRtmpStream` | `DONE(advanced/RTMPStreaming.java)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStreaming/)` | `DONE(Advanced/RTMPStream/)` | Windows also has `RTMPinject`; verify the requested API before routing. | -| Media metadata | Full RTC | `registerMediaMetadataObserver` or platform metadata delegate APIs | `DONE(advanced/MediaMetadata.java)` | `DONE(Advanced/VideoMetadata/)` | `MISSING` | `DONE(Advanced/Metadata/)` | macOS has no metadata case. iOS names this VideoMetadata. | -| Data stream messaging | Full RTC | `createDataStream`, `sendStreamMessage` | `DONE(advanced/SendDataStream.java)` | `DONE(Advanced/CreateDataStream/)` | `DONE(Advanced/CreateDataStream/)` | `MISSING` | Windows has no data stream case. | -| Custom audio source | Full RTC | `createCustomAudioTrack`, `setExternalAudioSource`, `pushExternalAudioFrame` | `DONE(advanced/customaudio/CustomAudioSource.java)` | `DONE(Advanced/CustomPcmAudioSource/)` | `DONE(Advanced/CustomAudioSource/)` | `DONE(Advanced/CustomAudioCapture/)` | Platform APIs differ; compare demonstrated behavior, not only method names. | -| Custom audio render | Full RTC | `setExternalAudioSink`, pull playback audio frame APIs | `DONE(advanced/customaudio/CustomAudioRender.java)` | `DONE(Advanced/CustomAudioRender/)` | `DONE(Advanced/CustomAudioRender/)` | `MISSING` | Windows has no custom audio render case. | -| Custom video source | Full RTC | `setExternalVideoSource`, `pushExternalVideoFrame`, custom video track APIs | `DONE(advanced/PushExternalVideoYUV.java)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoSourcePush/)` | `DONE(Advanced/CustomVideoCapture/)` | Multi-source variants are tracked separately. | -| Raw audio data | Full RTC | `registerAudioFrameObserver`, audio frame delegate APIs | `DONE(advanced/ProcessAudioRawData.java)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/RawAudioData/)` | `DONE(Advanced/OriginalAudio/)` | Check callback threading in every platform review. | -| Raw video data | Full RTC | `registerVideoFrameObserver`, video frame delegate APIs | `DONE(advanced/ProcessRawData.java)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/RawVideoData/)` | `DONE(Advanced/OriginalVideo/)` | Raw media combined samples are tracked separately. | -| Screen sharing | Full RTC | `startScreenCapture`, `updateScreenCaptureParameters`, `stopScreenCapture` | `DONE(advanced/ScreenSharing.java)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | `DONE(Advanced/ScreenShare/)` | iOS requires a ReplayKit extension; Android requires a foreground service. | -| Media player | Full RTC | `createMediaPlayer`, `open`, `play`, publish/update channel options | `DONE(advanced/MediaPlayer.java)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | `DONE(Advanced/MediaPlayer/)` | Standalone media player coverage exists in all four primary projects. | -| Spatial audio | Full RTC | spatial audio engine, remote position, media player spatialization APIs | `DONE(advanced/SpatialSound.java)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | `DONE(Advanced/SpatialAudio/)` | iOS and Android architecture docs record different key APIs; verify the scenario first. | -| Stream encryption | Full RTC | `enableEncryption`, encryption configuration APIs | `DONE(advanced/ChannelEncryption.java)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/StreamEncryption/)` | `DONE(Advanced/CustomEncrypt/ + MediaEncrypt/)` | Windows has multiple encryption cases; route by requested API. | -| Pre-call test | Full RTC | `startEchoTest`, `stopEchoTest`, last-mile probe or network test APIs | `DONE(advanced/PreCallTest.java)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PrecallTest/)` | `DONE(Advanced/PreCallTest/)` | Primary-project coverage exists on all four platforms. | -| Third-party beauty | Full RTC | extension enablement, third-party SDK initialization, video frame processing | `DONE(advanced/ThirdPartyBeauty.java)` | `DONE(Advanced/ThirdBeautify/)` | `MISSING` | `MISSING` | Release work must check third-party license expiration. | -| Extension sample | Full RTC extension | `enableExtension`, extension property APIs, matching native headers | `DONE(advanced/SimpleExtension.java)` | `DONE(Advanced/SimpleFilter/)` | `DONE(Advanced/SimpleFilter/)` | `MISSING` | During SDK bumps, verify Android extension include files and Apple native assets. | -| Audio mixing | Full RTC | `startAudioMixing`, `stopAudioMixing`, `adjustAudioMixingVolume`, effect APIs | `DONE(advanced/PlayAudioFiles.java)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage across the four primary projects. | -| Rhythm player | Full RTC | `startRhythmPlayer`, `stopRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(Advanced/RhythmPlayer/)` | `N/A(no case)` | `N/A(no case)` | Android and iOS sources are retained but hidden because the APIs are deprecated since RTC SDK 4.6.0. | -| Media recorder | Full RTC | `createMediaRecorder`, `startRecording`, `stopRecording` | `DONE(advanced/MediaRecorder.java)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Basic/JoinChannelVideo(Recorder)/)` | `DONE(Advanced/MediaRecorder/)` | iOS and macOS embed recorder behavior in a join-channel variant; Android and Windows use standalone cases. | -| Local video transcoding | Full RTC | `startLocalVideoTranscoder`, `startCameraCapture`, compositing APIs | `DONE(advanced/LocalVideoTranscoding.java)` | `DONE(Advanced/LocalCompositeGraph/)` | `DONE(Advanced/LocalVideoTranscoding/)` | `DONE(Advanced/LocalVideoTranscoding/)` | iOS names the equivalent case LocalCompositeGraph. | -| Multi-channel join | Full RTC | `joinChannelEx`, `leaveChannelEx`, multi-channel connection APIs | `DONE(advanced/JoinMultipleChannel.java)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/JoinMultiChannel/)` | `DONE(Advanced/MultiChannel/)` | Full coverage across the four primary projects. | -| Multipath | Full RTC | `enableMultipath`, `setMultipathConfig`, or `updateChannel` with multipath options | `DONE(advanced/Multipath.java)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | `DONE(Advanced/Multipath/)` | Full coverage across the four primary projects. | -| Content inspect | Full RTC | `enableContentInspect` | `DONE(advanced/ContentInspect.java)` | `DONE(Advanced/ContentInspect/)` | `DONE(Advanced/ContentInspect/)` | `MISSING` | Windows has no content inspect case. | -| Channel media relay | Full RTC | `startOrUpdateChannelMediaRelay`, `stopChannelMediaRelay`, pause/resume APIs | `DONE(advanced/HostAcrossChannel.java)` | `DONE(Advanced/MediaChannelRelay/)` | `DONE(Advanced/ChannelMediaRelay/)` | `DONE(Advanced/CrossChannel/)` | Full coverage across the four primary projects. | -| Picture in picture | Full RTC | `enterPictureInPictureMode` or `AVPictureInPictureController` | `DONE(advanced/PictureInPicture.java)` | `DONE(Advanced/PictureInPicture/)` | `MISSING` | `MISSING` | macOS and Windows have no picture-in-picture case. | -| Voice beautifier and effects | Full RTC | `setVoiceBeautifierPreset`, `setAudioEffectPreset`, `setVoiceConversionPreset` | `DONE(advanced/VoiceEffects.java)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/VoiceChanger/)` | `DONE(Advanced/BeautyAudio/)` | Windows bundles voice effects with video beauty in `BeautyAudio`. | -| Audio waveform | Full RTC | `enableAudioVolumeIndication`, audio frame observer waveform rendering | `DONE(audio/AudioWaveform.java)` | `DONE(Advanced/AudioWaveform/)` | `MISSING` | `MISSING` | Windows `AudioVolume` adjusts signal volume and is not a waveform case. | -| Third-party player audio routing | Full RTC | third-party player playback combined with the SDK audio route | `DONE(audio/AudioRouterPlayer.java)` | `DONE(Advanced/AudioRouterPlayer/)` | `MISSING` | `MISSING` | Confirm the third-party player dependency is available before porting. | -| Agora beauty (video effect object) | Full RTC | `createVideoEffectObject`, `setVideoEffectStringParam`, `setVideoEffectFloatParam` | `DONE(advanced/AgoraBeauty.java)` | `DONE(Advanced/AgoraBeauty/)` | `DONE(Advanced/AgoraBeauty/)` | `DONE(Advanced/Beauty2.0/)` | Distinct from the built-in `setBeautyEffectOptions` video-enhancement case. | -| Simulcast | Full RTC | `setSimulcastConfig` | `DONE(advanced/Simulcast.java)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | `DONE(Advanced/Simulcast/)` | Verified by `setSimulcastConfig`. | -| Multiple custom video tracks | Full RTC | `createCustomVideoTrack`, `joinChannelEx`, `pushExternalVideoFrameById` | `DONE(advanced/MultiVideoSourceTracks.java)` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `DONE(Advanced/CustomVideoSourcePushMulti/)` | `DONE(Advanced/MultiVideoSourceTracks/)` | Case names differ per platform; correspondence is verified by `createCustomVideoTrack`. | -| Custom remote video render | Full RTC | remote render path via `registerVideoFrameObserver` or `setVideoFrameDelegate` | `DONE(advanced/CustomRemoteVideoRender.java)` | `DONE(Advanced/CustomVideoRender/)` | `DONE(Advanced/CustomVideoRender/)` | `MISSING` | Windows `OriginalVideo` is raw video data and is tracked in its own row. | -| Transparent background rendering | Full RTC | alpha mask and alpha stitch mode with encoded alpha | `DONE(advanced/TransparentRendering.java)` | `DONE(Advanced/TransparentRender/)` | `MISSING` | `DONE(Advanced/TransparentBg/)` | macOS uses alpha options only inside `VideoProcess`/`AgoraBeauty`, not in a dedicated case. | -| Quick channel switch | Full RTC | rapid `joinChannel` / `leaveChannel` switching across channels | `DONE(advanced/VideoQuickSwitch.java)` | `DONE(Advanced/QuickSwitchChannel/)` | `DONE(Advanced/QuickSwitchChannel/)` | `MISSING` | Correspondence is by scenario and case name; this feature has no single distinctive API. | -| URL live stream player | Full RTC | RTE player `openWithUrl`, `startWithUrl` | `DONE(advanced/UrlLiveStream.java)` | `UNKNOWN` | `UNKNOWN` | `UNKNOWN` | Apple and Windows ship an `RtePlayer` case that was not confirmed to demonstrate the same scenario. Run `query-cases` before treating these as gaps. | -| KTV copyright music | None (docs link) | none - the case only opens the Agora documentation page | `DONE(advanced/KtvCopyrightMusic.java)` | `DONE(Advanced/KtvCopyrightMusic/)` | `N/A(docs-link case)` | `N/A(docs-link case)` | The Android and iOS implementations only open a docs URL; absence elsewhere is not a parity gap. | - -## Confirmed Gaps - -These gaps are derived from the primary-project matrix above and should be prioritized when parity work is requested. - -| Gap | Affected Units | Severity | -| --- | --- | --- | -| Join channel video | Windows | High - foundational non-token flow remains partial | -| Basic audio-only join channel | Windows | High - missing foundational case | -| Media metadata | macOS | Medium - full-RTC platform gap | -| Data stream messaging | Windows | Medium - full-RTC platform gap | -| Custom audio render | Windows | Medium - full-RTC platform gap | -| Third-party beauty | macOS, Windows | Low - third-party SDK and license overhead | -| Extension sample | Windows | Medium - extension enablement gap | -| Content inspect | Windows | Low - moderation feature gap | -| Picture in picture | macOS, Windows | Low - platform feature gap | -| Audio waveform | macOS, Windows | Medium - audio visualization gap | -| Third-party player audio routing | macOS, Windows | Low - depends on third-party player dependencies | -| Custom remote video render | Windows | Medium - full-RTC platform gap | -| Transparent background rendering | macOS | Low - niche rendering feature | -| Quick channel switch | Windows | Low - switching-performance demo gap | - -## Maintenance Rules - -- Keep the matrix header fixed to `Android full`, `iOS UIKit`, `macOS`, and `Windows`; project variants are outside repository-level parity scope. -- Do not change an `UNKNOWN` cell to `MISSING` until the primary target project's `query-cases` skill or architecture file has been checked. -- Do not change a `PARTIAL` cell to `DONE` unless registration, discoverability, reference parity, review, and build evidence are confirmed for the active work. -- Prefer feature names based on user-visible scenario rather than platform-specific class names. -- Keep API names in the matrix broad enough to route work, then validate exact signatures in the target SDK version. -- Keep release-risk notes in this matrix short; detailed release checks belong in `docs/ai-engineering/release-known-issues.md`. -- When a new case is added to a primary project, update the matching row in the same edit or follow-up. diff --git a/docs/ai-engineering/knowledge-index.md b/docs/ai-engineering/knowledge-index.md deleted file mode 100644 index 3b9b3a09a..000000000 --- a/docs/ai-engineering/knowledge-index.md +++ /dev/null @@ -1,220 +0,0 @@ -# API Examples AI Engineering Knowledge Index - -This index is the first file to read when an agent turns a product request into API Example changes. It links the existing repository knowledge into one route so the agent does not rediscover platform rules, case patterns, build commands, and known failure modes from scratch. - -## Source Priority - -Use sources in this order: - -1. Product request or issue text provided by the user. -2. `AGENTS.md` at the repository root for cross-platform boundaries. -3. Platform `AGENTS.md` for platform selection and red lines. -4. Project `AGENTS.md` for concrete commands and project-level skills. -5. Project `ARCHITECTURE.md` for case index, registration rules, and canonical file layout. -6. `docs/ai-engineering/repository-profile.json` for repository-specific SDK package names and version sources. -7. `docs/ai-engineering/case-maintenance-matrix.md` for cross-platform parity planning and known gaps. -8. Existing project `.agents/skills/*/SKILL.md` for case query, creation, and review procedures. -9. `docs/ai-engineering/release-known-issues.md` for release packaging and pipeline risks. -10. Repository hooks and platform build scripts for final verification. - -Do not use memory or sibling repositories as the source of truth when the current checkout has conflicting instructions. Treat external knowledge as a hint, then verify it against this repository. - -## Repository Boundary - -The repository has four independent platform families: - -| Platform | Root | Primary Rules | -| --- | --- | --- | -| Android | `Android/` | `Android/AGENTS.md` | -| iOS | `iOS/` | `iOS/AGENTS.md` | -| macOS | `macOS/` | `macOS/AGENTS.md` | -| Windows | `windows/` | `windows/AGENTS.md` | - -Never share source files, build scripts, SDK dependencies, or generated project metadata across platform roots unless a platform rule explicitly says to do so. - -## Primary Project Selection - -Repository-level AI engineering always targets these four primary APIExample projects: - -| Platform | Target | -| --- | --- | -| Android | `Android/APIExample/` | -| iOS | `iOS/APIExample/` | -| macOS | `macOS/` | -| Windows | `windows/` | - -Audio, Compose, SwiftUI, and Objective-C variants remain independent repository projects, but they are outside the repository-level release-iteration matrix, backlog, Contract, and acceptance scope. If the product request does not name a platform, require all four primary projects. Narrow the scope only when the user or Contract records an explicit waiver reason. Treat each required primary project as a separate implementation and verification unit. - -## Existing Local Skills - -| Scope | Skills | -| --- | --- | -| `Android/APIExample/` | `query-cases`, `upsert-case`, `review-case` | -| `iOS/APIExample/` | `query-cases`, `upsert-case`, `review-case` | -| `macOS/` | `query-cases`, `upsert-case`, `review-case` | -| `windows/` | `query-cases`, `upsert-case`, `review-case` | - -The repository-level orchestration skill is `.agents/skills/api-example-release-iteration/SKILL.md`. - -## Repository Profile - -`docs/ai-engineering/repository-profile.json` contains the SDK version sources that differ between API Examples distributions. Shared Python tools parse source kinds such as Gradle properties, CocoaPods packages, and SDK archive names without embedding distribution package names in code. An `external-injection` entry records that a primary project has no trustworthy tracked version source and deliberately keeps that platform's SDK check blocked. - -The orchestrator stores the profile path and SHA-256 in the execution package, input snapshots, and acceptance manifest. Dispatch and assembly stop if the checked-in profile changes after initialization, and the standalone manifest validator recomputes release checks from the bound live profile. Coverage state and historical knowledge do not belong in this profile; keep them in the matrix and durable knowledge documents. - -## Codex Role Routing - -`docs/ai-engineering/role-routing.json` defines three role types and logical profiles. Provider model names remain runtime inputs through `--model` or `CODEX_MODEL_STANDARD`, `CODEX_MODEL_DEEP`, and `CODEX_MODEL_REVIEW`. The orchestrator starts replayable independent `codex exec` sessions; do not describe them as parent-managed Codex subagents. - -The phase order is: - -1. `contract`: one shared Contract for product behavior, reference, cross-platform invariants, and platform targets. -2. `implementation`: Android, iOS, macOS, and Windows agents run independently after Contract passes. A shared checkout serializes them and reconciles each delta before the next run. -3. `verification`: independent Android, iOS, macOS, and Windows agents run concurrently after their matching implementation passes. - -Release checks are mandatory manifest data, not an agent phase. Every platform dispatch has its own timeout, hashed JSONL command log, run identity, host platform, input snapshot, and Contract-selected working directory. Use `--platform` for a focused retry; otherwise the workflow covers all four official roots. - -## Agent Acceptance Artifacts - -Use `docs/ai-engineering/templates/acceptance-manifest-template.json` when an iteration changes source, workflow gates, matrix state, SDK-version logic, or docs that affect agent behavior. - -Validate the filled manifest with: - -```bash -python3 docs/ai-engineering/tools/validate_acceptance_manifest.py -``` - -The filled manifest is normally an execution artifact, not a repository document. Commit it only when the user explicitly asks for an evidence snapshot or when it is selected as a curated pilot-run example. - -For case backfill work, generate candidate platform execution units from the matrix: - -```bash -python3 docs/ai-engineering/tools/generate_case_backlog.py -``` - -Use the generated priority to select the next product requirement, then deliver that requirement across all four primary APIExample projects. Contract must preserve the fixed target mapping above. - -## Case Implementation Knowledge - -For cross-platform parity requests: - -- Start with `docs/ai-engineering/case-maintenance-matrix.md`. -- Verify every relevant matrix cell against the target project before editing. -- Treat `UNKNOWN` as "not checked yet", not as a confirmed gap. -- Treat each required platform as an independent delivery/verification unit inside one requirement-level acceptance manifest. - -Before adding a case: - -- Use the target project's `query-cases` skill when available. -- Read the target project's `ARCHITECTURE.md` case index. -- Confirm whether the case already exists by feature name and SDK API name. -- Confirm the target group, registration mechanism, display name, and index rules. -- Extract the reference contract for parity work before implementation. The closest existing target-project case may guide lifecycle, permissions, UI framework, and registration patterns, but product semantics must come from the source reference contract. - -While adding or modifying a case: - -- Follow the target project's `upsert-case` skill. -- Keep edits inside the selected project unless the project-level instructions say otherwise. -- Update `ARCHITECTURE.md` when the case list, path, or key APIs change. -- Keep sensitive configuration placeholders intact. Do not commit real App IDs, certificates, tokens, or credentials. - -After implementation: - -- Run the target project's `review-case` skill. -- Run the applicable build or static check from the project `AGENTS.md`. -- Record which checks passed, failed, or were skipped with a reason. PASS/FAIL commands must match a JSONL `command_execution` event and exit code. -- Fill and validate an acceptance manifest before claiming final acceptance. - -## Knowledge Maintenance Protocol - -When an iteration uncovers a reusable failure pattern, record it in the right durable document instead of leaving it only in a chat summary. - -Use this structure: - -- Source: review finding, CI failure, release blocker, customer report, or manual smoke. -- Impact platform/project. -- Symptom. -- Root cause. -- Guardrail or rule. -- Verification command or evidence. -- Updated date. - -Where to write it: - -- Cross-platform routing or implementation traps: `docs/ai-engineering/knowledge-index.md`. -- Release, signing, CI, packaging, license, or SDK-version risks: `docs/ai-engineering/release-known-issues.md`. -- Case coverage state: `docs/ai-engineering/case-maintenance-matrix.md`. -- Project-specific implementation traps: the target project `ARCHITECTURE.md` or `.agents/skills/*/SKILL.md`. - -Do not duplicate the same rule in every file. Put the durable rule at the lowest scope that future agents must read. - -When an acceptance manifest includes `knowledge_updates`, at least one platform Implementation must list the durable document or skill change in `files_changed`. The validator enforces this so new failure patterns cannot remain only in the execution summary. - -## Cross-Cutting Red Lines - -- Every case owns its RTC engine lifecycle. -- Leave the channel before destroying or releasing the engine. -- SDK callbacks can arrive on background threads; dispatch UI updates to the platform main thread. -- Do not hardcode real credentials. -- Do not mix full SDK and audio-only SDK APIs. -- Do not move common sample code across platform roots to reduce duplication. -- Do not edit packaging or CI scripts unless the product request or failure requires it. - -## Verification Entrypoints - -| Area | Entrypoint | -| --- | --- | -| Sensitive info and commit-message hooks | `HOOKS-GUIDE.md`, `.git-hooks/`, `.pre-commit-config.yaml`, `.gitleaks.toml` | -| Android build/test | project `AGENTS.md`, project Gradle files | -| iOS build | project `AGENTS.md`, project `Podfile`, `.github/ci/build/build_ios*.sh` | -| macOS build | `macOS/AGENTS.md`, `macOS/Podfile`, `.github/ci/build/build_mac*.sh` | -| Windows build | `windows/AGENTS.md`, `.github/ci/build/build_windows.*` | -| CI/CD | `azure-pipelines.yml`, `.github/ci/`, `.github/workflows/`, `cicd/` | -| Release known issues | `docs/ai-engineering/release-known-issues.md` | - -Prefer the smallest check that validates the changed surface. For docs-only changes, use static validation and link checks instead of full platform builds. - -## Pilot Run Reports - -`docs/ai-engineering/pilot-runs/` contains curated workflow validation examples, not a log of every agent execution. - -Use these reports to understand how a gate was exercised at a point in time. Do not use them as the source of truth for current SDK versions, case coverage, CI status, or release readiness. Re-check the live repository and current CI before reusing any conclusion. - -Only add a new pilot-run report when it is intentionally selected as a representative workflow example or when the user explicitly asks for a repository evidence snapshot. Routine run results should stay in the response, PR discussion, CI artifact, or task tracker. - -Use `docs/ai-engineering/templates/release-dry-run-template.md` as the starting point when such a repository evidence snapshot is needed. - -## Known Failure Patterns To Guard Against - -| Pattern | Guardrail | -| --- | --- | -| Wrong platform touched during a focused fix | Re-read root and platform `AGENTS.md`; list intended files before editing. | -| Case added but not visible in app | Verify project registration mechanism and case index. | -| Android case ordering collision | Use `query-cases` and scan source annotations before choosing a sort index. | -| SDK callback updates UI directly | Run `review-case`; check every callback that touches UI. | -| Engine leak after leaving screen | Check leave-channel before destroy/release in the real screen-close path. | -| Audio-only project receives video API | Re-check selected project SDK type before implementation. | -| CI diagnosis starts in the wrong layer | Identify the first failing log line before changing scripts or signing settings. | -| Third-party beauty license expires silently | Track expiration and confirm renewal before release branches are packaged. | -| Android Extension SDK headers drift from SDK version | Check extension `include` files during SDK version bumps. | -| iOS/macOS certificates expire on build machines | Inspect or print certificate expiration during release preparation. | -| SDK dependency version is not bumped on release branch | Verify platform SDK version files against the release target. | -| Model-declared build result is not backed by command telemetry | Bind each executed verification command to the hashed Codex JSONL log and exit code, and accept build status only for recognized platform build tools. | -| Codex command telemetry omits `cwd` | Record `cwd_source=dispatch`; the dispatcher pins both its process cwd and `codex exec -C` to the main project, while build-command validation rejects directory changes and sibling-project paths. If a future command event supplies `cwd`, require it to match the dispatch target. | -| Platform run misses nested instructions | Start `codex exec` in the Contract-selected target so root/platform/project `AGENTS.md` files load automatically. | -| External packaging metadata leaks into repository acceptance | Keep Jenkins jobs, artifact URLs, QA evidence, and website publication outside the acceptance manifest. | -| An externally injected SDK is assigned a version through unrelated pod metadata | Mark the source as `external-injection`; keep the repository SDK gate blocked until tracked project evidence exists. | -| Windows packaging fails from path length or permissions | Run Windows script preflight after path or packaging changes. | - -## Output Contract - -Every completed AI-assisted iteration should end with an acceptance summary: - -- Product request interpreted. -- Target platform/project. -- Files changed. -- Skills or docs used. -- Verification run and results. -- Open risks or skipped checks. - -Use `docs/ai-engineering/release-iteration-gate.md` for the full gate. diff --git a/docs/ai-engineering/pilot-runs/README.md b/docs/ai-engineering/pilot-runs/README.md deleted file mode 100644 index 13253ac77..000000000 --- a/docs/ai-engineering/pilot-runs/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Pilot Run Reports - -This directory stores curated workflow validation examples for the API Examples AI engineering process. - -Pilot-run reports are historical evidence. They show how a gate or workflow behaved at a specific branch, commit, and date. They are not the source of truth for current SDK versions, case coverage, CI status, signing state, or release readiness. - -Use `docs/ai-engineering/templates/release-dry-run-template.md` when a new release dry-run evidence snapshot is explicitly needed. - -## What Belongs Here - -- First dry run for a new repository gate or workflow. -- Representative cross-platform rehearsal that future agents can learn from. -- Explicitly requested evidence snapshot for a release or workflow decision. - -## What Does Not Belong Here - -- Every routine agent execution result. -- Repeated acceptance summaries for normal feature work. -- CI logs or build output that already belongs in CI artifacts. -- Mutable release status that must be checked live. - -## Maintenance Rules - -- Use dated filenames. -- Keep reviewed reports immutable; create a new report for a new run. -- Include branch, commit, scope, commands, skipped checks, and final status. -- Re-check live files and CI before using an old report as evidence. diff --git a/docs/ai-engineering/release-iteration-gate.md b/docs/ai-engineering/release-iteration-gate.md deleted file mode 100644 index 9ea833068..000000000 --- a/docs/ai-engineering/release-iteration-gate.md +++ /dev/null @@ -1,163 +0,0 @@ -# API Examples Release Iteration Gate - -One RTC product requirement is accepted across Android, iOS, macOS, and Windows. The Lead coordinates shared scope and cross-platform closure; platform implementation and verification remain independently owned. - -## Topology - -| Agent | Count | Pass Evidence | -| --- | --- | --- | -| Contract | 1 | Shared behavior, APIs, non-goals, reference, platform targets and constraints | -| Platform Implementation | 4 | Query/upsert result, scoped files, matrix proposals | -| Platform Verification | 4 | Findings, parity, entry/UX, commands, build result, skipped reasons | -| Lead | 1 | Cross-platform differences, final status, manifest assembly | - -Contract runs once. In a shared checkout, Android/iOS/macOS/Windows Implementation runs are serialized so every repository delta is attributable to one agent. Each Verification depends on Contract and its matching Implementation, runs concurrently with other platform Verification roles, and cannot reuse the Implementation agent identity. - -## Intake Gate - -Before Contract starts, identify feature behavior, SDK family, key APIs, target SDK version for each platform, reference case, expected user flow, repository scope, and available target verification hosts. SDK targets may differ when release lines are staggered. Confirm that the checked-in repository profile describes the current SDK package names and version sources. All four primary projects are required by default: `Android/APIExample/`, `iOS/APIExample/`, `macOS/`, and `windows/`. Contract may waive a platform with an explicit reason, but may not substitute an audio, Compose, SwiftUI, or Objective-C variant for a primary project. - -## Contract Gate - -Contract passes only when: - -- Scenario, inputs, success signals, APIs, and non-goals are observable. -- A reference source and result are recorded when parity is required. -- Cross-platform invariant behavior is explicit. -- Android, iOS, macOS, and Windows each have `required`, `target_project`, constraints, and allowed files. -- Every required `target_project` matches the fixed primary-project mapping from the Intake Gate. -- Lifecycle, callback threading, registration, SDK family, and credential boundaries are explicit. - -Nearby cases guide framework patterns; product behavior comes from the shared Contract/reference. - -## Platform Implementation Gate - -Each platform Implementation passes only when it: - -- Reads that platform and target-project instructions. -- Runs from the Contract-selected target project so nested `AGENTS.md` instructions are loaded by Codex. -- Queries existing cases before editing. -- Uses the selected project skill and stays inside Contract's allowed paths. -- Completes registration, localization, lifecycle cleanup, and architecture index updates. -- Lets the orchestrator derive its real platform file delta and records matrix proposals. - -Platform source roots are independent, but Implementation runs sharing one checkout are serialized so cross-root edits cannot be credited to the wrong agent. Each run refreshes an input snapshot bound to the execution package and dependency artifacts; retries retain attempt provenance and produce one cumulative net delta. Separate workspaces may execute them concurrently only when each produces an independently attributable patch. - -## Platform Verification Gate - -Each Verification independently checks: - -- Contract/reference parity, SDK types/defaults, lifecycle, threading, errors, and cleanup. -- Registration, display name, inputs, feedback, layout, and audio/video affordances. -- The strongest valid build or static command on the current verification host. -- Each executed PASS/FAIL command is bound to the hashed Codex JSONL command event and exit code; build results use recognized platform build actions from the Contract working directory. When the Codex event includes `cwd`, it must match; current CLI versions that omit it record `cwd_source=dispatch`, backed by both the process cwd and `codex exec -C` target. -- Dispatch provenance records the host platform. Windows build `PASS` requires a Windows host. -- The Verification run does not modify tracked or untracked repository content. -- Concrete reasons for every skipped check. - -For non-`BLOCKED` acceptance, every required platform needs role status, review result, parity result, and build result `PASS`. Final `PASS` permits no skipped checks. - -## Host Mismatch - -Host mismatch never authorizes SDK downloads, emulation, cross-compilation, or substitute compilers as evidence. On macOS, Windows Verification may report static findings but must keep `build_result=BLOCKED` and role status `BLOCKED`. A retry on a real Windows host can replace that artifact using `--platform windows --retry`. - -## Cross-Platform Gate - -After platform Verification, the Lead records: - -- `result`: `PASS`, `FAIL`, or `BLOCKED`. -- Evidence that all required platform contracts and behavior were compared. -- Intentional platform differences and their product/API justification. - -Any required platform blocker or cross-platform result other than `PASS` forces final `BLOCKED`. - -## Release Checklist - -Release is mandatory manifest data, not an agent. `requirement.target_sdk_versions` and `release.target_sdk_versions` must contain the same Android, iOS, macOS, and Windows mapping. The manifest binds `release.repository_profile` by repository-relative path and SHA-256; assembly and the standalone validator recompute SDK checks from that live profile. The orchestrator compares each platform only with its own target while refreshing dependency versions from live repository files during assembly. Non-`BLOCKED` acceptance requires all SDK-version checks to pass and permits no skipped repository release checks. A platform remaining on an older SDK is valid and does not require changing the script or repository profile. - -Jenkins packaging, QA validation, artifact URLs, and external website publication are downstream processes outside this repository workflow. A final `PASS` means the API Example source and repository checks are ready for that external handoff; the manifest does not represent packaging or QA completion. - -When a primary project receives an SDK outside tracked repository files, declare that boundary as `external-injection` in the repository profile. Its SDK-version check remains `BLOCKED` until a trustworthy project-owned version source is available. A successful project build proves compatibility with the injected binaries, not their exact SDK version; do not change packaging scripts, pod metadata, or comments solely to force the repository gate to pass. - -## Manifest V4 - -The single state tree is: - -```text -requirement -contract -platforms.android.implementation / verification -platforms.ios.implementation / verification -platforms.macos.implementation / verification -platforms.windows.implementation / verification -cross_platform_acceptance -release -knowledge_updates -final_status -``` - -Validate it with: - -```bash -python3 docs/ai-engineering/tools/validate_acceptance_manifest.py -``` - -The validator enforces platform completeness, unique agent/run provenance, Contract target boundaries, platform result closure, cross-platform closure, matrix rules, SDK-version checks, repository paths, evidence-file hashes, and knowledge writeback. - -## Orchestration - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ - --matrix docs/ai-engineering/case-maintenance-matrix.md \ - --feature "" \ - --target-sdk-version "4.7.0" \ - --run-dir /tmp/api-example-requirement - -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement --phase contract --model "" - -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement --phase implementation --model "" - -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement --phase verification --model "" -``` - -`--target-sdk-version` is the baseline for all four platforms. Repeat `--platform-sdk-version platform=x.y.z` for each differing platform; omit overrides when all four use one version. If the product requirement is not yet an actionable matrix row, initialize it directly with `--sdk-family` and one or more `--key-api` arguments. - -Omitting `--platform` dispatches all four platform roles. Implementation runs are serialized in the shared checkout and reconciled after each run; Verification runs execute concurrently. Each independent `codex exec` run has a timeout, host provenance, a target-project working directory, and a hashed JSONL log. The execution package and input snapshots bind the checked-in repository profile by path and SHA-256. A prior `FAIL`/`BLOCKED` platform artifact can be replaced with `--retry`. - -## Automation Boundary - -The tools prepare platform agents, serialize shared-checkout implementation, parallelize immutable verification, enforce dependencies/timeouts, validate v4, and gate matrix changes. The Lead still approves phase advancement, retries, intentional differences, and final status. - -## Final Summary - -```markdown -## Acceptance Summary - -Requirement: -- Scenario/APIs: -- Contract/reference: - -Platforms: -- Android: files, verification, build -- iOS: files, verification, build -- macOS: files, verification, build -- Windows: files, verification, build - -Cross-platform: -- Result/evidence: -- Intentional differences: - -Release: -- SDK-version checks: -- External handoff required: Jenkins packaging and QA - -Manifest: -- Path/validation: - -Final status: PASS | PASS WITH RISKS | BLOCKED -``` - -Routine manifests remain execution artifacts unless explicitly selected as repository evidence. diff --git a/docs/ai-engineering/release-known-issues.md b/docs/ai-engineering/release-known-issues.md deleted file mode 100644 index ce312a546..000000000 --- a/docs/ai-engineering/release-known-issues.md +++ /dev/null @@ -1,21 +0,0 @@ -# API Examples Release Known Issues - -This file records repository-specific risks that can affect API Example source acceptance. Keep Jenkins packaging, QA validation, artifact distribution, and website publication outside the acceptance manifest. - -## Repository Acceptance Risks - -| Area | Risk | Repository Gate | -| --- | --- | --- | -| SDK versions | The Android, iOS, macOS, and Windows main APIExample version declarations or evidence can drift across release branches. | Validate every main-project source declared in `repository-profile.json` against the platform target SDK version; keep unverifiable external injection blocked. | -| iOS full demo SDK source | `iOS/APIExample/Podfile` uses a local `sdk.podspec`, while the ignored `libs/*.xcframework` binaries are injected outside the repository. The podspec package version and commented remote pod example do not prove the injected RTC SDK version. | Keep the repository SDK check `BLOCKED` until a trustworthy project-owned version source exists; a successful build alone is not version evidence. | -| Android extensions | Extension samples can retain headers or native assets from an older SDK. | Review extension `include` files and matching assets whenever the SDK dependency changes. | -| Third-party beauty | Sample licenses and bundled assets can expire or become incompatible. | Record the current license or asset limitation when the related example changes. | -| iOS/macOS signing | Local verification may not cover external package-signing configuration. | Treat signing and packaging as an external handoff; do not report repository acceptance as package completion. | -| Windows verification | macOS cannot provide MSBuild evidence. | Keep Windows build verification blocked until the role runs on a real Windows host. | - -## Maintenance Rules - -- Keep target SDK package names and version sources in `repository-profile.json`, not Python code. -- Add a reusable failure pattern to this file only when it is verified against this repository. -- Keep case coverage in `case-maintenance-matrix.md`; do not use this file as a second coverage tracker. -- Do not add Jenkins URLs, package artifact URLs, QA ownership, or website publication state to the acceptance manifest. diff --git a/docs/ai-engineering/repository-profile.json b/docs/ai-engineering/repository-profile.json deleted file mode 100644 index 788899fee..000000000 --- a/docs/ai-engineering/repository-profile.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": 1, - "sdk_version_sources": { - "android": [ - { - "path": "Android/APIExample/gradle.properties", - "kind": "gradle-property", - "key": "rtc_sdk_version" - } - ], - "ios": [ - { - "path": "iOS/APIExample/Podfile", - "kind": "external-injection", - "reason": "The RTC SDK frameworks are injected outside the repository, so their version cannot be verified from tracked project files." - } - ], - "macos": [ - { - "path": "macOS/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_macOS" - } - ], - "windows": [ - { - "path": "windows/APIExample/install.ps1", - "kind": "archive-name", - "prefix": "Agora_Native_SDK_for_Windows_v", - "suffix": "_FULL.zip" - } - ] - } -} diff --git a/docs/ai-engineering/role-routing.json b/docs/ai-engineering/role-routing.json deleted file mode 100644 index 8e9d67cf0..000000000 --- a/docs/ai-engineering/role-routing.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": 1, - "profiles": { - "standard": { - "reasoning_effort": "medium" - }, - "deep": { - "reasoning_effort": "high" - }, - "review": { - "reasoning_effort": "high" - } - }, - "phases": [ - "contract", - "implementation", - "verification" - ], - "roles": { - "contract": { - "profile": "standard", - "phase": "contract", - "sandbox": "read-only" - }, - "implementation": { - "profile": "deep", - "phase": "implementation", - "sandbox": "workspace-write" - }, - "verification": { - "profile": "review", - "phase": "verification", - "sandbox": "workspace-write" - } - } -} diff --git a/docs/ai-engineering/templates/README.md b/docs/ai-engineering/templates/README.md deleted file mode 100644 index ab62636a2..000000000 --- a/docs/ai-engineering/templates/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# AI Engineering Templates - -This directory stores reusable templates for repository-level AI engineering workflows. - -Templates are not completed evidence. Before using a template, replace every placeholder with live repository data and current command output. - -Templates may intentionally fail validation until their placeholders are replaced. The version 4 manifest stores one shared Contract and one Implementation/Verification pair for each official platform; use the orchestrator to add model, run, host, prompt/artifact, input-snapshot, working-directory, repository-delta, repository-profile, and hashed command-log provenance. Release data is limited to per-platform target SDK versions and repository checks. - -Do not commit routine execution output from these templates unless the user explicitly asks for a repository evidence snapshot or the run is selected as a representative workflow validation example. - -## Templates - -| Template | Purpose | -| --- | --- | -| `acceptance-manifest-template.json` | Requirement-level Contract, four platform delivery units, cross-platform acceptance, release, and knowledge evidence. | -| `release-dry-run-template.md` | Human-readable release dry-run evidence snapshot. | diff --git a/docs/ai-engineering/templates/acceptance-manifest-template.json b/docs/ai-engineering/templates/acceptance-manifest-template.json deleted file mode 100644 index 6052a0cd3..000000000 --- a/docs/ai-engineering/templates/acceptance-manifest-template.json +++ /dev/null @@ -1,285 +0,0 @@ -{ - "version": 4, - "final_status": "BLOCKED", - "requirement": { - "feature": "", - "sdk_family": "", - "key_apis": [""], - "target_sdk_versions": { - "android": "", - "ios": "", - "macos": "", - "windows": "" - } - }, - "contract": { - "agent_id": "contract-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/contract.md", - "artifact": "role-artifacts/contract.json" - }, - "status": "BLOCKED", - "evidence": "Pending shared Contract.", - "summary": "Pending shared behavior, reference, and platform targets.", - "output": { - "scenario": "", - "key_apis": [""], - "non_goals": [], - "reference": { - "required": false, - "source_case": "", - "contract_result": "SKIPPED" - }, - "cross_platform_requirements": [""], - "platform_targets": { - "android": { - "required": true, - "target_project": "Android/APIExample/", - "key_constraints": [], - "files_allowed": ["Android/APIExample/"], - "waiver_reason": "" - }, - "ios": { - "required": true, - "target_project": "iOS/APIExample/", - "key_constraints": [], - "files_allowed": ["iOS/APIExample/"], - "waiver_reason": "" - }, - "macos": { - "required": true, - "target_project": "macOS/", - "key_constraints": [], - "files_allowed": ["macOS/"], - "waiver_reason": "" - }, - "windows": { - "required": true, - "target_project": "windows/", - "key_constraints": [], - "files_allowed": ["windows/"], - "waiver_reason": "" - } - } - } - }, - "platforms": { - "android": { - "implementation": { - "agent_id": "android-implementation-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/android-implementation.md", - "artifact": "role-artifacts/android-implementation.json" - }, - "status": "BLOCKED", - "evidence": "Pending Android implementation.", - "summary": "Pending Android changes.", - "output": { - "target_project": "Android/APIExample/", - "query_cases": "Pending.", - "upsert_case": "Pending.", - "files_changed": [], - "matrix_updates": [] - } - }, - "verification": { - "agent_id": "android-verification-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/android-verification.md", - "artifact": "role-artifacts/android-verification.json" - }, - "status": "BLOCKED", - "evidence": "Pending Android verification.", - "summary": "Pending Android review and build.", - "output": { - "result": "BLOCKED", - "findings": [], - "parity_result": "BLOCKED", - "entry_point": "", - "ux_notes": "", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [] - } - } - }, - "ios": { - "implementation": { - "agent_id": "ios-implementation-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/ios-implementation.md", - "artifact": "role-artifacts/ios-implementation.json" - }, - "status": "BLOCKED", - "evidence": "Pending iOS implementation.", - "summary": "Pending iOS changes.", - "output": { - "target_project": "iOS/APIExample/", - "query_cases": "Pending.", - "upsert_case": "Pending.", - "files_changed": [], - "matrix_updates": [] - } - }, - "verification": { - "agent_id": "ios-verification-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/ios-verification.md", - "artifact": "role-artifacts/ios-verification.json" - }, - "status": "BLOCKED", - "evidence": "Pending iOS verification.", - "summary": "Pending iOS review and build.", - "output": { - "result": "BLOCKED", - "findings": [], - "parity_result": "BLOCKED", - "entry_point": "", - "ux_notes": "", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [] - } - } - }, - "macos": { - "implementation": { - "agent_id": "macos-implementation-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/macos-implementation.md", - "artifact": "role-artifacts/macos-implementation.json" - }, - "status": "BLOCKED", - "evidence": "Pending macOS implementation.", - "summary": "Pending macOS changes.", - "output": { - "target_project": "macOS/", - "query_cases": "Pending.", - "upsert_case": "Pending.", - "files_changed": [], - "matrix_updates": [] - } - }, - "verification": { - "agent_id": "macos-verification-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/macos-verification.md", - "artifact": "role-artifacts/macos-verification.json" - }, - "status": "BLOCKED", - "evidence": "Pending macOS verification.", - "summary": "Pending macOS review and build.", - "output": { - "result": "BLOCKED", - "findings": [], - "parity_result": "BLOCKED", - "entry_point": "", - "ux_notes": "", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [] - } - } - }, - "windows": { - "implementation": { - "agent_id": "windows-implementation-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/windows-implementation.md", - "artifact": "role-artifacts/windows-implementation.json" - }, - "status": "BLOCKED", - "evidence": "Pending Windows implementation.", - "summary": "Pending Windows changes.", - "output": { - "target_project": "windows/", - "query_cases": "Pending.", - "upsert_case": "Pending.", - "files_changed": [], - "matrix_updates": [] - } - }, - "verification": { - "agent_id": "windows-verification-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/windows-verification.md", - "artifact": "role-artifacts/windows-verification.json" - }, - "status": "BLOCKED", - "evidence": "Pending Windows verification.", - "summary": "Pending Windows review and MSBuild evidence.", - "output": { - "result": "BLOCKED", - "findings": [], - "parity_result": "BLOCKED", - "entry_point": "", - "ux_notes": "", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [] - } - } - } - }, - "cross_platform_acceptance": { - "result": "BLOCKED", - "evidence": "Pending all required platform verification.", - "differences": [] - }, - "release": { - "required": true, - "repository_profile": "docs/ai-engineering/repository-profile.json", - "repository_profile_sha256": "", - "target_sdk_versions": { - "android": "", - "ios": "", - "macos": "", - "windows": "" - }, - "checks": [ - { - "name": "sdk-version-android", - "result": "BLOCKED", - "expected_version": "", - "actual_versions": {}, - "evidence": "", - "reason": "Pending live Android SDK version check." - }, - { - "name": "sdk-version-ios", - "result": "BLOCKED", - "expected_version": "", - "actual_versions": {}, - "evidence": "", - "reason": "Pending live iOS SDK version check." - }, - { - "name": "sdk-version-macos", - "result": "BLOCKED", - "expected_version": "", - "actual_versions": {}, - "evidence": "", - "reason": "Pending live macOS SDK version check." - }, - { - "name": "sdk-version-windows", - "result": "BLOCKED", - "expected_version": "", - "actual_versions": {}, - "evidence": "", - "reason": "Pending live Windows SDK version check." - } - ], - "skipped_checks": [] - }, - "knowledge_updates": [] -} diff --git a/docs/ai-engineering/templates/release-dry-run-template.md b/docs/ai-engineering/templates/release-dry-run-template.md deleted file mode 100644 index 73de74f61..000000000 --- a/docs/ai-engineering/templates/release-dry-run-template.md +++ /dev/null @@ -1,131 +0,0 @@ -# Release Dry Run Report Template - -Use this template only when a release dry run is intentionally selected as a repository-level evidence snapshot. Routine execution results should stay in the agent response, PR discussion, CI artifact, or task tracker. - -Do not fill this template with stale values from an older run. Re-check the live repository and target release version before completing every section. - -## Scope - -- Repository: -- Branch: -- Commit: -- Target release SDK version: -- Product scenario: -- Target platform roots: -- Verification budget: -- Non-goals: - -## Sources Used - -- `AGENTS.md` -- `.agents/skills/api-example-release-iteration/SKILL.md` -- `docs/ai-engineering/knowledge-index.md` -- `docs/ai-engineering/repository-profile.json` -- `docs/ai-engineering/release-iteration-gate.md` -- `docs/ai-engineering/release-known-issues.md` -- Platform `AGENTS.md` files: -- Platform SDK version files: -- Platform build scripts: - -## Findings - -### Product Gate - -Status: - -Evidence: - -### Architecture Gate - -Status: - -Evidence: - -### Repository Release Gate - -Status: - -| Gate | Result | Evidence | -| --- | --- | --- | -| SDK version consistency | | | -| Android Extension include freshness | | | -| Third-party beauty license validity | | | -| Repository build checks | | | - -### Static Version Snapshot - -| Platform Project | Version Source | Observed Version | -| --- | --- | --- | -| `Android/APIExample` | `gradle.properties` | | -| `Android/APIExample-Audio` | `gradle.properties` | | -| `Android/APIExample-Compose` | `gradle.properties` | | -| `iOS/APIExample` | `Podfile` | | -| `iOS/APIExample-Audio` | `Podfile` | | -| `iOS/APIExample-OC` | `Podfile` | | -| `iOS/APIExample-SwiftUI` | `Podfile` | | -| `macOS` | `Podfile` | | -| `windows/APIExample` | SDK package or install script | | - -### Script Checks - -Commands: - -```bash - -``` - -Result: - -Not covered: - -- - -### Existing Automation Found - -| Area | Evidence | -| --- | --- | -| SDK version validation | | -| Build script preflight | | -| Signing visibility | | -| Windows packaging preflight | | - -## Automation Gaps - -1. - -## Acceptance Summary - -Product: -- Scenario: -- Target: -- Non-goals: - -Architecture: -- Platform/project: -- Key constraints: - -Implementation: -- Files changed: -- Skills/docs used: - -Review: -- Result: -- Findings: - -Testing: -- Commands: -- Result: -- Skipped checks: - -Release: -- Required: -- Target SDK versions by platform: -- Checks: -- Skipped checks: -- External handoff: Jenkins packaging and QA - -UX: -- Entry point: -- Notes: - -Final status: PASS | PASS WITH RISKS | BLOCKED diff --git a/docs/ai-engineering/tools/README.md b/docs/ai-engineering/tools/README.md deleted file mode 100644 index d6c17d0d9..000000000 --- a/docs/ai-engineering/tools/README.md +++ /dev/null @@ -1,104 +0,0 @@ -# AI Engineering Tools - -`orchestrate_case_execution.py` turns one RTC requirement into a shared Contract plus attributed Android, iOS, macOS, and Windows delivery. It launches independent, replayable `codex exec` sessions rather than parent-managed Codex subagent threads. - -## Role Types - -| Phase | Agents | Profile | -| --- | --- | --- | -| `contract` | One shared Contract | `standard` | -| `implementation` | Four platform Implementation agents | `deep` | -| `verification` | Four independent platform Verification agents | `review` | - -Provider model names remain runtime inputs through `--model` or `CODEX_MODEL_STANDARD`, `CODEX_MODEL_DEEP`, and `CODEX_MODEL_REVIEW`. - -## Run A Requirement - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py init \ - --matrix docs/ai-engineering/case-maintenance-matrix.md \ - --feature "Join channel audio" \ - --target-sdk-version "4.7.0" \ - --run-dir /tmp/api-example-requirement -``` - -For a new feature outside the matrix backlog, add `--sdk-family "Full RTC" --key-api ""`; repeat `--key-api` for multiple APIs. - -`--target-sdk-version` is a baseline expanded to Android, iOS, macOS, and Windows. The command above resolves every platform to 4.7.0. Repeat `--platform-sdk-version platform=x.y.z` for any platform on a different SDK line. The final mapping is stored in the execution package and manifest; platform release cadence changes do not require edits to Python or the repository profile. - -The default `docs/ai-engineering/repository-profile.json` defines repository-specific SDK version sources. Shared tools support Gradle property, CocoaPods package, SDK archive-name, and explicit external-injection sources without hard-coding a distribution in Python. An external-injection source keeps its platform SDK check `BLOCKED` because tracked project files cannot prove the injected binary version. `init` accepts `--repository-profile` for a checked-in alternative and binds its path and SHA-256 into the execution package, input snapshots, and acceptance manifest. - -Dispatch Contract, then the two platform phases: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement --phase contract --model "" - -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement --phase implementation --model "" - -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement --phase verification --model "" -``` - -Omitting `--platform` covers Android, iOS, macOS, and Windows. Implementation runs are serialized in a shared checkout and reconciled before the next agent starts; Verification runs execute concurrently. Use `--platform windows` for one platform. Use `--retry` only to replace a prior `FAIL` or `BLOCKED` artifact; an Implementation retry automatically invalidates that platform's old Verification. - -Each run starts in the Contract-selected target project, writes independent stdout/stderr logs, records its host platform, and has a 900-second default timeout. Input snapshots bind the execution package, current repository state, routing config, repository profile, and dependency artifact hashes. This lets Codex load nested platform/project `AGENTS.md` files automatically. `--dry-run` resolves prompts, content-aware snapshots, models, working directories, and commands without starting Codex. - -Verification PASS/FAIL commands are accepted only when their exact command string and exit code exist in the hashed Codex JSONL log. Build commands must execute a real target-platform build action from the Contract working directory; Windows build `PASS` additionally requires a Windows host. Contract and Verification may create ignored build output but fail if they modify tracked or untracked repository content. Implementation `files_changed` is derived from the real per-run repository delta rather than trusted from model output. - -Assemble with an explicit cross-platform conclusion: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py assemble \ - --run-dir /tmp/api-example-requirement \ - --matrix docs/ai-engineering/case-maintenance-matrix.md \ - --final-status "BLOCKED" \ - --cross-platform-result "BLOCKED" \ - --cross-platform-evidence "Windows-host verification pending" -``` - -Manifest v4 stores one shared Contract and one Implementation/Verification pair per official platform. Implementation retries preserve attempt history and expose one cumulative net delta whose file list must match the manifest. Matrix updates are applied only after structural and evidence-file validation and never for final `BLOCKED`. Assembly also refreshes all repository-verifiable platform SDK dependency versions and preserves `BLOCKED` for external injection boundaries. - -Assembly does not accept CI job URLs, package artifact URLs, or QA metadata. Jenkins packaging, QA validation, artifact distribution, and website publication are external handoff processes rather than repository acceptance gates. - -## macOS And Windows - -Windows Verification on macOS is static review only. It must not download Windows SDKs, emulate, cross-compile, or treat another compiler as MSBuild evidence. Keep the Windows artifact `BLOCKED`, then replace it from a real Windows host with: - -```bash -python3 docs/ai-engineering/tools/orchestrate_case_execution.py dispatch \ - --run-dir /tmp/api-example-requirement \ - --phase verification \ - --platform windows \ - --retry \ - --model "" -``` - -## Supporting Tools - -- `generate_case_backlog.py`: reads `MISSING`/`PARTIAL` matrix cells and prioritizes requirement candidates. -- `prepare_case_execution.py`: creates a v4 requirement package without starting Codex. -- `validate_acceptance_manifest.py`: validates a filled v4 manifest. - -```bash -python3 docs/ai-engineering/tools/prepare_case_execution.py \ - --feature "Join channel audio" \ - --target-sdk-version "4.7.0" \ - --repository-profile docs/ai-engineering/repository-profile.json -python3 docs/ai-engineering/tools/validate_acceptance_manifest.py -``` - -The standalone validator resolves evidence paths relative to the manifest, verifies the bound repository profile hash, recomputes live SDK checks, and recomputes prompt, role-artifact, input-snapshot, command-log, and repository-delta hashes. - -Routine manifests and dispatch logs stay outside the repository unless the user requests an evidence snapshot. - -## Tests - -```bash -PYTHONDONTWRITEBYTECODE=1 python3 -m unittest \ - docs/ai-engineering/tools/validate_acceptance_manifest_test.py \ - docs/ai-engineering/tools/generate_case_backlog_test.py \ - docs/ai-engineering/tools/prepare_case_execution_test.py \ - docs/ai-engineering/tools/orchestrate_case_execution_test.py -``` diff --git a/docs/ai-engineering/tools/generate_case_backlog.py b/docs/ai-engineering/tools/generate_case_backlog.py deleted file mode 100644 index 34435fc11..000000000 --- a/docs/ai-engineering/tools/generate_case_backlog.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python3 -"""Generate platform execution units from the case-maintenance matrix.""" - -import argparse -import json -import re -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[3] -DEFAULT_MATRIX = REPO_ROOT / "docs/ai-engineering/case-maintenance-matrix.md" -NON_PLATFORM_COLUMNS = {"Feature", "SDK Family", "Key APIs", "Notes"} -PLATFORM_PROJECTS = { - "Android full": "Android/APIExample/", - "iOS UIKit": "iOS/APIExample/", - "macOS": "macOS/", - "Windows": "windows/", -} -SEVERITY_PRIORITY = { - "High": 10, - "Medium": 20, - "Low": 30, - "Unspecified": 90, -} -STATUS_PRIORITY_OFFSET = { - "MISSING": 0, - "PARTIAL": 5, -} -CONFIRMED_GAP_ALIASES = { - "Basic audio-only join channel": ["Join channel audio"], -} - - -def split_markdown_row(line): - return [cell.strip() for cell in line.strip().strip("|").split("|")] - - -def strip_code(value): - value = value.strip() - if value.startswith("`") and value.endswith("`"): - return value[1:-1] - return value - - -def parse_status(cell): - status, detail = parse_matrix_cell(cell) - if status in {"MISSING", "PARTIAL"}: - return status, detail - return "", "" - - -def parse_matrix_cell(cell): - value = strip_code(cell) - if value in {"MISSING", "UNKNOWN"}: - return value, "" - match = re.fullmatch(r"(DONE|HIDDEN|PARTIAL|N/A)\((.+)\)", value) - if match: - return match.group(1), match.group(2) - return "", "" - - -def parse_done_path(cell): - status, detail = parse_matrix_cell(cell) - return detail if status == "DONE" else "" - - -def parse_key_apis(cell): - apis = re.findall(r"`([^`]+)`", cell) - if apis: - return [api.strip() for api in apis] - return [part.strip() for part in cell.split(",") if part.strip()] - - -def find_matrix_table(lines): - for index, line in enumerate(lines): - if not line.startswith("|"): - continue - header = split_markdown_row(line) - if "Feature" in header and "Key APIs" in header: - rows = [] - for row_line in lines[index + 2 :]: - if not row_line.startswith("|"): - break - rows.append(split_markdown_row(row_line)) - return header, rows - raise ValueError("could not find pilot matrix table with Feature and Key APIs columns") - - -def validate_matrix_header(header): - expected = list(PLATFORM_PROJECTS) - actual = [column for column in header if column not in NON_PLATFORM_COLUMNS] - if actual != expected: - raise ValueError( - "platform columns must be exactly: " - + ", ".join(expected) - + f"; found: {', '.join(actual) or 'none'}" - ) - - -def find_confirmed_gap_table(lines): - for index, line in enumerate(lines): - if not line.startswith("|"): - continue - header = split_markdown_row(line) - if {"Gap", "Affected Units", "Severity"}.issubset(set(header)): - rows = [] - for row_line in lines[index + 2 :]: - if not row_line.startswith("|"): - break - rows.append(split_markdown_row(row_line)) - return header, rows - return [], [] - - -def parse_severity(value): - label = re.split(r"\s+[-—]\s+", value.strip(), maxsplit=1)[0].strip() - return label if label in SEVERITY_PRIORITY else "Unspecified" - - -def gap_names_for_lookup(gap_name): - return [gap_name, *CONFIRMED_GAP_ALIASES.get(gap_name, [])] - - -def build_gap_severity_map(lines): - header, rows = find_confirmed_gap_table(lines) - if not header: - return {} - gap_index = header.index("Gap") - affected_index = header.index("Affected Units") - severity_index = header.index("Severity") - mapping = {} - for row in rows: - if len(row) != len(header): - continue - severity = parse_severity(row[severity_index]) - affected_units = [unit.strip() for unit in row[affected_index].split(",") if unit.strip()] - for unit in affected_units: - for gap_name in gap_names_for_lookup(row[gap_index]): - mapping[(gap_name, unit)] = severity - return mapping - - -def find_reference_candidates(header, row, target_platform): - candidates = [] - for column_index, column_name in enumerate(header): - if column_name in NON_PLATFORM_COLUMNS or column_name == target_platform: - continue - if column_name not in PLATFORM_PROJECTS: - continue - done_path = parse_done_path(row[column_index]) - if not done_path: - continue - candidates.append( - { - "platform_unit": column_name, - "project": PLATFORM_PROJECTS[column_name], - "path": done_path, - } - ) - return candidates - - -def generate_execution_units(matrix_path): - lines = matrix_path.read_text(encoding="utf-8").splitlines() - header, rows = find_matrix_table(lines) - validate_matrix_header(header) - gap_severities = build_gap_severity_map(lines) - feature_index = header.index("Feature") - sdk_index = header.index("SDK Family") - key_api_index = header.index("Key APIs") - notes_index = header.index("Notes") if "Notes" in header else None - execution_units = [] - errors = [] - - for row in rows: - if len(row) != len(header): - errors.append(f"row has {len(row)} cells but header has {len(header)} cells: {row}") - continue - for column_index, column_name in enumerate(header): - if column_name in NON_PLATFORM_COLUMNS: - continue - status, status_note = parse_status(row[column_index]) - if not status: - continue - if column_name not in PLATFORM_PROJECTS: - errors.append(f"unknown platform column with actionable status: {column_name}") - continue - severity = gap_severities.get((row[feature_index], column_name), "Unspecified") - priority = SEVERITY_PRIORITY[severity] + STATUS_PRIORITY_OFFSET[status] - execution_units.append( - { - "feature": row[feature_index], - "sdk_family": row[sdk_index], - "key_apis": parse_key_apis(row[key_api_index]), - "platform_unit": column_name, - "target_project": PLATFORM_PROJECTS[column_name], - "status": status, - "status_note": status_note, - "severity": severity, - "priority": priority, - "reference_candidates": find_reference_candidates(header, row, column_name), - "notes": row[notes_index] if notes_index is not None else "", - } - ) - - if errors: - raise ValueError("\n".join(errors)) - execution_units.sort(key=lambda unit: (unit["priority"], unit["feature"], unit["platform_unit"])) - return {"execution_units": execution_units} - - -def main(argv=None): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--matrix", default=str(DEFAULT_MATRIX), help="Path to case-maintenance-matrix.md") - args = parser.parse_args(argv) - - try: - payload = generate_execution_units(Path(args.matrix)) - except (OSError, ValueError) as exc: - print(f"ERROR: {exc}", file=sys.stderr) - return 1 - - print(json.dumps(payload, indent=2, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/ai-engineering/tools/generate_case_backlog_test.py b/docs/ai-engineering/tools/generate_case_backlog_test.py deleted file mode 100644 index 53d490d5e..000000000 --- a/docs/ai-engineering/tools/generate_case_backlog_test.py +++ /dev/null @@ -1,165 +0,0 @@ -import json -import subprocess -import sys -import tempfile -import textwrap -import unittest -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[3] -GENERATOR = REPO_ROOT / "docs/ai-engineering/tools/generate_case_backlog.py" - -sys.path.insert(0, str(GENERATOR.parent)) -from generate_case_backlog import parse_matrix_cell - - -class CaseBacklogGeneratorTest(unittest.TestCase): - def run_generator(self, matrix_text): - with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as handle: - handle.write(matrix_text) - matrix_path = handle.name - try: - return subprocess.run( - [sys.executable, str(GENERATOR), "--matrix", matrix_path], - cwd=REPO_ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) - finally: - Path(matrix_path).unlink(missing_ok=True) - - def test_generates_execution_units_for_missing_and_partial_cells(self): - matrix = textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | - | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | - | Media metadata | Full RTC | `registerMediaMetadataObserver` | `DONE(advanced/MediaMetadata.java)` | `DONE(Advanced/VideoMetadata/)` | `DONE(Advanced/VideoMetadata/)` | `PARTIAL(Advanced/Metadata; smoke pending)` | Runtime metadata smoke pending. | - | Audio mixing | Full RTC | `startAudioMixing` | `DONE(advanced/PlayAudioFiles.java)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | `DONE(Advanced/AudioMixing/)` | Full coverage. | - - ## Confirmed Gaps - - | Gap | Affected Units | Severity | - | --- | --- | --- | - | Basic audio-only join channel | Windows | High - missing foundational case | - """ - ) - - result = self.run_generator(matrix) - - self.assertEqual(result.returncode, 0, result.stderr) - payload = json.loads(result.stdout) - self.assertEqual(len(payload["execution_units"]), 2) - self.assertEqual(payload["execution_units"][0]["feature"], "Join channel audio") - self.assertEqual(payload["execution_units"][0]["platform_unit"], "Windows") - self.assertEqual(payload["execution_units"][0]["target_project"], "windows/") - self.assertEqual(payload["execution_units"][0]["status"], "MISSING") - self.assertEqual( - payload["execution_units"][0]["key_apis"], - ["joinChannel", "setAudioProfile"], - ) - self.assertEqual(payload["execution_units"][0]["severity"], "High") - self.assertEqual(payload["execution_units"][0]["priority"], 10) - self.assertEqual( - payload["execution_units"][0]["reference_candidates"], - [ - { - "platform_unit": "Android full", - "project": "Android/APIExample/", - "path": "basic/JoinChannelAudio.java", - }, - { - "platform_unit": "iOS UIKit", - "project": "iOS/APIExample/", - "path": "Basic/JoinChannelAudio/", - }, - { - "platform_unit": "macOS", - "project": "macOS/", - "path": "Basic/JoinChannelAudio/", - }, - ], - ) - self.assertEqual(payload["execution_units"][1]["platform_unit"], "Windows") - self.assertEqual(payload["execution_units"][1]["status"], "PARTIAL") - self.assertEqual(payload["execution_units"][1]["status_note"], "Advanced/Metadata; smoke pending") - self.assertEqual(payload["execution_units"][1]["severity"], "Unspecified") - self.assertEqual(payload["execution_units"][1]["priority"], 95) - - def test_hidden_cell_is_parsed_but_not_added_to_backlog(self): - matrix = textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | - | Rhythm player | Full RTC | `startRhythmPlayer` | `HIDDEN(advanced/RhythmPlayer.java)` | `HIDDEN(Advanced/RhythmPlayer/)` | `N/A(no case)` | `N/A(no case)` | Deprecated API retained for reference. | - """ - ) - - result = self.run_generator(matrix) - - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(json.loads(result.stdout)["execution_units"], []) - self.assertEqual( - parse_matrix_cell("HIDDEN(advanced/RhythmPlayer.java)"), - ("HIDDEN", "advanced/RhythmPlayer.java"), - ) - - def test_unknown_platform_column_fails(self): - matrix = textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Unknown Platform | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | - | Feature A | Full RTC | `apiA` | `DONE(path)` | `DONE(path)` | `DONE(path)` | `MISSING` | Missing. | - """ - ) - - result = self.run_generator(matrix) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("Unknown Platform", result.stderr) - - def test_confirmed_gap_alias_applies_priority(self): - matrix = textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | - | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(basic/JoinChannelAudio.java)` | `DONE(Basic/JoinChannelAudio/)` | `DONE(Basic/JoinChannelAudio/)` | `MISSING` | Windows has no basic audio-only join case. | - - ## Confirmed Gaps - - | Gap | Affected Units | Severity | - | --- | --- | --- | - | Basic audio-only join channel | Windows | High - missing foundational case | - """ - ) - - result = self.run_generator(matrix) - - self.assertEqual(result.returncode, 0, result.stderr) - unit = json.loads(result.stdout)["execution_units"][0] - self.assertEqual(unit["feature"], "Join channel audio") - self.assertEqual(unit["platform_unit"], "Windows") - self.assertEqual(unit["severity"], "High") - self.assertEqual(unit["priority"], 10) - - def test_legacy_project_variant_column_fails_even_without_actionable_status(self): - matrix = textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | Android Compose | iOS UIKit | macOS | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | --- | - | Feature A | Full RTC | `apiA` | `DONE(path)` | `DONE(path)` | `DONE(path)` | `DONE(path)` | `DONE(path)` | Covered. | - """ - ) - - result = self.run_generator(matrix) - - self.assertNotEqual(result.returncode, 0) - self.assertIn("platform columns must be exactly", result.stderr) - self.assertIn("Android Compose", result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/ai-engineering/tools/lint_agent_assets.py b/docs/ai-engineering/tools/lint_agent_assets.py deleted file mode 100644 index fca55b4a2..000000000 --- a/docs/ai-engineering/tools/lint_agent_assets.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 -"""Lint the repository AI engineering assets and skill bundles. - -Each rule guards a drift class that has actually broken these assets before: - -- `skills-directory-name` a project puts its skills in the legacy `.agent/skills/` -- `skill-frontmatter` a SKILL.md has no YAML frontmatter, so it cannot be selected -- `skill-name` `name` is missing, malformed, or disagrees with its directory -- `skill-description` `description` is missing or exceeds the specification limit -- `asset-reference` an AGENTS.md, ARCHITECTURE.md, or SKILL.md points at a missing file - -Exit code is 0 when every rule passes and 1 when any finding is reported. -""" - -import argparse -import json -import os -import re -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[3] - -SKILLS_DIR_NAME = ".agents" -LEGACY_SKILLS_DIR_NAMES = (".agent",) - -# Directories that never hold committed agent assets. Pruning them keeps the walk -# fast on a repository that also contains platform build output. -SKIP_DIR_NAMES = { - ".cxx", - ".git", - ".gradle", - ".idea", - "Pods", - "__pycache__", - "build", - "libs", - "node_modules", - "scope", - "sdk", - "xcuserdata", -} - -ASSET_FILE_NAMES = ("AGENTS.md", "ARCHITECTURE.md", "SKILL.md") - -FRONTMATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) -FRONTMATTER_KEY_RE = re.compile(r"^([A-Za-z0-9_-]+):\s*(.*)$") -FOLD_INDICATORS = {">", "|", ">-", "|-", ">+", "|+"} - -# Agent Skills specification: 1-64 chars, lowercase alphanumerics and single hyphens, -# no leading or trailing hyphen. -SKILL_NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") -MAX_SKILL_NAME_LENGTH = 64 -MAX_SKILL_DESCRIPTION_LENGTH = 1024 - -INLINE_CODE_RE = re.compile(r"`([^`\n]+)`") -# ARCHITECTURE directory trees commonly put exact skill paths inside fenced code -# blocks rather than inline code spans. Match those paths without interpreting -# arbitrary project source paths as agent-asset references. -BARE_SKILL_REFERENCE_RE = re.compile( - r"(?/-])" - r"((?:[A-Za-z0-9_.-]+/)*\.agents/skills/[a-z0-9]+(?:-[a-z0-9]+)*/)" -) -PLACEHOLDER_CHARS = set("<>*?|$()[]") -# Only references that identify an agent asset are resolved. Project source paths are -# written relative to a source root that varies per platform, so checking them here -# would report noise instead of drift. -ASSET_REFERENCE_MARKERS = ( - f"{SKILLS_DIR_NAME}/skills/", - "references/", - "docs/ai-engineering/", -) -ASSET_REFERENCE_SUFFIXES = ("AGENTS.md", "ARCHITECTURE.md", "SKILL.md") - - -class Finding: - """A single rule violation, reported with the location that must be edited.""" - - def __init__(self, rule, path, line, message): - self.rule = rule - self.path = path - self.line = line - self.message = message - - def as_dict(self): - return { - "rule": self.rule, - "path": self.path, - "line": self.line, - "message": self.message, - } - - def format(self): - location = f"{self.path}:{self.line}" if self.line else self.path - return f"{location}: [{self.rule}] {self.message}" - - -def relative_path(path, root): - try: - return Path(path).resolve().relative_to(Path(root).resolve()).as_posix() - except ValueError: - return Path(path).as_posix() - - -def walk_directories(root): - """Yield (dirpath, dirnames) with build and vendor directories pruned in place.""" - for dirpath, dirnames, _ in os.walk(root): - dirnames[:] = sorted(name for name in dirnames if name not in SKIP_DIR_NAMES) - yield dirpath, dirnames - - -def find_asset_files(root, names=ASSET_FILE_NAMES): - found = [] - for dirpath, _ in walk_directories(root): - for name in names: - candidate = Path(dirpath) / name - if candidate.is_file(): - found.append(candidate) - return sorted(found) - - -def parse_frontmatter(text): - """Return the frontmatter fields, or None when the block is absent. - - Only the subset of YAML used by these skills is supported: top-level scalars and - folded block scalars introduced with `>` or `|`. This avoids a PyYAML dependency - that the repository does not otherwise carry. - """ - match = FRONTMATTER_RE.match(text) - if not match: - return None - fields = {} - current_key = None - for raw_line in match.group(1).splitlines(): - if not raw_line.strip(): - continue - indented = raw_line[:1].isspace() - key_match = FRONTMATTER_KEY_RE.match(raw_line.strip()) - if key_match and not indented: - current_key = key_match.group(1) - value = key_match.group(2).strip() - fields[current_key] = "" if value in FOLD_INDICATORS else value - elif current_key is not None and indented: - continuation = raw_line.strip() - existing = fields.get(current_key, "") - fields[current_key] = f"{existing} {continuation}".strip() - return fields - - -def check_skill_directory_names(root): - findings = [] - for dirpath, dirnames in walk_directories(root): - for legacy_name in LEGACY_SKILLS_DIR_NAMES: - if legacy_name not in dirnames: - continue - legacy_dir = Path(dirpath) / legacy_name - if not (legacy_dir / "skills").is_dir(): - continue - findings.append( - Finding( - "skills-directory-name", - relative_path(legacy_dir, root), - 0, - f"skills must live in `{SKILLS_DIR_NAME}/skills/`; " - f"rename this legacy `{legacy_name}/skills/` directory", - ) - ) - return findings - - -def check_skill_metadata(skill_path, root): - findings = [] - rel = relative_path(skill_path, root) - text = skill_path.read_text(encoding="utf-8", errors="replace") - fields = parse_frontmatter(text) - if fields is None: - findings.append( - Finding( - "skill-frontmatter", - rel, - 1, - "missing YAML frontmatter; the file must open with a `---` block " - "declaring `name` and `description`", - ) - ) - return findings - - expected_name = skill_path.parent.name - name = fields.get("name", "").strip() - if not name: - findings.append(Finding("skill-name", rel, 1, "frontmatter is missing `name`")) - else: - if name != expected_name: - findings.append( - Finding( - "skill-name", - rel, - 1, - f"`name: {name}` must match the skill directory name `{expected_name}`", - ) - ) - if len(name) > MAX_SKILL_NAME_LENGTH: - findings.append( - Finding( - "skill-name", - rel, - 1, - f"`name` is {len(name)} characters; the limit is {MAX_SKILL_NAME_LENGTH}", - ) - ) - if not SKILL_NAME_RE.match(name): - findings.append( - Finding( - "skill-name", - rel, - 1, - f"`name: {name}` must use lowercase alphanumerics separated by " - "single hyphens, with no leading or trailing hyphen", - ) - ) - - description = fields.get("description", "").strip() - if not description: - findings.append( - Finding("skill-description", rel, 1, "frontmatter is missing `description`") - ) - elif len(description) > MAX_SKILL_DESCRIPTION_LENGTH: - findings.append( - Finding( - "skill-description", - rel, - 1, - f"`description` is {len(description)} characters; " - f"the limit is {MAX_SKILL_DESCRIPTION_LENGTH}", - ) - ) - return findings - - -def is_asset_reference(ref): - if not ref or ref.startswith(("http://", "https://", "#")): - return False - if any(char in ref for char in PLACEHOLDER_CHARS) or " " in ref: - return False - if ref.endswith(ASSET_REFERENCE_SUFFIXES): - return True - return any(marker in ref for marker in ASSET_REFERENCE_MARKERS) - - -def resolve_reference(ref, source_path, root): - """Resolve a reference against the source directory, then each ancestor. - - Asset docs write sibling paths (`ARCHITECTURE.md`), project-relative paths - (`APIExample/.agents/skills/...`), and repository-relative paths - (`docs/ai-engineering/knowledge-index.md`). Walking up from the source directory - accepts all three without accepting a path that exists nowhere. - """ - root = Path(root).resolve() - candidates = [root / ref] - current = Path(source_path).resolve().parent - while True: - candidates.append(current / ref) - if current == root or root not in current.parents: - break - current = current.parent - return any(candidate.exists() for candidate in candidates) - - -def check_asset_references(asset_path, root): - findings = [] - rel = relative_path(asset_path, root) - seen = set() - for line_number, line in enumerate( - asset_path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1 - ): - references = [match.group(1) for match in INLINE_CODE_RE.finditer(line)] - references.extend( - match.group(1) for match in BARE_SKILL_REFERENCE_RE.finditer(line) - ) - for raw_ref in references: - ref = raw_ref.strip().rstrip(",.;:") - if not is_asset_reference(ref): - continue - if ref in seen: - continue - seen.add(ref) - if not resolve_reference(ref, asset_path, root): - findings.append( - Finding( - "asset-reference", - rel, - line_number, - f"referenced path does not exist: {ref}", - ) - ) - return findings - - -def lint(root=REPO_ROOT): - root = Path(root) - findings = list(check_skill_directory_names(root)) - asset_files = find_asset_files(root) - for asset_path in asset_files: - if asset_path.name == "SKILL.md": - findings.extend(check_skill_metadata(asset_path, root)) - findings.extend(check_asset_references(asset_path, root)) - findings.sort(key=lambda finding: (finding.path, finding.line, finding.rule)) - return { - "checked_files": [relative_path(path, root) for path in asset_files], - "findings": [finding.as_dict() for finding in findings], - }, findings - - -def main(argv=None): - parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - parser.add_argument("--root", default=str(REPO_ROOT), help="Repository root to lint") - parser.add_argument("--json", action="store_true", help="Emit findings as JSON") - args = parser.parse_args(argv) - - try: - payload, findings = lint(Path(args.root)) - except OSError as exc: - print(f"ERROR: {exc}", file=sys.stderr) - return 1 - - if args.json: - print(json.dumps(payload, indent=2, ensure_ascii=False)) - else: - for finding in findings: - print(finding.format(), file=sys.stderr) - checked = len(payload["checked_files"]) - if findings: - print( - f"\n{len(findings)} finding(s) across {checked} agent asset file(s)", - file=sys.stderr, - ) - else: - print(f"OK: {checked} agent asset file(s) passed all rules") - return 1 if findings else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/ai-engineering/tools/lint_agent_assets_test.py b/docs/ai-engineering/tools/lint_agent_assets_test.py deleted file mode 100644 index 66db55b84..000000000 --- a/docs/ai-engineering/tools/lint_agent_assets_test.py +++ /dev/null @@ -1,353 +0,0 @@ -import json -import subprocess -import sys -import tempfile -import textwrap -import unittest -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[3] -LINTER = REPO_ROOT / "docs/ai-engineering/tools/lint_agent_assets.py" - -sys.path.insert(0, str(LINTER.parent)) -from lint_agent_assets import lint, parse_frontmatter - - -class LintAgentAssetsTest(unittest.TestCase): - def write(self, root, rel_path, text): - path = Path(root) / rel_path - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(textwrap.dedent(text).lstrip("\n"), encoding="utf-8") - return path - - def write_skill(self, root, project, skill, name=None, description="Does a thing. Use when asked.", body="Body.\n"): - name = skill if name is None else name - path = Path(root) / f"{project}/.agents/skills/{skill}/SKILL.md" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - f"---\nname: {name}\ndescription: {description}\n---\n\n{body}", - encoding="utf-8", - ) - return path - - def write_clean_tree(self, root): - """A minimal tree that must satisfy every rule.""" - self.write(root, "AGENTS.md", "# Root\n\nSee `Android/AGENTS.md`.\n") - self.write(root, "Android/AGENTS.md", "# Android\n\nSee `APIExample/AGENTS.md`.\n") - self.write( - root, - "Android/APIExample/AGENTS.md", - """ - # APIExample - - | Skill | Path | - |-------|------| - | upsert-case | `.agents/skills/upsert-case/` | - - See `ARCHITECTURE.md`. - """, - ) - self.write(root, "Android/APIExample/ARCHITECTURE.md", "# Architecture\n") - self.write_skill(root, "Android/APIExample", "upsert-case") - - def rules(self, findings): - return sorted(finding.rule for finding in findings) - - def messages(self, findings): - return "\n".join(finding.format() for finding in findings) - - def test_clean_tree_reports_no_findings(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - - payload, findings = lint(tmpdir) - - self.assertEqual(findings, [], self.messages(findings)) - self.assertIn("Android/APIExample/.agents/skills/upsert-case/SKILL.md", payload["checked_files"]) - self.assertIn("Android/APIExample/ARCHITECTURE.md", payload["checked_files"]) - self.assertEqual(payload["findings"], []) - - def test_flags_legacy_agent_skills_directory(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "iOS/APIExample/.agent/skills/review-case/SKILL.md", - """ - --- - name: review-case - description: Reviews a case. Use when reviewing. - --- - - Body. - """, - ) - - _, findings = lint(tmpdir) - - self.assertIn("skills-directory-name", self.rules(findings)) - self.assertIn("iOS/APIExample/.agent", self.messages(findings)) - - def test_ignores_dot_agent_directory_without_skills(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - (Path(tmpdir) / "iOS/.agent/notes").mkdir(parents=True) - - _, findings = lint(tmpdir) - - self.assertEqual(findings, [], self.messages(findings)) - - def test_flags_skill_without_frontmatter(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "Android/APIExample/.agents/skills/query-cases/SKILL.md", - "## What this skill is for\n\nBody.\n", - ) - - _, findings = lint(tmpdir) - - self.assertEqual(self.rules(findings), ["skill-frontmatter"]) - - def test_flags_name_that_disagrees_with_directory(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write_skill(tmpdir, "Android/APIExample", "query-cases", name="query-case") - - _, findings = lint(tmpdir) - - self.assertEqual(self.rules(findings), ["skill-name"]) - self.assertIn("must match the skill directory name `query-cases`", self.messages(findings)) - - def test_flags_malformed_name_charset(self): - for bad_name in ["Upsert_Case", "-upsert", "upsert-", "upsert--case"]: - with self.subTest(name=bad_name), tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - f"Android/APIExample/.agents/skills/{bad_name}/SKILL.md", - f""" - --- - name: {bad_name} - description: Does a thing. Use when asked. - --- - - Body. - """, - ) - - _, findings = lint(tmpdir) - - self.assertIn("skill-name", self.rules(findings), self.messages(findings)) - - def test_flags_missing_and_oversized_description(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "Android/APIExample/.agents/skills/query-cases/SKILL.md", - """ - --- - name: query-cases - --- - - Body. - """, - ) - - _, findings = lint(tmpdir) - - self.assertEqual(self.rules(findings), ["skill-description"]) - - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write_skill(tmpdir, "Android/APIExample", "query-cases", description="x" * 1025) - - _, findings = lint(tmpdir) - - self.assertEqual(self.rules(findings), ["skill-description"]) - self.assertIn("the limit is 1024", self.messages(findings)) - - def test_accepts_description_at_the_limit(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write_skill(tmpdir, "Android/APIExample", "query-cases", description="x" * 1024) - - _, findings = lint(tmpdir) - - self.assertEqual(findings, [], self.messages(findings)) - - def test_flags_agents_md_pointing_at_missing_skill_directory(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "Android/APIExample/AGENTS.md", - """ - # APIExample - - | review-case | `.agents/skills/review-case/` | - - See `ARCHITECTURE.md`. - """, - ) - - _, findings = lint(tmpdir) - - self.assertEqual(self.rules(findings), ["asset-reference"]) - self.assertIn(".agents/skills/review-case/", self.messages(findings)) - - def test_flags_architecture_md_pointing_at_missing_skill_directory(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "Android/APIExample/ARCHITECTURE.md", - """ - ```text - APIExample/ - .agents/skills/query-cases/ # Missing inventory skill - ``` - """, - ) - - payload, findings = lint(tmpdir) - - self.assertIn("Android/APIExample/ARCHITECTURE.md", payload["checked_files"]) - self.assertEqual(self.rules(findings), ["asset-reference"]) - self.assertIn(".agents/skills/query-cases/", self.messages(findings)) - - def test_flags_skill_pointing_at_missing_bundled_reference(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write_skill( - tmpdir, - "Android/APIExample", - "upsert-case", - body="Use `APIExample/.agents/skills/upsert-case/references/fragment-template.java`.\n", - ) - - _, findings = lint(tmpdir) - - self.assertEqual(self.rules(findings), ["asset-reference"]) - self.assertIn("fragment-template.java", self.messages(findings)) - - def test_resolves_project_relative_reference_via_ancestor_walk(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write_skill( - tmpdir, - "Android/APIExample", - "upsert-case", - body="Use `APIExample/.agents/skills/upsert-case/references/fragment-template.java`.\n", - ) - self.write( - tmpdir, - "Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java", - "class Template {}\n", - ) - - _, findings = lint(tmpdir) - - self.assertEqual(findings, [], self.messages(findings)) - - def test_skips_placeholder_and_url_references(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write_skill( - tmpdir, - "Android/APIExample", - "upsert-case", - body=( - "Read `/AGENTS.md` and `/ARCHITECTURE.md`.\n" - "Glob `.agents/skills/*/SKILL.md`.\n" - "See https://agentskills.io/specification for the spec.\n" - ), - ) - - _, findings = lint(tmpdir) - - self.assertEqual(findings, [], self.messages(findings)) - - def test_prunes_build_and_vendor_directories(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "Android/APIExample/build/.agents/skills/stale/SKILL.md", - "no frontmatter here\n", - ) - self.write(tmpdir, "Pods/AGENTS.md", "Points at `missing/AGENTS.md`.\n") - - payload, findings = lint(tmpdir) - - self.assertEqual(findings, [], self.messages(findings)) - self.assertFalse([path for path in payload["checked_files"] if "build/" in path]) - self.assertFalse([path for path in payload["checked_files"] if path.startswith("Pods/")]) - - def test_parses_folded_block_scalar_description(self): - fields = parse_frontmatter( - textwrap.dedent( - """ - --- - name: upsert-case - description: > - Add a new case or modify an existing one. - Use when: adding a demo screen. - --- - - Body. - """ - ).lstrip("\n") - ) - - self.assertEqual(fields["name"], "upsert-case") - self.assertEqual( - fields["description"], - "Add a new case or modify an existing one. Use when: adding a demo screen.", - ) - - def test_returns_none_without_frontmatter(self): - self.assertIsNone(parse_frontmatter("## Heading\n\nBody.\n")) - - def test_cli_reports_findings_and_exits_nonzero(self): - with tempfile.TemporaryDirectory() as tmpdir: - self.write_clean_tree(tmpdir) - self.write( - tmpdir, - "Android/APIExample/.agents/skills/query-cases/SKILL.md", - "## No frontmatter\n", - ) - - result = subprocess.run( - [sys.executable, str(LINTER), "--root", tmpdir, "--json"], - cwd=REPO_ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) - - self.assertEqual(result.returncode, 1, result.stderr) - payload = json.loads(result.stdout) - self.assertEqual( - [finding["rule"] for finding in payload["findings"]], ["skill-frontmatter"] - ) - - def test_cli_passes_on_the_live_repository(self): - result = subprocess.run( - [sys.executable, str(LINTER)], - cwd=REPO_ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) - - self.assertEqual(result.returncode, 0, result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/ai-engineering/tools/orchestrate_case_execution.py b/docs/ai-engineering/tools/orchestrate_case_execution.py deleted file mode 100644 index b23f920af..000000000 --- a/docs/ai-engineering/tools/orchestrate_case_execution.py +++ /dev/null @@ -1,1676 +0,0 @@ -#!/usr/bin/env python3 -"""Run one cross-platform API Examples requirement workflow.""" - -import argparse -import concurrent.futures -import hashlib -import json -import os -import signal -import stat -import subprocess -import sys -from pathlib import Path - -from generate_case_backlog import parse_matrix_cell, split_markdown_row -from prepare_case_execution import ( - DEFAULT_REPOSITORY_PROFILE, - PLATFORMS, - collect_sdk_version_checks, - load_repository_profile, - prepare_case_execution, -) -from validate_acceptance_manifest import ( - MAIN_PLATFORM_TARGETS, - PLATFORM_ROOTS, - is_platform_build_command, - normalize_command, - path_is_within, - validate_contract, - validate_evidence_files, - validate_manifest, - validate_platform_implementation, - validate_platform_verification, -) - - -ROLE_ORDER = ["contract", "implementation", "verification"] -PHASE_ORDER = ["contract", "implementation", "verification"] -REPO_ROOT = Path(__file__).resolve().parents[3] -DEFAULT_ROUTING_CONFIG = REPO_ROOT / "docs/ai-engineering/role-routing.json" -COMPLETED_DEPENDENCY_STATUSES = {"PASS", "WAIVED"} -RESULT_VALUES = ["PASS", "FAIL", "BLOCKED", "SKIPPED"] -ROLE_STATUS_VALUES = ["PASS", "FAIL", "BLOCKED", "WAIVED"] - -STRING = {"type": "string"} -STRING_LIST = {"type": "array", "items": STRING} -RESULT = {"type": "string", "enum": RESULT_VALUES} -REFERENCE = { - "type": "object", - "additionalProperties": False, - "required": ["required", "source_case", "contract_result"], - "properties": { - "required": {"type": "boolean"}, - "source_case": STRING, - "contract_result": RESULT, - }, -} -PLATFORM_TARGET = { - "type": "object", - "additionalProperties": False, - "required": [ - "required", - "target_project", - "key_constraints", - "files_allowed", - "waiver_reason", - ], - "properties": { - "required": {"type": "boolean"}, - "target_project": STRING, - "key_constraints": STRING_LIST, - "files_allowed": STRING_LIST, - "waiver_reason": STRING, - }, -} -PLATFORM_TARGETS = { - "type": "object", - "additionalProperties": False, - "required": PLATFORMS, - "properties": {platform: PLATFORM_TARGET for platform in PLATFORMS}, -} -MATRIX_UPDATE = { - "type": "object", - "additionalProperties": False, - "required": ["feature", "platform_unit", "from", "to", "to_cell", "evidence"], - "properties": { - "feature": STRING, - "platform_unit": STRING, - "from": STRING, - "to": STRING, - "to_cell": STRING, - "evidence": STRING, - }, -} -COMMAND_LIST = { - "type": "array", - "items": { - "type": "object", - "additionalProperties": False, - "required": ["kind", "command", "result"], - "properties": { - "kind": {"type": "string", "enum": ["build", "test", "static", "runtime"]}, - "command": STRING, - "result": RESULT, - }, - }, -} -SKIPPED_CHECK_LIST = { - "type": "array", - "items": { - "type": "object", - "additionalProperties": False, - "required": ["name", "reason"], - "properties": {"name": STRING, "reason": STRING}, - }, -} -ROLE_OUTPUT_PROPERTIES = { - "contract": { - "scenario": STRING, - "key_apis": STRING_LIST, - "non_goals": STRING_LIST, - "reference": REFERENCE, - "cross_platform_requirements": STRING_LIST, - "platform_targets": PLATFORM_TARGETS, - }, - "implementation": { - "target_project": STRING, - "query_cases": STRING, - "upsert_case": STRING, - "files_changed": STRING_LIST, - "matrix_updates": {"type": "array", "items": MATRIX_UPDATE}, - }, - "verification": { - "result": RESULT, - "findings": STRING_LIST, - "parity_result": RESULT, - "entry_point": STRING, - "ux_notes": STRING, - "commands": COMMAND_LIST, - "build_result": RESULT, - "skipped_checks": SKIPPED_CHECK_LIST, - }, -} - - -def write_json(path, payload): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") - - -def read_json(path): - return json.loads(path.read_text(encoding="utf-8")) - - -def load_role_routing(path): - routing = read_json(path) - if routing.get("version") != 1: - raise ValueError("role routing version must be 1") - profiles = routing.get("profiles") - roles = routing.get("roles") - phases = routing.get("phases") - if not isinstance(profiles, dict) or not profiles: - raise ValueError("role routing profiles must be a non-empty object") - if not isinstance(roles, dict) or set(roles) != set(ROLE_ORDER): - raise ValueError(f"role routing must define exactly: {', '.join(ROLE_ORDER)}") - if phases != PHASE_ORDER: - raise ValueError(f"role routing phases must be: {', '.join(PHASE_ORDER)}") - for role, route in roles.items(): - if route.get("phase") != role: - raise ValueError(f"role routing {role}.phase must be {role}") - profile = route.get("profile") - if profile not in profiles: - raise ValueError(f"role routing {role}.profile={profile} is not defined") - if "reasoning_effort" not in profiles[profile]: - raise ValueError(f"role routing profile {profile} needs reasoning_effort") - return routing - - -def init_workspace(args): - run_dir = Path(args.run_dir).resolve() - run_dir.mkdir(parents=True, exist_ok=True) - matrix_path = Path(args.matrix).resolve() - routing_path = Path(args.routing_config).resolve() - profile_path = Path(args.repository_profile).resolve() - routing = load_role_routing(routing_path) - load_repository_profile(profile_path) - package = prepare_case_execution( - matrix_path, - args.feature, - index=args.index, - sdk_family=args.sdk_family, - key_apis=args.key_api, - target_sdk_version=args.target_sdk_version, - platform_sdk_versions=args.platform_sdk_version, - repository_profile=profile_path, - ) - package["matrix_path"] = stable_matrix_path(matrix_path) - package["routing_config"] = repo_relative_path(routing_path) - package["routing_config_sha256"] = sha256_file(routing_path) - package["repository_profile"] = repo_relative_path(profile_path) - package["repository_profile_sha256"] = sha256_file(profile_path) - write_json(run_dir / "execution-package.json", package) - write_json(run_dir / "acceptance-manifest.json", package["acceptance_manifest_seed"]) - - contract = package["acceptance_manifest_seed"]["contract"] - write_artifact_seed(run_dir, "contract", "contract", None, contract) - write_role_schema(run_dir / "role-schemas/contract.json", "contract") - write_prompt(run_dir, "contract", "contract", None, package, routing, {}) - for platform in PLATFORMS: - unit = package["acceptance_manifest_seed"]["platforms"][platform] - for role in ["implementation", "verification"]: - name = artifact_name(role, platform) - write_artifact_seed(run_dir, name, role, platform, unit[role]) - write_role_schema(run_dir / "role-schemas" / f"{name}.json", role) - write_prompt(run_dir, name, role, platform, package, routing, {}) - print(f"Execution workspace initialized: {run_dir}") - return 0 - - -def write_artifact_seed(run_dir, name, role, platform, artifact): - payload = {"name": name, "role": role, **artifact} - if platform: - payload["platform"] = platform - write_json(run_dir / "role-artifacts" / f"{name}.json", payload) - - -def write_role_schema(path, role): - output_properties = ROLE_OUTPUT_PROPERTIES[role] - schema = { - "type": "object", - "additionalProperties": False, - "required": ["status", "evidence", "summary", "output"], - "properties": { - "status": {"type": "string", "enum": ROLE_STATUS_VALUES}, - "evidence": STRING, - "summary": STRING, - "waiver_reason": STRING, - "output": { - "type": "object", - "additionalProperties": False, - "required": list(output_properties), - "properties": output_properties, - }, - }, - } - write_json(path, schema) - - -def write_prompt(run_dir, name, role, platform, package, routing, dependencies): - requirement = package["requirement"] - target = "all official platforms" - if platform: - target = resolved_platform_target(package, platform, dependencies) - host_note = host_constraint(platform) if role == "verification" else "" - waiver_note = "" - if platform: - waiver_note = ( - "If Contract marks this platform required=false, do not modify or verify it. " - "Return status=WAIVED, copy Contract's waiver_reason into the top-level " - "waiver_reason field, use SKIPPED for Verification result/parity_result/build_result, " - "and report no executed build command." - ) - contract_context = "" - if role == "contract": - contract_context = f"""Selected platform units: -{json.dumps(package['platform_units'], indent=2, ensure_ascii=False)} - -Prepared reference contract: -{json.dumps(package['reference_contract'], indent=2, ensure_ascii=False)} - -Use this routing and reference context when producing platform_targets and reference. Every -target_project must remain the main APIExample project declared for that platform. -""" - body = f"""# {name} Agent Task - -Requirement: {requirement['feature']} -SDK family: {requirement['sdk_family']} -Key APIs: {', '.join(requirement['key_apis'])} -Target SDK versions: -{json.dumps(requirement['target_sdk_versions'], indent=2, ensure_ascii=False)} -Role: {role} -Platform: {platform or 'shared'} -Target: {target} -Model profile: {routing['roles'][role]['profile']} - -Required output: -{json.dumps(package['role_contracts'][role], indent=2, ensure_ascii=False)} - -{contract_context} - -Dependency artifacts: -{format_dependency_context(dependencies)} - -The dispatcher starts platform work in the Contract-selected target so nested AGENTS.md files load automatically. Confirm those instructions before acting. Keep platform source changes inside the Contract target and never share source files across platform roots. - -{host_note} - -For Verification, report every executed check with kind=build|test|static|runtime and copy the command string exactly from Codex command telemetry. The dispatcher binds the declared result to the JSONL exit code. - -{waiver_note} - -Return only the JSON object required by the supplied output schema. The dispatcher records identity and provenance. -""" - path = run_dir / "role-prompts" / f"{name}.md" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(body, encoding="utf-8") - return path - - -def host_constraint(platform): - if platform == "windows" and sys.platform != "win32": - host = "macOS" if sys.platform == "darwin" else sys.platform - return ( - f"This host is {host}. {host} cannot provide Windows build evidence. " - "Do not download Windows SDK archives, emulate Windows, cross-compile, or use a " - "substitute compiler as platform evidence. Run repository-local static checks only " - "and report unavailable Windows build/runtime evidence as BLOCKED." - ) - if platform in {"ios", "macos"} and sys.platform != "darwin": - return ( - f"This host is {sys.platform}. {sys.platform} cannot provide {platform} Xcode build " - "evidence. Run repository-local static checks only and report unavailable native " - "build/runtime evidence as BLOCKED. A build_result=PASS requires host_platform=darwin." - ) - return "" - - -def resolved_platform_target(package, platform, dependencies): - contract = dependencies.get("contract", {}) - output = contract.get("output", {}) if isinstance(contract, dict) else {} - targets = output.get("platform_targets", {}) if isinstance(output, dict) else {} - if platform in targets and targets[platform].get("target_project"): - return targets[platform]["target_project"] - return package["acceptance_manifest_seed"]["contract"]["output"]["platform_targets"][platform][ - "target_project" - ] - - -def format_dependency_context(dependencies): - if not dependencies: - return "None." - sections = [] - for name, artifact in dependencies.items(): - payload = { - "status": artifact.get("status"), - "summary": artifact.get("summary"), - "output": artifact.get("output"), - } - sections.append(f"- {name}:\n```json\n{json.dumps(payload, indent=2, ensure_ascii=False)}\n```") - return "\n".join(sections) - - -def dispatch_workspace(args): - run_dir = Path(args.run_dir).resolve() - package = read_json(run_dir / "execution-package.json") - routing_path = Path(args.routing_config).resolve() - routing = load_role_routing(routing_path) - validate_execution_configuration(package, routing_path) - specs = dispatch_specs(run_dir, args.phase, args.platform, args.retry) - route = routing["roles"][args.phase] - profile = route["profile"] - model = resolve_model(profile, args.model) - codex_version = "dry-run" if args.dry_run else read_codex_version(args.codex_bin) - tasks = [] - for spec in specs: - dependencies = dependencies_for_spec(run_dir, spec) - working_directory = resolve_working_directory(package, spec, dependencies) - resolved = { - "version": 1, - "routing_config": repo_relative_path(routing_path), - "routing_config_sha256": sha256_file(routing_path), - "phase": args.phase, - "name": spec["name"], - "platform": spec["platform"], - "profile": profile, - "model": model, - "reasoning_effort": routing["profiles"][profile]["reasoning_effort"], - "codex_version": codex_version, - "working_directory": repo_relative_path(working_directory), - } - write_json(run_dir / "resolved-role-routing" / f"{spec['name']}.json", resolved) - snapshot_path, snapshot_sha256 = write_input_snapshot( - run_dir, args.phase, spec["name"], dependencies, resolved - ) - input_snapshot = read_json(snapshot_path) - expected_repository_state = input_snapshot["repository_state"] - prompt_path = write_prompt( - run_dir, spec["name"], spec["role"], spec["platform"], package, routing, dependencies - ) - schema_path = run_dir / "role-schemas" / f"{spec['name']}.json" - write_role_schema(schema_path, spec["role"]) - raw_result_path = run_dir / "raw-results" / f"{spec['name']}.json" - command = build_codex_command( - args.codex_bin, - route, - model, - routing["profiles"][profile]["reasoning_effort"], - schema_path, - raw_result_path, - working_directory, - ) - tasks.append( - { - **spec, - "command": command, - "prompt_path": prompt_path, - "artifact_path": run_dir / "role-artifacts" / f"{spec['name']}.json", - "raw_result_path": raw_result_path, - "log_path": run_dir / "dispatch-logs" / f"{spec['name']}.jsonl", - "stderr_path": run_dir / "dispatch-logs" / f"{spec['name']}.stderr.log", - "model_profile": profile, - "model": model, - "reasoning_effort": routing["profiles"][profile]["reasoning_effort"], - "codex_version": codex_version, - "working_directory": working_directory, - "working_directory_manifest": repo_relative_path(working_directory), - "input_snapshot": run_relative_path(snapshot_path, run_dir), - "input_snapshot_sha256": snapshot_sha256, - "expected_repository_state": expected_repository_state, - "snapshot_dependencies": dependencies, - "resolved_routing": resolved, - "previous_files_changed": ( - read_json(run_dir / "role-artifacts" / f"{spec['name']}.json") - .get("output", {}) - .get("files_changed", []) - if spec["role"] == "implementation" - else [] - ), - "timeout_seconds": args.timeout_seconds, - } - ) - if args.dry_run: - print( - json.dumps( - { - "phase": args.phase, - "commands": [ - { - "name": task["name"], - "platform": task["platform"], - "command": task["command"], - } - for task in tasks - ], - }, - indent=2, - ensure_ascii=False, - ) - ) - return 0 - if args.phase == "implementation": - for task in tasks: - invalidate_platform_verification(run_dir, package, task["platform"]) - run_implementation_tasks(run_dir, tasks) - elif args.phase == "verification": - run_verification_tasks(run_dir, tasks) - else: - run_tasks(tasks) - if args.phase == "contract": - validate_completed_contract(run_dir) - print(f"Codex phase completed: {args.phase} ({', '.join(task['name'] for task in tasks)})") - return 0 - - -def dispatch_specs(run_dir, phase, requested_platforms, retry): - if phase == "contract": - if requested_platforms: - raise ValueError("--platform is not valid for the shared contract phase") - candidates = [{"name": "contract", "role": "contract", "platform": None}] - else: - platforms = requested_platforms or PLATFORMS - candidates = [ - {"name": artifact_name(phase, platform), "role": phase, "platform": platform} - for platform in platforms - ] - selected = [] - for spec in candidates: - path = run_dir / "role-artifacts" / f"{spec['name']}.json" - if not path.exists(): - raise ValueError(f"missing role artifact: {spec['name']}") - artifact = read_json(path) - already_dispatched = artifact.get("dispatch", {}).get("mode") == "codex-exec" - status = artifact.get("status") - if already_dispatched: - if retry and status in {"FAIL", "BLOCKED"}: - selected.append(spec) - elif requested_platforms or phase == "contract": - raise ValueError(f"role {spec['name']} has already been dispatched with status {status}") - continue - selected.append(spec) - if not selected: - raise ValueError(f"all {phase} role artifacts are already complete") - return selected - - -def validate_completed_contract(run_dir): - artifact_path = run_dir / "role-artifacts/contract.json" - artifact = read_json(artifact_path) - if artifact.get("status") != "PASS": - if artifact.get("status") not in {"FAIL", "BLOCKED"}: - artifact["status"] = "BLOCKED" - artifact["evidence"] = "Contract is required and cannot be waived." - artifact["summary"] = "Contract requires correction before platform implementation." - write_json(artifact_path, artifact) - raise ValueError(f"Contract gate returned status {artifact.get('status')}") - manifest = read_json(run_dir / "acceptance-manifest.json") - manifest["contract"] = strip_artifact_metadata(artifact) - errors = [] - validate_contract(manifest, errors, {}) - if not errors: - return - artifact["status"] = "BLOCKED" - artifact["evidence"] = "Contract semantic gate failed: " + "; ".join(errors) - artifact["summary"] = "Contract requires correction before platform implementation." - write_json(artifact_path, artifact) - raise ValueError("Contract semantic gate failed: " + "; ".join(errors)) - - -def dependencies_for_spec(run_dir, spec): - dependency_names = [] - if spec["role"] == "implementation": - dependency_names = ["contract"] - elif spec["role"] == "verification": - dependency_names = ["contract", artifact_name("implementation", spec["platform"])] - dependencies = {} - for name in dependency_names: - if name == "contract": - validate_completed_contract(run_dir) - if name.endswith("-implementation"): - validate_completed_implementation(run_dir, spec["platform"]) - path = run_dir / "role-artifacts" / f"{name}.json" - if not path.exists(): - raise ValueError(f"role {spec['name']} requires missing dependency artifact: {name}") - artifact = read_json(path) - status = artifact.get("status") - allowed_statuses = {"PASS"} if name == "contract" else COMPLETED_DEPENDENCY_STATUSES - if status not in allowed_statuses: - raise ValueError( - f"role {spec['name']} requires {name} status " - f"{'PASS' if name == 'contract' else 'PASS or WAIVED'}, got {status}" - ) - dependencies[name] = artifact - return dependencies - - -def invalidate_platform_verification(run_dir, package, platform): - seed = package["acceptance_manifest_seed"]["platforms"][platform]["verification"] - name = artifact_name("verification", platform) - write_artifact_seed(run_dir, name, "verification", platform, seed) - - -def resolve_working_directory(package, spec, dependencies): - if not spec["platform"]: - return REPO_ROOT - target = resolved_platform_target(package, spec["platform"], dependencies) - expected_target = MAIN_PLATFORM_TARGETS[spec["platform"]] - if target != expected_target: - raise ValueError( - f"{spec['platform']} target must be exactly {expected_target}: {target}" - ) - path = (REPO_ROOT / target).resolve() - try: - path.relative_to(REPO_ROOT) - except ValueError as exc: - raise ValueError(f"platform target must be inside the repository: {target}") from exc - if not path.is_dir(): - raise ValueError(f"platform target directory does not exist: {target}") - return path - - -def run_tasks(tasks): - errors = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(tasks), len(PLATFORMS))) as executor: - future_tasks = {executor.submit(run_codex_role, **task_args(task)): task for task in tasks} - for future in concurrent.futures.as_completed(future_tasks): - task = future_tasks[future] - try: - future.result() - except (OSError, ValueError, json.JSONDecodeError) as exc: - errors.append((task["name"], str(exc))) - if errors: - errors.sort() - raise ValueError("; ".join(detail for _, detail in errors)) - - -def run_verification_tasks(run_dir, tasks): - errors = [] - try: - run_tasks(tasks) - except ValueError as exc: - errors.append(str(exc)) - for task in tasks: - try: - validate_completed_verification(run_dir, task["platform"]) - except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: - errors.append(str(exc)) - if errors: - raise ValueError("; ".join(errors)) - - -def run_implementation_tasks(run_dir, tasks): - errors = [] - for task in tasks: - refresh_implementation_input_snapshot(run_dir, task) - before = collect_repository_fingerprints() - try: - run_codex_role(**task_args(task)) - except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: - errors.append((task["name"], str(exc))) - try: - reconcile_implementation_changes(run_dir, [task], before) - except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: - errors.append((task["name"], str(exc))) - try: - validate_completed_implementation(run_dir, task["platform"]) - except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: - errors.append((task["name"], str(exc))) - if errors: - errors.sort() - raise ValueError("; ".join(detail for _, detail in errors)) - - -def validate_completed_implementation(run_dir, platform): - artifact_path = run_dir / "role-artifacts" / f"{platform}-implementation.json" - artifact = read_json(artifact_path) - contract = read_json(run_dir / "role-artifacts/contract.json") - target = contract.get("output", {}).get("platform_targets", {}).get(platform, {}) - required = bool(target.get("required", True)) - expected_status = "PASS" if required else "WAIVED" - errors = [] - if artifact.get("status") != expected_status: - errors.append( - f"{platform} Implementation gate requires status {expected_status}, " - f"got {artifact.get('status')}" - ) - manifest = read_json(run_dir / "acceptance-manifest.json") - manifest["final_status"] = "PASS" - verification_stub = { - "build_result": "PASS", - "parity_result": "PASS", - "skipped_checks": [], - } - output = artifact.get("output") - if not isinstance(output, dict) or not output: - errors.append(f"{platform} Implementation output must be a non-empty object") - else: - validate_platform_implementation( - platform, - target, - output, - verification_stub, - manifest, - errors, - ) - if not errors: - return - if artifact.get("status") not in {"FAIL", "BLOCKED"}: - artifact["status"] = "BLOCKED" - artifact["evidence"] = "Implementation semantic gate failed: " + "; ".join(errors) - artifact["summary"] = "Implementation requires correction before Verification." - write_json(artifact_path, artifact) - raise ValueError("Implementation semantic gate failed: " + "; ".join(errors)) - - -def validate_completed_verification(run_dir, platform): - artifact_path = run_dir / "role-artifacts" / f"{platform}-verification.json" - artifact = read_json(artifact_path) - contract = read_json(run_dir / "role-artifacts/contract.json") - target = contract.get("output", {}).get("platform_targets", {}).get(platform, {}) - required = bool(target.get("required", True)) - status = artifact.get("status") - if required and status in {"FAIL", "BLOCKED"}: - return - errors = [] - if required and status != "PASS": - errors.append(f"{platform} Verification gate requires PASS, FAIL, or BLOCKED, got {status}") - if not required and status != "WAIVED": - errors.append(f"{platform} Verification gate requires status WAIVED, got {status}") - manifest = read_json(run_dir / "acceptance-manifest.json") - manifest["final_status"] = "PASS WITH RISKS" - output = artifact.get("output") - if not isinstance(output, dict) or not output: - errors.append(f"{platform} Verification output must be a non-empty object") - else: - validate_platform_verification( - platform, - required, - output, - artifact.get("dispatch", {}), - manifest, - errors, - ) - if not errors: - return - if status not in {"FAIL", "BLOCKED"}: - artifact["status"] = "BLOCKED" - artifact["evidence"] = "Verification semantic gate failed: " + "; ".join(errors) - artifact["summary"] = "Verification requires correction before acceptance." - write_json(artifact_path, artifact) - raise ValueError("Verification semantic gate failed: " + "; ".join(errors)) - - -def refresh_implementation_input_snapshot(run_dir, task): - snapshot_path, snapshot_sha256 = write_input_snapshot( - run_dir, - "implementation", - task["name"], - task["snapshot_dependencies"], - task["resolved_routing"], - ) - task["input_snapshot"] = run_relative_path(snapshot_path, run_dir) - task["input_snapshot_sha256"] = snapshot_sha256 - task["expected_repository_state"] = read_json(snapshot_path)["repository_state"] - - -def task_args(task): - allowed = { - "command", - "name", - "role", - "platform", - "prompt_path", - "artifact_path", - "raw_result_path", - "log_path", - "stderr_path", - "model_profile", - "model", - "reasoning_effort", - "codex_version", - "working_directory", - "working_directory_manifest", - "input_snapshot", - "input_snapshot_sha256", - "expected_repository_state", - "timeout_seconds", - } - return {key: value for key, value in task.items() if key in allowed} - - -def resolve_bound_repository_profile(package): - profile_value = package.get("repository_profile") - if not isinstance(profile_value, str) or not profile_value: - raise ValueError("execution package repository profile is missing; re-run init") - profile_path = (REPO_ROOT / profile_value).resolve() - try: - profile_path.relative_to(REPO_ROOT.resolve()) - except ValueError as exc: - raise ValueError("execution package repository profile must be inside the repository") from exc - if package.get("repository_profile_sha256") != sha256_file(profile_path): - raise ValueError("repository profile content changed since init; re-run init") - load_repository_profile(profile_path) - return profile_path - - -def validate_execution_configuration(package, routing_path): - if package.get("routing_config") != repo_relative_path(routing_path): - raise ValueError("routing config path changed since init; re-run init") - if package.get("routing_config_sha256") != sha256_file(routing_path): - raise ValueError("routing config content changed since init; re-run init") - resolve_bound_repository_profile(package) - - -def resolve_model(profile, override): - model = override or os.environ.get(f"CODEX_MODEL_{profile.upper()}") - if not model: - raise ValueError(f"model is required; pass --model or set CODEX_MODEL_{profile.upper()}") - return model - - -def read_codex_version(codex_bin): - try: - result = subprocess.run( - [*codex_command_prefix(codex_bin), "--version"], - cwd=REPO_ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=10, - ) - except subprocess.TimeoutExpired as exc: - raise ValueError("timed out while reading Codex version") from exc - if result.returncode != 0: - raise ValueError(f"failed to read Codex version: {result.stderr.strip()}") - if not result.stdout.strip(): - raise ValueError("Codex version output is empty") - return result.stdout.strip() - - -def codex_command_prefix(codex_bin): - path = str(codex_bin) - if Path(path).suffix.lower() == ".py": - return [sys.executable, path] - return [path] - - -def write_input_snapshot(run_dir, phase, name, dependencies, resolved_routing): - package = read_json(run_dir / "execution-package.json") - dependency_hashes = { - dependency: sha256_file(run_dir / "role-artifacts" / f"{dependency}.json") - for dependency in dependencies - } - snapshot = { - "version": 1, - "phase": phase, - "name": name, - "execution_package_sha256": sha256_file(run_dir / "execution-package.json"), - "repository_state": repository_state(), - "dependency_artifact_sha256": dependency_hashes, - "routing_config": resolved_routing["routing_config"], - "routing_config_sha256": resolved_routing["routing_config_sha256"], - "repository_profile": package["repository_profile"], - "repository_profile_sha256": package["repository_profile_sha256"], - } - serialized = json.dumps(snapshot, indent=2, ensure_ascii=False) + "\n" - snapshot_sha256 = sha256_text(serialized) - path = run_dir / "input-snapshots" / f"{name}-{snapshot_sha256[:12]}.json" - path.parent.mkdir(parents=True, exist_ok=True) - if path.exists() and path.read_text(encoding="utf-8") != serialized: - raise ValueError(f"input snapshot hash collision: {path}") - path.write_text(serialized, encoding="utf-8") - return path, snapshot_sha256 - - -def run_git(arguments): - result = subprocess.run( - ["git", *arguments], cwd=REPO_ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - if result.returncode != 0: - raise ValueError(f"git {' '.join(arguments)} failed: {result.stderr.strip()}") - return result.stdout.rstrip("\n") - - -def run_git_bytes(arguments): - result = subprocess.run( - ["git", *arguments], cwd=REPO_ROOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - if result.returncode != 0: - detail = result.stderr.decode("utf-8", errors="replace").strip() - raise ValueError(f"git {' '.join(arguments)} failed: {detail}") - return result.stdout - - -def collect_repository_fingerprints(repo_root=REPO_ROOT, include_tracked=True): - repo_root = Path(repo_root) - fingerprints = {} - if include_tracked: - staged = run_git_bytes_at(repo_root, ["ls-files", "--stage", "-z"]) - for record in filter(None, staged.split(b"\0")): - metadata, separator, raw_path = record.partition(b"\t") - if not separator: - continue - mode, object_id, stage = metadata.split() - path_text = os.fsdecode(raw_path) - fingerprints[path_text] = { - "type": "index", - "mode": mode.decode("ascii"), - "object_id": object_id.decode("ascii"), - "stage": stage.decode("ascii"), - } - dirty = run_git_bytes_at(repo_root, ["diff", "--name-only", "-z"]) - for raw_path in filter(None, dirty.split(b"\0")): - path_text = os.fsdecode(raw_path) - fingerprints[path_text] = fingerprint_repository_path(repo_root / path_text) - - untracked = run_git_bytes_at( - repo_root, ["ls-files", "-z", "--others", "--exclude-standard"] - ) - for raw_path in filter(None, untracked.split(b"\0")): - path_text = os.fsdecode(raw_path) - fingerprints[path_text] = fingerprint_repository_path(repo_root / path_text) - return dict(sorted(fingerprints.items())) - - -def run_git_bytes_at(repo_root, arguments): - result = subprocess.run( - ["git", *arguments], cwd=repo_root, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - if result.returncode != 0: - detail = result.stderr.decode("utf-8", errors="replace").strip() - raise ValueError(f"git {' '.join(arguments)} failed: {detail}") - return result.stdout - - -def fingerprint_repository_path(path): - try: - metadata = path.lstat() - except FileNotFoundError: - return {"type": "missing", "mode": "", "sha256": ""} - mode = oct(stat.S_IMODE(metadata.st_mode)) - if path.is_symlink(): - return { - "type": "symlink", - "mode": mode, - "sha256": sha256_text(os.readlink(path)), - } - if path.is_file(): - return {"type": "file", "mode": mode, "sha256": sha256_file(path)} - return {"type": "other", "mode": mode, "sha256": ""} - - -def repository_state(): - return { - "head": run_git(["rev-parse", "HEAD"]), - "status": run_git(["status", "--short"]), - "diff_sha256": sha256_bytes(run_git_bytes(["diff", "--binary", "HEAD"])), - "untracked_files": collect_repository_fingerprints(include_tracked=False), - } - - -def changed_repository_paths(before, after): - return sorted( - path - for path in set(before) | set(after) - if before.get(path) != after.get(path) - ) - - -def normalize_repo_path_text(path_text): - path = Path(path_text) - if path.is_absolute(): - return repo_relative_path(path) - normalized = str(path_text).replace("\\", "/") - return normalized[2:] if normalized.startswith("./") else normalized - - -def reconcile_implementation_changes(run_dir, tasks, before): - after = collect_repository_fingerprints() - changed = changed_repository_paths(before, after) - dispatched_platforms = {task["platform"] for task in tasks} - unexpected = [ - path - for path in changed - if not any( - platform in dispatched_platforms and path_is_within(path, root) - for platform, root in PLATFORM_ROOTS.items() - ) - ] - contract = read_json(run_dir / "role-artifacts/contract.json") - targets = contract.get("output", {}).get("platform_targets", {}) - scope_errors = [] - for task in tasks: - platform = task["platform"] - platform_changes = [ - path for path in changed if path_is_within(path, PLATFORM_ROOTS[platform]) - ] - allowed = targets.get(platform, {}).get("files_allowed", []) - outside_allowed = [ - path - for path in platform_changes - if not any(path_is_within(path, allowed_path) for allowed_path in allowed) - ] - if outside_allowed: - scope_errors.append( - f"{platform} Implementation modified files outside Contract files_allowed: " - + ", ".join(outside_allowed) - ) - - artifact_path = run_dir / "role-artifacts" / f"{task['name']}.json" - artifact = read_json(artifact_path) - previous = { - normalize_repo_path_text(path) for path in task.get("previous_files_changed", []) - } - delta_path = run_dir / "repository-deltas" / f"{task['name']}.json" - prior_delta = read_json(delta_path) if delta_path.exists() else {} - before_values = dict(prior_delta.get("before", {})) - after_values = dict(prior_delta.get("after", {})) - for path in previous: - before_values.setdefault(path, None) - after_values.setdefault(path, after.get(path)) - for path in platform_changes: - before_values.setdefault(path, before.get(path)) - after_values[path] = after.get(path) - cumulative_paths = previous | set(platform_changes) | set(prior_delta.get("changed_files", [])) - net_changes = sorted( - path for path in cumulative_paths if before_values.get(path) != after_values.get(path) - ) - artifact["output"]["files_changed"] = net_changes - current_dispatch = artifact.get("dispatch", {}) - current_snapshot = current_dispatch.get("input_snapshot_sha256") - attempt = { - "input_snapshot": task.get("input_snapshot"), - "input_snapshot_sha256": task.get("input_snapshot_sha256"), - "run_id": ( - current_dispatch.get("run_id") - if current_snapshot == task.get("input_snapshot_sha256") - else "" - ), - "status": ( - artifact.get("status") - if current_snapshot == task.get("input_snapshot_sha256") - else "DISPATCH_ERROR" - ), - "changed_files": platform_changes, - } - delta = { - "version": 2, - "platform": platform, - "changed_files": net_changes, - "before": {path: before_values.get(path) for path in net_changes}, - "after": {path: after_values.get(path) for path in net_changes}, - "attempts": [*prior_delta.get("attempts", []), attempt], - } - write_json(delta_path, delta) - artifact["dispatch"]["repository_delta"] = run_relative_path(delta_path, run_dir) - artifact["dispatch"]["repository_delta_sha256"] = sha256_file(delta_path) - write_json(artifact_path, artifact) - if unexpected: - violation_path = run_dir / "repository-deltas/implementation-scope-violations.json" - write_json( - violation_path, - { - "version": 1, - "changed_files": unexpected, - "before": {path: before.get(path) for path in unexpected}, - "after": {path: after.get(path) for path in unexpected}, - }, - ) - scope_errors.append( - "Implementation modified files outside dispatched platform roots: " - + ", ".join(unexpected) - ) - if scope_errors: - raise ValueError("; ".join(scope_errors)) - - -def build_codex_command( - codex_bin, route, model, reasoning_effort, schema_path, raw_result_path, working_directory -): - raw_result_path.parent.mkdir(parents=True, exist_ok=True) - command = [ - *codex_command_prefix(codex_bin), - "exec", - "-C", - str(working_directory), - "-s", - route["sandbox"], - ] - command.extend( - [ - "--json", - "--output-schema", - str(schema_path), - "--output-last-message", - str(raw_result_path), - "--model", - model, - "-c", - f'model_reasoning_effort="{reasoning_effort}"', - "-", - ] - ) - return command - - -def run_codex_role( - command, - name, - role, - platform, - prompt_path, - artifact_path, - raw_result_path, - log_path, - stderr_path, - model_profile, - model, - reasoning_effort, - codex_version, - working_directory, - working_directory_manifest, - input_snapshot, - input_snapshot_sha256, - expected_repository_state, - timeout_seconds, -): - prompt = prompt_path.read_text(encoding="utf-8") - log_path.parent.mkdir(parents=True, exist_ok=True) - print(f"Dispatching {name} (timeout {timeout_seconds}s); logs: {log_path}", flush=True) - execution_error = None - with log_path.open("w", encoding="utf-8") as stdout_handle, stderr_path.open( - "w", encoding="utf-8" - ) as stderr_handle: - process = subprocess.Popen( - command, - cwd=working_directory, - stdin=subprocess.PIPE, - stdout=stdout_handle, - stderr=stderr_handle, - text=True, - start_new_session=sys.platform != "win32", - creationflags=( - getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) - if sys.platform == "win32" - else 0 - ), - ) - try: - process.communicate(input=prompt, timeout=timeout_seconds) - except subprocess.TimeoutExpired: - terminate_process_tree(process) - process.communicate() - execution_error = ValueError( - f"Codex dispatch for {name} timed out after {timeout_seconds} seconds" - ) - stdout = log_path.read_text(encoding="utf-8") - stderr = stderr_path.read_text(encoding="utf-8") - if execution_error is None and process.returncode != 0: - execution_error = ValueError( - f"Codex dispatch failed for {name}: {(stderr or stdout).strip()}" - ) - mutation_error = None - if role in {"contract", "verification"}: - actual_repository_state = repository_state() - if actual_repository_state != expected_repository_state: - violation_path = ( - artifact_path.parent.parent - / "repository-deltas" - / f"{name}-scope-violation.json" - ) - write_json( - violation_path, - { - "version": 1, - "role": role, - "name": name, - "before": expected_repository_state, - "after": actual_repository_state, - }, - ) - mutation_error = ValueError( - f"Codex {role} run {name} modified repository files" - ) - if execution_error or mutation_error: - details = [str(error) for error in [execution_error, mutation_error] if error] - raise ValueError("; ".join(details)) - run_id = extract_run_id(stdout) - if not run_id: - raise ValueError(f"Codex dispatch for {name} did not report a thread or run id") - agent_result = read_json(raw_result_path) - for field in ["status", "evidence", "summary", "output"]: - if field not in agent_result: - raise ValueError(f"Codex result for {name} is missing {field}") - if role == "verification": - validate_verification_host(platform, agent_result, sys.platform) - bind_verification_command_evidence( - agent_result["output"], - stdout, - run_relative_path(log_path, artifact_path.parent.parent), - platform, - working_directory_manifest, - ) - artifact = { - "name": name, - "role": role, - **({"platform": platform} if platform else {}), - "agent_id": f"{name}-{run_id}", - "dispatch": { - "mode": "codex-exec", - "run_id": run_id, - "prompt": f"role-prompts/{name}.md", - "prompt_sha256": sha256_text(prompt), - "artifact": f"role-artifacts/{name}.json", - "evidence": f"{codex_version} completed {name}; log: dispatch-logs/{name}.jsonl", - "command_log": run_relative_path(log_path, artifact_path.parent.parent), - "command_log_sha256": sha256_file(log_path), - "model_profile": model_profile, - "model": model, - "reasoning_effort": reasoning_effort, - "codex_version": codex_version, - "working_directory": working_directory_manifest, - "host_platform": sys.platform, - "input_snapshot": input_snapshot, - "input_snapshot_sha256": input_snapshot_sha256, - }, - "status": agent_result["status"], - "evidence": agent_result["evidence"], - "summary": agent_result["summary"], - "output": agent_result["output"], - } - if "waiver_reason" in agent_result: - artifact["waiver_reason"] = agent_result["waiver_reason"] - write_json(artifact_path, artifact) - - -def terminate_process_tree(process): - if process.poll() is not None: - return - if sys.platform == "win32": - result = subprocess.run( - ["taskkill", "/F", "/T", "/PID", str(process.pid)], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) - if result.returncode != 0 and process.poll() is None: - process.kill() - return - try: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) - except ProcessLookupError: - return - - -def validate_verification_host(platform, agent_result, host_platform): - output = agent_result.get("output", {}) - has_build_execution = any( - isinstance(command, dict) - and command.get("kind") == "build" - and command.get("result") in {"PASS", "FAIL"} - for command in output.get("commands", []) - ) - has_pass_build = any( - isinstance(command, dict) - and command.get("kind") == "build" - and command.get("result") == "PASS" - for command in output.get("commands", []) - ) - if ( - platform == "windows" - and host_platform != "win32" - and agent_result.get("status") == "WAIVED" - ): - if output.get("build_result") == "PASS" or has_build_execution: - raise ValueError("WAIVED Windows Verification cannot report build execution") - return - if platform == "windows" and host_platform != "win32" and ( - agent_result.get("status") != "BLOCKED" - or output.get("result") != "BLOCKED" - or output.get("build_result") != "BLOCKED" - or has_build_execution - ): - raise ValueError( - "Windows Verification build evidence requires a Windows host; " - f"current host_platform={host_platform}" - ) - if ( - platform in {"ios", "macos"} - and host_platform != "darwin" - and (output.get("build_result") == "PASS" or has_pass_build) - ): - raise ValueError( - f"{platform} Verification build PASS requires a Darwin host; " - f"current host_platform={host_platform}" - ) - - -def bind_verification_command_evidence( - output, jsonl, log_path, platform, working_directory_manifest -): - commands = output.get("commands", []) if isinstance(output, dict) else [] - events = extract_command_events(jsonl) - used = set() - for index, declared in enumerate(commands): - result = declared.get("result") - if result not in {"PASS", "FAIL"}: - continue - if declared.get("kind") == "build" and not is_platform_build_command( - platform, declared.get("command"), working_directory_manifest - ): - raise ValueError( - f"Verification command {index} is not a recognized {platform} build command: " - f"{declared.get('command')}" - ) - declared_text = normalize_command(declared.get("command")) - candidates = [ - event_index - for event_index, event in enumerate(events) - if event_index not in used and normalize_command(event["command"]) == declared_text - ] - if not candidates: - raise ValueError( - f"Verification command {index} has no matching command_execution event: {declared.get('command')}" - ) - match_index = next( - ( - event_index - for event_index in candidates - if events[event_index].get("cwd") - and command_event_cwd_matches( - events[event_index]["cwd"], working_directory_manifest - ) - ), - None, - ) - if match_index is None: - # Codex CLI 0.144.x omits cwd from command_execution events. In that - # format the subprocess cwd and `codex exec -C` target are the bound - # source; recognized build commands separately reject cwd overrides. - match_index = next( - ( - event_index - for event_index in candidates - if not events[event_index].get("cwd") - ), - None, - ) - if match_index is None: - actual_cwds = [events[event_index].get("cwd") for event_index in candidates] - raise ValueError( - f"Verification command {index} cwd does not match dispatched working directory " - f"{working_directory_manifest}: {actual_cwds}" - ) - used.add(match_index) - event = events[match_index] - cwd_source = "command_event" if event.get("cwd") else "dispatch" - actual_result = "PASS" if event["exit_code"] == 0 else "FAIL" - if result != actual_result: - raise ValueError( - f"Verification command {declared.get('command')} declared {result} but exit_code={event['exit_code']}" - ) - declared["evidence"] = ( - f"{log_path}#{event['id']}; exit_code={event['exit_code']}; " - f"cwd={working_directory_manifest}; cwd_source={cwd_source}" - ) - - -def extract_command_events(jsonl): - events = [] - for line in jsonl.splitlines(): - try: - event = json.loads(line) - except json.JSONDecodeError: - continue - item = event.get("item") - if event.get("type") != "item.completed" or not isinstance(item, dict): - continue - if item.get("type") != "command_execution" or not isinstance(item.get("exit_code"), int): - continue - events.append( - { - "id": str(item.get("id") or f"command-{len(events)}"), - "command": item.get("command"), - "exit_code": item["exit_code"], - "cwd": item.get("cwd") or item.get("working_directory"), - } - ) - return events - - -def command_event_cwd_matches(event_cwd, working_directory_manifest): - if not isinstance(event_cwd, str) or not event_cwd.strip(): - return False - expected = (REPO_ROOT / working_directory_manifest).resolve() - actual = Path(event_cwd) - if not actual.is_absolute(): - actual = REPO_ROOT / actual - return actual.resolve() == expected - - -def extract_run_id(jsonl): - for line in jsonl.splitlines(): - try: - event = json.loads(line) - except json.JSONDecodeError: - continue - for field in ["thread_id", "run_id", "session_id"]: - if event.get(field): - return str(event[field]) - thread = event.get("thread") - if isinstance(thread, dict) and thread.get("id"): - return str(thread["id"]) - return "" - - -def assemble_workspace(args): - run_dir = Path(args.run_dir).resolve() - scope_violations = sorted( - (run_dir / "repository-deltas").glob("*scope-violation*.json") - ) - if scope_violations: - raise ValueError( - "repository scope violation is unresolved; restore the repository and re-run init" - ) - package = read_json(run_dir / "execution-package.json") - profile_path = resolve_bound_repository_profile(package) - matrix_path = Path(args.matrix).resolve() - if package.get("matrix_path") != stable_matrix_path(matrix_path): - raise ValueError("matrix path changed since init; re-run init") - manifest = read_json(run_dir / "acceptance-manifest.json") - if manifest.get("requirement") != package.get("requirement"): - raise ValueError("requirement changed since init; re-run init") - artifacts = load_role_artifacts(run_dir / "role-artifacts") - manifest["final_status"] = args.final_status - manifest["contract"] = strip_artifact_metadata(artifacts["contract"]) - for platform in PLATFORMS: - manifest["platforms"][platform] = { - role: strip_artifact_metadata(artifacts[artifact_name(role, platform)]) - for role in ["implementation", "verification"] - } - manifest["cross_platform_acceptance"] = { - "result": args.cross_platform_result, - "evidence": args.cross_platform_evidence, - "differences": args.cross_platform_difference, - } - manifest["release"]["repository_profile"] = package["repository_profile"] - manifest["release"]["repository_profile_sha256"] = package[ - "repository_profile_sha256" - ] - manifest["release"]["checks"] = collect_sdk_version_checks( - manifest["requirement"]["target_sdk_versions"], profile_path=profile_path - ) - errors = validate_manifest(manifest) - errors.extend(validate_evidence_files(manifest, run_dir)) - if errors: - for error in errors: - print(f"ERROR: {error}", file=sys.stderr) - return 1 - updates = [] - for platform in PLATFORMS: - updates.extend(manifest["platforms"][platform]["implementation"]["output"]["matrix_updates"]) - if updates and manifest["final_status"] != "BLOCKED": - update_matrix(matrix_path, updates) - output_path = run_dir / "final-acceptance-manifest.json" - write_json(output_path, manifest) - print(f"Acceptance manifest valid: {output_path}") - return 0 - -def load_role_artifacts(artifact_dir): - artifacts = {} - for artifact_path in sorted(artifact_dir.glob("*.json")): - artifact = read_json(artifact_path) - if not isinstance(artifact, dict): - raise ValueError(f"role artifact must be an object: {artifact_path.name}") - name = artifact.get("name") or artifact_path.stem - attach_dispatch_hashes(artifact, name, artifact_path, artifact_dir.parent) - artifacts[name] = artifact - expected = {"contract"} - expected.update( - artifact_name(role, platform) - for platform in PLATFORMS - for role in ["implementation", "verification"] - ) - missing = expected - set(artifacts) - if missing: - raise ValueError(f"missing role artifacts: {', '.join(sorted(missing))}") - return artifacts - - -def attach_dispatch_hashes(artifact, name, artifact_path, run_dir): - dispatch = artifact.setdefault("dispatch", {}) - dispatch.setdefault("artifact", f"role-artifacts/{name}.json") - dispatch["artifact_sha256"] = sha256_file(artifact_path) - prompt_path = resolve_run_path(run_dir, dispatch.get("prompt", f"role-prompts/{name}.md")) - if prompt_path.exists(): - actual_sha256 = sha256_file(prompt_path) - expected_sha256 = dispatch.get("prompt_sha256") - if expected_sha256 and expected_sha256 != actual_sha256: - raise ValueError(f"prompt sha256 mismatch for {name}: {dispatch.get('prompt')}") - dispatch["prompt_sha256"] = actual_sha256 - snapshot_value = dispatch.get("input_snapshot") - if snapshot_value: - snapshot_path = resolve_run_path(run_dir, snapshot_value) - if not snapshot_path.exists(): - raise ValueError(f"input snapshot does not exist for {name}: {snapshot_value}") - actual_sha256 = sha256_file(snapshot_path) - if dispatch.get("input_snapshot_sha256") != actual_sha256: - raise ValueError(f"input snapshot sha256 mismatch for {name}: {snapshot_value}") - snapshot = read_json(snapshot_path) - if not isinstance(snapshot, dict): - raise ValueError(f"input snapshot must be an object for {name}") - package_path = run_dir / "execution-package.json" - if not package_path.is_file(): - raise ValueError(f"execution package does not exist for {name}") - if snapshot.get("execution_package_sha256") != sha256_file(package_path): - raise ValueError(f"execution package sha256 mismatch for {name}") - dependency_hashes = snapshot.get("dependency_artifact_sha256", {}) - if not isinstance(dependency_hashes, dict): - raise ValueError(f"input snapshot dependencies must be an object for {name}") - for dependency, expected_sha256 in dependency_hashes.items(): - if Path(dependency).name != dependency: - raise ValueError(f"invalid dependency artifact name for {name}: {dependency}") - dependency_path = run_dir / "role-artifacts" / f"{dependency}.json" - if not dependency_path.is_file(): - raise ValueError(f"dependency artifact does not exist for {name}: {dependency}") - if sha256_file(dependency_path) != expected_sha256: - raise ValueError( - f"dependency artifact sha256 mismatch for {name}: {dependency}" - ) - command_log_value = dispatch.get("command_log") - if command_log_value: - command_log_path = resolve_run_path(run_dir, command_log_value) - if not command_log_path.exists(): - raise ValueError(f"command log does not exist for {name}: {command_log_value}") - actual_sha256 = sha256_file(command_log_path) - if dispatch.get("command_log_sha256") != actual_sha256: - raise ValueError(f"command log sha256 mismatch for {name}: {command_log_value}") - repository_delta_value = dispatch.get("repository_delta") - if repository_delta_value: - repository_delta_path = resolve_run_path(run_dir, repository_delta_value) - if not repository_delta_path.exists(): - raise ValueError( - f"repository delta does not exist for {name}: {repository_delta_value}" - ) - actual_sha256 = sha256_file(repository_delta_path) - if dispatch.get("repository_delta_sha256") != actual_sha256: - raise ValueError( - f"repository delta sha256 mismatch for {name}: {repository_delta_value}" - ) - delta = read_json(repository_delta_path) - if not isinstance(delta, dict): - raise ValueError(f"repository delta must be an object for {name}") - delta_files = { - normalize_repo_path_text(path) for path in delta.get("changed_files", []) - } - artifact_files = { - normalize_repo_path_text(path) - for path in artifact.get("output", {}).get("files_changed", []) - } - if delta_files != artifact_files: - raise ValueError(f"repository delta changed_files mismatch for {name}") - - -def strip_artifact_metadata(artifact): - return { - key: value - for key, value in artifact.items() - if key not in {"name", "role", "platform"} - } - - -def resolve_run_path(run_dir, path_text): - path = Path(path_text) - if path.is_absolute() or ".." in path.parts: - raise ValueError(f"execution artifact path must be relative to run directory: {path_text}") - return run_dir / path - - -def update_matrix(matrix_path, updates): - lines = matrix_path.read_text(encoding="utf-8").splitlines() - for update in updates: - lines = apply_matrix_update(lines, update) - matrix_path.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def apply_matrix_update(lines, update): - for index, line in enumerate(lines): - if not line.startswith("|"): - continue - header = split_markdown_row(line) - if "Feature" not in header or update["platform_unit"] not in header: - continue - feature_index = header.index("Feature") - platform_index = header.index(update["platform_unit"]) - for row_index in range(index + 2, len(lines)): - row_line = lines[row_index] - if not row_line.startswith("|"): - break - row = split_markdown_row(row_line) - if len(row) != len(header) or row[feature_index] != update["feature"]: - continue - status, _ = parse_matrix_cell(row[platform_index]) - if status != update["from"]: - raise ValueError( - f"matrix cell {update['feature']} / {update['platform_unit']} is {status}, expected {update['from']}" - ) - row[platform_index] = f"`{matrix_to_cell(update)}`" - lines[row_index] = "| " + " | ".join(row) + " |" - return lines - raise ValueError(f"matrix row not found for {update['feature']} / {update['platform_unit']}") - - -def matrix_to_cell(update): - cell = str(update.get("to_cell") or "").strip() - if cell.startswith("`") and cell.endswith("`"): - cell = cell[1:-1] - status, _ = parse_matrix_cell(cell) - if status != update.get("to") or any(character in cell for character in "\n\r|`"): - raise ValueError( - f"matrix to_cell must encode to={update.get('to')}: {update.get('to_cell')}" - ) - return cell - - -def artifact_name(role, platform): - return role if platform is None else f"{platform}-{role}" - - -def repo_relative_path(path): - try: - return path.resolve().relative_to(REPO_ROOT).as_posix() - except ValueError as exc: - raise ValueError(f"path must be inside the repository: {path}") from exc - - -def stable_matrix_path(path): - resolved = Path(path).resolve() - try: - return resolved.relative_to(REPO_ROOT).as_posix() - except ValueError: - return str(resolved) - - -def run_relative_path(path, run_dir): - return path.resolve().relative_to(run_dir.resolve()).as_posix() - - -def sha256_file(path): - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def sha256_text(value): - return hashlib.sha256(value.encode("utf-8")).hexdigest() - - -def sha256_bytes(value): - return hashlib.sha256(value).hexdigest() - - -def main(argv=None): - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - init_parser = subparsers.add_parser("init", help="Create a requirement workspace") - init_parser.add_argument("--matrix", required=True) - init_parser.add_argument("--feature", help="Requirement feature; defaults to highest-priority gap") - init_parser.add_argument("--sdk-family", help="Required for a feature outside the matrix backlog") - init_parser.add_argument("--key-api", action="append", help="Key SDK API; repeat as needed") - init_parser.add_argument("--target-sdk-version", required=True) - init_parser.add_argument( - "--platform-sdk-version", - action="append", - default=[], - help="Override one platform as platform=x.y.z; repeat as needed", - ) - init_parser.add_argument("--run-dir", required=True) - init_parser.add_argument("--index", type=int, default=0) - init_parser.add_argument("--routing-config", default=str(DEFAULT_ROUTING_CONFIG)) - init_parser.add_argument("--repository-profile", default=str(DEFAULT_REPOSITORY_PROFILE)) - - dispatch_parser = subparsers.add_parser("dispatch", help="Dispatch one requirement phase") - dispatch_parser.add_argument("--run-dir", required=True) - dispatch_parser.add_argument("--phase", required=True, choices=PHASE_ORDER) - dispatch_parser.add_argument("--platform", action="append", choices=PLATFORMS) - dispatch_parser.add_argument("--model") - dispatch_parser.add_argument("--routing-config", default=str(DEFAULT_ROUTING_CONFIG)) - dispatch_parser.add_argument("--codex-bin", default="codex") - dispatch_parser.add_argument("--timeout-seconds", type=int, default=900) - dispatch_parser.add_argument("--retry", action="store_true") - dispatch_parser.add_argument("--dry-run", action="store_true") - - assemble_parser = subparsers.add_parser("assemble", help="Assemble the v4 manifest") - assemble_parser.add_argument("--run-dir", required=True) - assemble_parser.add_argument("--matrix", required=True) - assemble_parser.add_argument( - "--final-status", choices=["PASS", "PASS WITH RISKS", "BLOCKED"], default="BLOCKED" - ) - assemble_parser.add_argument( - "--cross-platform-result", choices=RESULT_VALUES, default="BLOCKED" - ) - assemble_parser.add_argument( - "--cross-platform-evidence", default="Pending required platform verification." - ) - assemble_parser.add_argument("--cross-platform-difference", action="append", default=[]) - args = parser.parse_args(argv) - try: - if getattr(args, "timeout_seconds", 1) <= 0: - raise ValueError("timeout-seconds must be positive") - if args.command == "init": - return init_workspace(args) - if args.command == "dispatch": - return dispatch_workspace(args) - if args.command == "assemble": - return assemble_workspace(args) - except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc: - print(f"ERROR: {exc}", file=sys.stderr) - return 1 - return 2 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/ai-engineering/tools/orchestrate_case_execution_test.py b/docs/ai-engineering/tools/orchestrate_case_execution_test.py deleted file mode 100644 index 3cbcdc9ae..000000000 --- a/docs/ai-engineering/tools/orchestrate_case_execution_test.py +++ /dev/null @@ -1,1358 +0,0 @@ -import json -import subprocess -import sys -import tempfile -import textwrap -import time -import unittest -from pathlib import Path -from unittest import mock - - -REPO_ROOT = Path(__file__).resolve().parents[3] -ORCHESTRATOR = REPO_ROOT / "docs/ai-engineering/tools/orchestrate_case_execution.py" -VALIDATOR = REPO_ROOT / "docs/ai-engineering/tools/validate_acceptance_manifest.py" -PLATFORMS = ["android", "ios", "macos", "windows"] -TOOLS_DIR = REPO_ROOT / "docs/ai-engineering/tools" -if str(TOOLS_DIR) not in sys.path: - sys.path.insert(0, str(TOOLS_DIR)) - -import orchestrate_case_execution as orchestrator -from orchestrate_case_execution import ( - bind_verification_command_evidence, - collect_repository_fingerprints, - run_codex_role, - validate_verification_host, -) - - -class RequirementOrchestratorTest(unittest.TestCase): - TARGET_SDK_VERSION = "4.6.4" - - def write_matrix(self): - handle = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) - handle.write( - textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | - | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(app/JoinChannelAudio.java)` | `DONE(APIExample/JoinChannelAudio.swift)` | `PARTIAL(APIExample/JoinChannelAudio.swift)` | `MISSING` | Keep official platforms aligned. | - - ## Confirmed Gaps - - | Gap | Affected Units | Severity | - | --- | --- | --- | - | Basic audio-only join channel | Windows | High - missing foundational case | - """ - ) - ) - handle.close() - self.addCleanup(lambda: Path(handle.name).unlink(missing_ok=True)) - return Path(handle.name) - - def run_orchestrator(self, *args): - return subprocess.run( - [sys.executable, str(ORCHESTRATOR), *args], - cwd=REPO_ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) - - def init_workspace(self, matrix_path, run_dir, platform_sdk_versions=None): - version_args = [] - for value in platform_sdk_versions or []: - version_args.extend(["--platform-sdk-version", value]) - result = self.run_orchestrator( - "init", - "--matrix", - str(matrix_path), - "--feature", - "Join channel audio", - "--target-sdk-version", - self.TARGET_SDK_VERSION, - *version_args, - "--run-dir", - str(run_dir), - ) - self.assertEqual(result.returncode, 0, result.stderr) - - def write_fake_codex( - self, directory, implementation_sleep=0, fail_implementation_platform=None - ): - path = Path(directory) / "fake-codex.py" - path.write_text( - textwrap.dedent( - f"""\ - #!/usr/bin/env python3 - import json - import sys - import time - from pathlib import Path - - if sys.argv[1:] == ["--version"]: - print("codex-cli 0.test") - raise SystemExit(0) - - args = sys.argv[1:] - output_path = Path(args[args.index("--output-last-message") + 1]) - name = output_path.stem - fail_name = {json.dumps(f'{fail_implementation_platform}-implementation' if fail_implementation_platform else '')} - if name == fail_name: - print(json.dumps({{"type": "thread.started", "thread_id": f"run-{{name}}"}}), flush=True) - raise SystemExit(7) - targets = {{ - "android": "Android/APIExample/", - "ios": "iOS/APIExample/", - "macos": "macOS/", - "windows": "windows/", - }} - build_commands = {{ - "android": "./gradlew assembleDebug", - "ios": "xcodebuild -scheme APIExample build", - "macos": "xcodebuild -scheme APIExample build", - "windows": "msbuild APIExample.sln /t:Build", - }} - status = "PASS" - if name == "contract": - output = {{ - "scenario": "Keep Join channel audio aligned across official platforms.", - "key_apis": ["joinChannel", "setAudioProfile"], - "non_goals": ["Release packaging"], - "reference": {{ - "required": True, - "source_case": "Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java", - "contract_result": "PASS", - }}, - "cross_platform_requirements": ["Equivalent join and leave behavior"], - "platform_targets": {{ - platform: {{ - "required": True, - "target_project": target, - "key_constraints": [f"Keep changes in {{target}}"], - "files_allowed": [target], - "waiver_reason": "", - }} - for platform, target in targets.items() - }}, - }} - else: - platform, role = name.rsplit("-", 1) - if role == "implementation": - time.sleep({implementation_sleep}) - output = {{ - "target_project": targets[platform], - "query_cases": "No duplicate case found.", - "upsert_case": "Platform case updated.", - "files_changed": [f"{{targets[platform]}}AGENTS.md"], - "matrix_updates": [], - }} - else: - host_mismatch = ( - (platform == "windows" and sys.platform != "win32") - or (platform in {{"ios", "macos"}} and sys.platform != "darwin") - ) - if host_mismatch: - status = "BLOCKED" - output = {{ - "result": "BLOCKED", - "findings": [f"{{platform}} build requires its native host."], - "parity_result": "PASS", - "entry_point": "Basic > Join channel audio", - "ux_notes": "Static review only on this host.", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [{{ - "name": f"{{platform}} native build", - "reason": "Current host cannot provide native build evidence.", - }}], - }} - else: - output = {{ - "result": "PASS", - "findings": ["No blocking findings."], - "parity_result": "PASS", - "entry_point": "Basic > Join channel audio", - "ux_notes": "Matches adjacent examples.", - "commands": [{{ - "kind": "build", - "command": build_commands[platform], - "result": "PASS", - }}], - "build_result": "PASS", - "skipped_checks": [], - }} - result = {{ - "status": status, - "evidence": f"{{name}} evidence", - "summary": f"{{name}} completed", - "output": output, - }} - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(json.dumps(result), encoding="utf-8") - print(json.dumps({{"type": "thread.started", "thread_id": f"run-{{name}}"}}), flush=True) - if name != "contract" and name.endswith("-verification") and output["commands"]: - print(json.dumps({{ - "type": "item.completed", - "item": {{ - "id": f"build-{{platform}}", - "type": "command_execution", - "command": build_commands[platform], - "status": "completed", - "exit_code": 0, - "cwd": str(Path.cwd()), - }}, - }}), flush=True) - """ - ), - encoding="utf-8", - ) - path.chmod(0o755) - return path - - def dispatch_contract(self, run_dir, codex_bin): - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "contract", - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - - def test_init_creates_shared_contract_and_eight_platform_role_tasks(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - - manifest = json.loads((run_dir / "acceptance-manifest.json").read_text()) - package = json.loads((run_dir / "execution-package.json").read_text()) - artifact_names = sorted(path.stem for path in (run_dir / "role-artifacts").glob("*.json")) - expected = ["contract"] + [ - f"{platform}-{role}" - for platform in PLATFORMS - for role in ["implementation", "verification"] - ] - self.assertEqual(artifact_names, sorted(expected)) - self.assertEqual(sorted(manifest["platforms"]), PLATFORMS) - self.assertNotIn("roles", manifest) - self.assertEqual( - package["repository_profile"], - "docs/ai-engineering/repository-profile.json", - ) - self.assertEqual(len(package["repository_profile_sha256"]), 64) - self.assertEqual( - manifest["release"]["repository_profile"], - package["repository_profile"], - ) - self.assertEqual( - manifest["release"]["repository_profile_sha256"], - package["repository_profile_sha256"], - ) - - def test_execution_configuration_rejects_repository_profile_drift(self): - routing_path = REPO_ROOT / "docs/ai-engineering/role-routing.json" - package = { - "routing_config": "docs/ai-engineering/role-routing.json", - "routing_config_sha256": orchestrator.sha256_file(routing_path), - "repository_profile": "docs/ai-engineering/repository-profile.json", - "repository_profile_sha256": "0" * 64, - } - - with self.assertRaisesRegex(ValueError, "repository profile content changed"): - orchestrator.validate_execution_configuration(package, routing_path) - - def test_contract_input_snapshot_binds_repository_profile(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - - package = json.loads((run_dir / "execution-package.json").read_text()) - artifact = json.loads((run_dir / "role-artifacts/contract.json").read_text()) - snapshot_path = run_dir / artifact["dispatch"]["input_snapshot"] - snapshot = json.loads(snapshot_path.read_text()) - - self.assertEqual(snapshot["repository_profile"], package["repository_profile"]) - self.assertEqual( - snapshot["repository_profile_sha256"], - package["repository_profile_sha256"], - ) - - def test_init_accepts_new_requirement_outside_the_matrix_backlog(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - result = self.run_orchestrator( - "init", - "--matrix", - str(matrix_path), - "--feature", - "Spatial audio", - "--sdk-family", - "Full RTC", - "--key-api", - "enableSpatialAudio", - "--target-sdk-version", - self.TARGET_SDK_VERSION, - "--run-dir", - str(run_dir), - ) - - self.assertEqual(result.returncode, 0, result.stderr) - package = json.loads((run_dir / "execution-package.json").read_text()) - self.assertEqual(package["requirement"]["feature"], "Spatial audio") - self.assertEqual(package["requirement"]["key_apis"], ["enableSpatialAudio"]) - contract_prompt = (run_dir / "role-prompts/contract.md").read_text() - self.assertNotIn("If Contract marks this platform required=false", contract_prompt) - - def test_contract_prompt_includes_platform_units_and_prepared_reference(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - - package = json.loads((run_dir / "execution-package.json").read_text()) - contract_prompt = (run_dir / "role-prompts/contract.md").read_text() - - self.assertIn("Selected platform units:", contract_prompt) - self.assertIn( - json.dumps(package["platform_units"], indent=2, ensure_ascii=False), - contract_prompt, - ) - self.assertIn("Prepared reference contract:", contract_prompt) - self.assertIn( - json.dumps(package["reference_contract"], indent=2, ensure_ascii=False), - contract_prompt, - ) - - def test_init_persists_and_prompts_staggered_platform_sdk_versions(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - result = self.run_orchestrator( - "init", - "--matrix", - str(matrix_path), - "--feature", - "Join channel audio", - "--target-sdk-version", - "4.6.2", - "--platform-sdk-version", - "android=4.6.3", - "--run-dir", - str(run_dir), - ) - - self.assertEqual(result.returncode, 0, result.stderr) - package = json.loads((run_dir / "execution-package.json").read_text()) - expected = { - "android": "4.6.3", - "ios": "4.6.2", - "macos": "4.6.2", - "windows": "4.6.2", - } - self.assertEqual(package["requirement"]["target_sdk_versions"], expected) - contract_prompt = (run_dir / "role-prompts/contract.md").read_text() - self.assertIn('"android": "4.6.3"', contract_prompt) - self.assertIn('"windows": "4.6.2"', contract_prompt) - - def test_repository_matrix_identity_is_portable(self): - identity = orchestrator.stable_matrix_path( - REPO_ROOT / "docs/ai-engineering/case-maintenance-matrix.md" - ) - - self.assertEqual(identity, "docs/ai-engineering/case-maintenance-matrix.md") - - def test_platform_implementation_waits_for_shared_contract(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--platform", - "android", - "--model", - "test-model", - "--dry-run", - ) - - self.assertEqual(result.returncode, 1) - self.assertIn("Contract gate returned status BLOCKED", result.stderr) - - def test_contract_semantic_failure_is_blocked_before_implementation(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - artifact_path = run_dir / "role-artifacts/contract.json" - artifact = json.loads(artifact_path.read_text()) - artifact["status"] = "PASS" - artifact["output"]["reference"]["contract_result"] = "PASS" - artifact["output"]["cross_platform_requirements"] = [] - artifact_path.write_text(json.dumps(artifact), encoding="utf-8") - - with self.assertRaisesRegex(ValueError, "Contract semantic gate failed"): - orchestrator.validate_completed_contract(run_dir) - - blocked = json.loads(artifact_path.read_text()) - self.assertEqual(blocked["status"], "BLOCKED") - self.assertIn("cross_platform_requirements", blocked["evidence"]) - - def test_implementation_phase_dispatches_four_platform_agents_with_scoped_deltas(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - for platform in PLATFORMS: - artifact = json.loads( - (run_dir / f"role-artifacts/{platform}-implementation.json").read_text() - ) - self.assertEqual(artifact["dispatch"]["mode"], "codex-exec") - self.assertEqual(artifact["dispatch"]["run_id"], f"run-{platform}-implementation") - self.assertEqual(artifact["output"]["files_changed"], []) - - def test_implementation_reconciles_each_agent_before_starting_the_next(self): - tasks = [ - {"name": "android-implementation", "platform": "android"}, - {"name": "ios-implementation", "platform": "ios"}, - ] - events = [] - - with mock.patch.object( - orchestrator, - "refresh_implementation_input_snapshot", - side_effect=lambda run_dir, task: events.append(f"snapshot:{task['name']}"), - ), mock.patch.object( - orchestrator, - "collect_repository_fingerprints", - side_effect=[{"state": "android"}, {"state": "ios"}], - ), mock.patch.object( - orchestrator, - "run_codex_role", - side_effect=lambda **kwargs: events.append(f"run:{kwargs['name']}"), - ), mock.patch.object( - orchestrator, - "reconcile_implementation_changes", - side_effect=lambda run_dir, task_group, before: events.append( - f"reconcile:{task_group[0]['name']}" - ), - ), mock.patch.object( - orchestrator, - "validate_completed_implementation", - side_effect=lambda run_dir, platform: events.append(f"validate:{platform}"), - ): - orchestrator.run_implementation_tasks(Path("/tmp/run"), tasks) - - self.assertEqual( - events, - [ - "snapshot:android-implementation", - "run:android-implementation", - "reconcile:android-implementation", - "validate:android", - "snapshot:ios-implementation", - "run:ios-implementation", - "reconcile:ios-implementation", - "validate:ios", - ], - ) - - def test_implementation_semantic_failure_blocks_verification(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - artifact_path = run_dir / "role-artifacts/android-implementation.json" - artifact = json.loads(artifact_path.read_text()) - artifact["status"] = "PASS" - artifact["output"]["query_cases"] = "" - artifact_path.write_text(json.dumps(artifact), encoding="utf-8") - - with self.assertRaisesRegex(ValueError, "Implementation semantic gate failed"): - orchestrator.validate_completed_implementation(run_dir, "android") - - blocked = json.loads(artifact_path.read_text()) - self.assertEqual(blocked["status"], "BLOCKED") - self.assertIn("query_cases", blocked["evidence"]) - - def test_implementation_refreshes_snapshot_after_previous_platform_delta(self): - task = { - "name": "ios-implementation", - "snapshot_dependencies": {"contract": {}}, - "resolved_routing": {"routing_config": "routing.json"}, - "input_snapshot": "old.json", - "input_snapshot_sha256": "old-sha", - "expected_repository_state": {"head": "old"}, - } - snapshot_path = Path("/tmp/run/input-snapshots/ios-new.json") - - with mock.patch.object( - orchestrator, - "write_input_snapshot", - return_value=(snapshot_path, "new-sha"), - ), mock.patch.object( - orchestrator, - "read_json", - return_value={"repository_state": {"head": "after-android"}}, - ): - orchestrator.refresh_implementation_input_snapshot(Path("/tmp/run"), task) - - self.assertEqual(task["input_snapshot"], "input-snapshots/ios-new.json") - self.assertEqual(task["input_snapshot_sha256"], "new-sha") - self.assertEqual(task["expected_repository_state"], {"head": "after-android"}) - - def test_repository_fingerprints_include_untracked_file_contents(self): - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - subprocess.run(["git", "init", "-q"], cwd=root, check=True) - path = root / "new-example.swift" - path.write_text("first\n", encoding="utf-8") - before = collect_repository_fingerprints(root) - - path.write_text("second\n", encoding="utf-8") - after = collect_repository_fingerprints(root) - - self.assertNotEqual(before["new-example.swift"], after["new-example.swift"]) - - def test_artifact_loading_rejects_stale_dependency_snapshot(self): - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - artifact_dir = run_dir / "role-artifacts" - artifact_dir.mkdir(parents=True) - package_path = run_dir / "execution-package.json" - package_path.write_text('{"requirement":{}}\n', encoding="utf-8") - contract_path = artifact_dir / "contract.json" - contract_path.write_text('{"status":"PASS"}\n', encoding="utf-8") - snapshot_path = run_dir / "input-snapshots/android-verification.json" - snapshot_path.parent.mkdir(parents=True) - snapshot_path.write_text( - json.dumps( - { - "execution_package_sha256": orchestrator.sha256_file(package_path), - "dependency_artifact_sha256": {"contract": "0" * 64}, - } - ), - encoding="utf-8", - ) - artifact_path = artifact_dir / "android-verification.json" - artifact = { - "dispatch": { - "mode": "codex-exec", - "input_snapshot": "input-snapshots/android-verification.json", - "input_snapshot_sha256": orchestrator.sha256_file(snapshot_path), - } - } - artifact_path.write_text(json.dumps(artifact), encoding="utf-8") - - with self.assertRaisesRegex(ValueError, "dependency artifact sha256 mismatch"): - orchestrator.attach_dispatch_hashes( - artifact, - "android-verification", - artifact_path, - run_dir, - ) - - def test_implementation_retry_delta_covers_all_attempt_changes(self): - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - contract_path = run_dir / "role-artifacts/contract.json" - contract_path.parent.mkdir(parents=True) - contract_path.write_text( - json.dumps( - { - "output": { - "platform_targets": { - "android": {"files_allowed": ["Android/APIExample/"]} - } - } - } - ), - encoding="utf-8", - ) - artifact_path = run_dir / "role-artifacts/android-implementation.json" - artifact = { - "status": "BLOCKED", - "dispatch": { - "mode": "codex-exec", - "run_id": "run-1", - "input_snapshot_sha256": "snapshot-1", - }, - "output": {"files_changed": []}, - } - artifact_path.write_text(json.dumps(artifact), encoding="utf-8") - first_before = {"Android/APIExample/A.java": {"sha256": "before-a"}} - first_after = {"Android/APIExample/A.java": {"sha256": "after-a"}} - first_task = { - "name": "android-implementation", - "platform": "android", - "input_snapshot": "input-snapshots/android-1.json", - "input_snapshot_sha256": "snapshot-1", - "previous_files_changed": [], - } - with mock.patch.object( - orchestrator, "collect_repository_fingerprints", return_value=first_after - ): - orchestrator.reconcile_implementation_changes( - run_dir, [first_task], first_before - ) - - artifact = json.loads(artifact_path.read_text()) - artifact["status"] = "PASS" - artifact["dispatch"]["run_id"] = "run-2" - artifact["dispatch"]["input_snapshot_sha256"] = "snapshot-2" - artifact_path.write_text(json.dumps(artifact), encoding="utf-8") - second_after = { - **first_after, - "Android/APIExample/B.java": {"sha256": "after-b"}, - } - second_task = { - "name": "android-implementation", - "platform": "android", - "input_snapshot": "input-snapshots/android-2.json", - "input_snapshot_sha256": "snapshot-2", - "previous_files_changed": ["Android/APIExample/A.java"], - } - with mock.patch.object( - orchestrator, "collect_repository_fingerprints", return_value=second_after - ): - orchestrator.reconcile_implementation_changes( - run_dir, [second_task], first_after - ) - - artifact = json.loads(artifact_path.read_text()) - delta = json.loads( - (run_dir / "repository-deltas/android-implementation.json").read_text() - ) - expected = ["Android/APIExample/A.java", "Android/APIExample/B.java"] - self.assertEqual(artifact["output"]["files_changed"], expected) - self.assertEqual(delta["changed_files"], expected) - self.assertEqual([attempt["run_id"] for attempt in delta["attempts"]], ["run-1", "run-2"]) - - def test_failed_implementation_still_reconciles_successful_artifacts(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex( - tmpdir, fail_implementation_platform="windows" - ) - self.dispatch_contract(run_dir, codex_bin) - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - - self.assertEqual(result.returncode, 1) - android = json.loads( - (run_dir / "role-artifacts/android-implementation.json").read_text() - ) - self.assertEqual(android["output"]["files_changed"], []) - self.assertIn("repository_delta", android["dispatch"]) - - def test_failed_verification_mutation_creates_persistent_scope_violation(self): - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - prompt_path = run_dir / "role-prompts/ios-verification.md" - prompt_path.parent.mkdir(parents=True) - prompt_path.write_text("verify", encoding="utf-8") - artifact_path = run_dir / "role-artifacts/ios-verification.json" - - with mock.patch( - "orchestrate_case_execution.repository_state", - return_value={"state": "after"}, - ): - with self.assertRaisesRegex(ValueError, "modified repository files"): - run_codex_role( - command=[sys.executable, "-c", "raise SystemExit(7)"], - name="ios-verification", - role="verification", - platform="ios", - prompt_path=prompt_path, - artifact_path=artifact_path, - raw_result_path=run_dir / "raw-results/ios-verification.json", - log_path=run_dir / "dispatch-logs/ios-verification.jsonl", - stderr_path=run_dir / "dispatch-logs/ios-verification.stderr.log", - model_profile="review", - model="test-model", - reasoning_effort="high", - codex_version="codex-cli test", - working_directory=Path(tmpdir), - working_directory_manifest="iOS/APIExample/", - input_snapshot="input-snapshots/ios-verification.json", - input_snapshot_sha256="a" * 64, - expected_repository_state={"state": "before"}, - timeout_seconds=5, - ) - - self.assertTrue( - (run_dir / "repository-deltas/ios-verification-scope-violation.json").exists() - ) - - def test_macos_rejects_windows_build_pass(self): - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - prompt_path = run_dir / "role-prompts/windows-verification.md" - prompt_path.parent.mkdir(parents=True) - prompt_path.write_text("verify", encoding="utf-8") - raw_result_path = run_dir / "raw-results/windows-verification.json" - raw_result_path.parent.mkdir(parents=True) - raw_result_path.write_text( - json.dumps( - { - "status": "PASS", - "evidence": "MSBuild passed.", - "summary": "Windows verified.", - "output": { - "result": "PASS", - "findings": [], - "parity_result": "PASS", - "entry_point": "Basic > Join channel audio", - "ux_notes": "Matches adjacent examples.", - "commands": [ - { - "kind": "build", - "command": "msbuild APIExample.sln /t:Build", - "result": "PASS", - } - ], - "build_result": "PASS", - "skipped_checks": [], - }, - } - ), - encoding="utf-8", - ) - command = [ - sys.executable, - "-c", - ( - "import json; " - "print(json.dumps({'type':'thread.started','thread_id':'run-windows'})); " - "print(json.dumps({'type':'item.completed','item':" - "{'id':'build','type':'command_execution','command':" - "'msbuild APIExample.sln /t:Build','exit_code':0}}))" - ), - ] - - with mock.patch.object(orchestrator.sys, "platform", "darwin"), mock.patch.object( - orchestrator, - "repository_state", - return_value={"state": "clean"}, - ): - with self.assertRaisesRegex(ValueError, "requires a Windows host"): - run_codex_role( - command=command, - name="windows-verification", - role="verification", - platform="windows", - prompt_path=prompt_path, - artifact_path=run_dir / "role-artifacts/windows-verification.json", - raw_result_path=raw_result_path, - log_path=run_dir / "dispatch-logs/windows-verification.jsonl", - stderr_path=run_dir / "dispatch-logs/windows-verification.stderr.log", - model_profile="review", - model="test-model", - reasoning_effort="high", - codex_version="codex-cli test", - working_directory=Path(tmpdir), - working_directory_manifest="windows/", - input_snapshot="input-snapshots/windows-verification.json", - input_snapshot_sha256="a" * 64, - expected_repository_state={"state": "clean"}, - timeout_seconds=5, - ) - - def test_non_darwin_host_rejects_apple_build_pass(self): - result = { - "status": "PASS", - "output": { - "result": "PASS", - "commands": [ - { - "kind": "build", - "command": "xcodebuild -scheme APIExample build", - "result": "PASS", - } - ], - "build_result": "PASS", - }, - } - - for platform in ["ios", "macos"]: - with self.subTest(platform=platform): - with self.assertRaisesRegex(ValueError, "requires a Darwin host"): - validate_verification_host(platform, result, "linux") - - def test_command_evidence_requires_dispatched_working_directory(self): - output = { - "commands": [ - { - "kind": "build", - "command": "xcodebuild -scheme APIExample build", - "result": "PASS", - } - ] - } - wrong_cwd_event = json.dumps( - { - "type": "item.completed", - "item": { - "id": "build", - "type": "command_execution", - "command": "xcodebuild -scheme APIExample build", - "exit_code": 0, - "cwd": str(REPO_ROOT / "iOS/APIExample-Audio"), - }, - } - ) - - with self.assertRaisesRegex(ValueError, "cwd does not match"): - bind_verification_command_evidence( - output, - wrong_cwd_event, - "dispatch-logs/ios-verification.jsonl", - "ios", - "iOS/APIExample/", - ) - - def test_command_evidence_uses_dispatch_cwd_when_event_omits_it(self): - output = { - "commands": [ - { - "kind": "build", - "command": "xcodebuild -scheme APIExample build", - "result": "PASS", - } - ] - } - missing_cwd_event = json.dumps( - { - "type": "item.completed", - "item": { - "id": "build", - "type": "command_execution", - "command": "/bin/zsh -lc 'xcodebuild -scheme APIExample build'", - "exit_code": 0, - }, - } - ) - - bind_verification_command_evidence( - output, - missing_cwd_event, - "dispatch-logs/ios-verification.jsonl", - "ios", - "iOS/APIExample/", - ) - - self.assertEqual( - output["commands"][0]["evidence"], - "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " - "cwd=iOS/APIExample/; cwd_source=dispatch", - ) - - def test_command_evidence_preserves_verified_cwd_binding(self): - output = { - "commands": [ - { - "kind": "build", - "command": "xcodebuild -scheme APIExample build", - "result": "PASS", - } - ] - } - event = json.dumps( - { - "type": "item.completed", - "item": { - "id": "build", - "type": "command_execution", - "command": "xcodebuild -scheme APIExample build", - "exit_code": 0, - "cwd": str(REPO_ROOT / "iOS/APIExample"), - }, - } - ) - - bind_verification_command_evidence( - output, - event, - "dispatch-logs/ios-verification.jsonl", - "ios", - "iOS/APIExample/", - ) - - self.assertEqual( - output["commands"][0]["evidence"], - "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " - "cwd=iOS/APIExample/; cwd_source=command_event", - ) - - def test_verification_semantic_failure_becomes_retryable(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - artifact_path = run_dir / "role-artifacts/ios-verification.json" - artifact = json.loads(artifact_path.read_text()) - artifact["status"] = "PASS" - artifact["output"]["result"] = "BLOCKED" - artifact_path.write_text(json.dumps(artifact), encoding="utf-8") - - with self.assertRaisesRegex(ValueError, "Verification semantic gate failed"): - orchestrator.validate_completed_verification(run_dir, "ios") - - blocked = json.loads(artifact_path.read_text()) - self.assertEqual(blocked["status"], "BLOCKED") - self.assertIn("output.result", blocked["evidence"]) - - def test_platform_filter_dispatches_only_requested_agent(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--platform", - "windows", - "--model", - "test-model", - "--dry-run", - ) - - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - payload = json.loads(result.stdout) - self.assertEqual([item["platform"] for item in payload["commands"]], ["windows"]) - command = payload["commands"][0]["command"] - self.assertIn("-C", command) - self.assertIn(str(REPO_ROOT / "windows"), command) - - def test_platform_dispatch_rejects_non_main_project_selected_by_contract(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - contract_path = run_dir / "role-artifacts/contract.json" - contract = json.loads(contract_path.read_text()) - contract["output"]["platform_targets"]["ios"]["target_project"] = "iOS/APIExample-OC/" - contract["output"]["platform_targets"]["ios"]["files_allowed"] = ["iOS/APIExample-OC/"] - contract_path.write_text(json.dumps(contract), encoding="utf-8") - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--platform", - "ios", - "--model", - "test-model", - "--dry-run", - ) - - self.assertEqual(result.returncode, 1) - self.assertIn( - "contract.output.platform_targets.ios.target_project must be exactly iOS/APIExample/", - result.stderr, - ) - - def test_verification_uses_corresponding_platform_implementation(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - implementation = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - self.assertEqual(implementation.returncode, 0, implementation.stderr) - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "verification", - "--platform", - "windows", - "--model", - "test-model", - "--dry-run", - ) - - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - prompt = (run_dir / "role-prompts/windows-verification.md").read_text() - self.assertIn("windows-implementation completed", prompt) - self.assertIn("macOS cannot provide Windows build evidence", prompt) - self.assertIn("Do not download Windows SDK archives", prompt) - implementation_prompt = (run_dir / "role-prompts/windows-implementation.md").read_text() - self.assertNotIn("cannot provide Windows build evidence", implementation_prompt) - - def test_platform_dispatch_timeout_keeps_other_platform_scope_isolated(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir, implementation_sleep=2) - self.dispatch_contract(run_dir, codex_bin) - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--platform", - "android", - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - "--timeout-seconds", - "1", - ) - - self.assertEqual(result.returncode, 1) - self.assertIn("android-implementation timed out after 1 seconds", result.stderr) - self.assertTrue((run_dir / "dispatch-logs/android-implementation.jsonl").exists()) - self.assertFalse((run_dir / "dispatch-logs/ios-implementation.jsonl").exists()) - - def test_timeout_terminates_agent_child_processes(self): - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - prompt_path = run_dir / "role-prompts/android-implementation.md" - prompt_path.parent.mkdir(parents=True) - prompt_path.write_text("implement", encoding="utf-8") - marker = Path(tmpdir) / "late-child-write.txt" - child_code = ( - "import time; from pathlib import Path; " - f"time.sleep(0.4); Path({str(marker)!r}).write_text('late')" - ) - parent_code = ( - "import subprocess, sys, time; " - f"subprocess.Popen([sys.executable, '-c', {child_code!r}]); " - "time.sleep(10)" - ) - - with self.assertRaisesRegex(ValueError, "timed out"): - run_codex_role( - command=[sys.executable, "-c", parent_code], - name="android-implementation", - role="implementation", - platform="android", - prompt_path=prompt_path, - artifact_path=run_dir / "role-artifacts/android-implementation.json", - raw_result_path=run_dir / "raw-results/android-implementation.json", - log_path=run_dir / "dispatch-logs/android-implementation.jsonl", - stderr_path=run_dir / "dispatch-logs/android-implementation.stderr.log", - model_profile="deep", - model="test-model", - reasoning_effort="high", - codex_version="codex-cli test", - working_directory=Path(tmpdir), - working_directory_manifest="Android/APIExample/", - input_snapshot="input-snapshots/android-implementation.json", - input_snapshot_sha256="a" * 64, - expected_repository_state={"state": "clean"}, - timeout_seconds=0.1, - ) - time.sleep(0.6) - - self.assertFalse(marker.exists()) - - def test_full_multi_platform_run_assembles_blocked_v4_manifest(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - for phase in ["implementation", "verification"]: - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - phase, - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - - result = self.run_orchestrator( - "assemble", - "--run-dir", - str(run_dir), - "--matrix", - str(matrix_path), - "--final-status", - "BLOCKED", - "--cross-platform-result", - "BLOCKED", - "--cross-platform-evidence", - "Target SDK and Windows-host build evidence are pending.", - ) - - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - final_path = run_dir / "final-acceptance-manifest.json" - final_manifest = json.loads(final_path.read_text()) - self.assertEqual(final_manifest["version"], 4) - self.assertEqual(final_manifest["cross_platform_acceptance"]["result"], "BLOCKED") - self.assertEqual(sorted(final_manifest["platforms"]), PLATFORMS) - self.assertTrue(final_manifest["release"]["required"]) - self.assertEqual( - final_manifest["release"]["target_sdk_versions"], - {platform: "4.6.4" for platform in PLATFORMS}, - ) - validation = subprocess.run( - [sys.executable, str(VALIDATOR), str(final_path)], - cwd=REPO_ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - self.assertEqual(validation.returncode, 0, validation.stderr) - - def test_assemble_reports_only_repository_gates_without_external_metadata(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - manifest_path = run_dir / "acceptance-manifest.json" - manifest = json.loads(manifest_path.read_text()) - ios = next( - check - for check in manifest["release"]["checks"] - if check["name"] == "sdk-version-ios" - ) - ios.update( - { - "result": "PASS", - "actual_versions": {"iOS/APIExample/sdk.podspec": self.TARGET_SDK_VERSION}, - "evidence": "Untrusted pre-assembly value.", - "reason": "", - } - ) - manifest_path.write_text(json.dumps(manifest), encoding="utf-8") - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - for phase in ["implementation", "verification"]: - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - phase, - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - - result = self.run_orchestrator( - "assemble", - "--run-dir", - str(run_dir), - "--matrix", - str(matrix_path), - "--final-status", - "PASS", - "--cross-platform-result", - "PASS", - "--cross-platform-evidence", - "All required platforms passed independent verification.", - ) - - self.assertEqual(result.returncode, 1) - self.assertIn("sdk-version-android", result.stderr) - self.assertIn("non-BLOCKED acceptance requires sdk-version-ios=PASS", result.stderr) - self.assertNotIn("qa_acceptance", result.stderr) - - def test_assemble_rejects_matrix_path_changed_since_init(self): - matrix_path = self.write_matrix() - other_matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - - result = self.run_orchestrator( - "assemble", - "--run-dir", - str(run_dir), - "--matrix", - str(other_matrix_path), - ) - - self.assertEqual(result.returncode, 1) - self.assertIn("matrix path changed since init", result.stderr) - - def test_assemble_rejects_requirement_changed_since_init(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - manifest_path = run_dir / "acceptance-manifest.json" - manifest = json.loads(manifest_path.read_text()) - manifest["requirement"]["feature"] = "Different requirement" - manifest_path.write_text(json.dumps(manifest), encoding="utf-8") - - result = self.run_orchestrator( - "assemble", - "--run-dir", - str(run_dir), - "--matrix", - str(matrix_path), - ) - - self.assertEqual(result.returncode, 1) - self.assertIn("requirement changed since init", result.stderr) - - def test_assemble_rejects_missing_completed_prompt_evidence(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - (run_dir / "role-prompts/contract.md").unlink() - - result = self.run_orchestrator( - "assemble", - "--run-dir", - str(run_dir), - "--matrix", - str(matrix_path), - ) - - self.assertEqual(result.returncode, 1) - self.assertIn("contract.dispatch.prompt does not exist", result.stderr) - - def test_retrying_platform_implementation_invalidates_old_verification(self): - matrix_path = self.write_matrix() - with tempfile.TemporaryDirectory() as tmpdir: - run_dir = Path(tmpdir) / "run" - self.init_workspace(matrix_path, run_dir) - codex_bin = self.write_fake_codex(tmpdir) - self.dispatch_contract(run_dir, codex_bin) - for phase in ["implementation", "verification"]: - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - phase, - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - implementation_path = run_dir / "role-artifacts/windows-implementation.json" - implementation = json.loads(implementation_path.read_text()) - implementation["status"] = "BLOCKED" - implementation_path.write_text(json.dumps(implementation), encoding="utf-8") - - result = self.run_orchestrator( - "dispatch", - "--run-dir", - str(run_dir), - "--phase", - "implementation", - "--platform", - "windows", - "--retry", - "--model", - "test-model", - "--codex-bin", - str(codex_bin), - ) - - self.assertEqual(result.returncode, 0, result.stderr + result.stdout) - verification = json.loads( - (run_dir / "role-artifacts/windows-verification.json").read_text() - ) - self.assertEqual(verification["status"], "BLOCKED") - self.assertEqual(verification["dispatch"]["mode"], "pending") - - def test_removed_single_platform_phases_are_rejected(self): - result = self.run_orchestrator( - "dispatch", - "--run-dir", - "/tmp/not-used", - "--phase", - "target-test", - "--model", - "test-model", - "--dry-run", - ) - self.assertNotEqual(result.returncode, 0) - self.assertIn("invalid choice", result.stderr) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/ai-engineering/tools/prepare_case_execution.py b/docs/ai-engineering/tools/prepare_case_execution.py deleted file mode 100644 index 85807710d..000000000 --- a/docs/ai-engineering/tools/prepare_case_execution.py +++ /dev/null @@ -1,511 +0,0 @@ -#!/usr/bin/env python3 -"""Prepare one cross-platform API Examples requirement package.""" - -import argparse -import hashlib -import json -import re -import sys -from pathlib import Path - -from generate_case_backlog import DEFAULT_MATRIX, REPO_ROOT, generate_execution_units - - -PLATFORMS = ["android", "ios", "macos", "windows"] -DEFAULT_REPOSITORY_PROFILE = REPO_ROOT / "docs/ai-engineering/repository-profile.json" -PROFILE_SOURCE_FIELDS = { - "gradle-property": {"path", "kind", "key"}, - "cocoapods": {"path", "kind", "package"}, - "external-injection": {"path", "kind", "reason"}, - "archive-name": {"path", "kind", "prefix", "suffix"}, -} -SEMVER_CAPTURE = r"([0-9]+\.[0-9]+\.[0-9]+)" -SDK_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") -DEFAULT_PLATFORM_TARGETS = { - "android": "Android/APIExample/", - "ios": "iOS/APIExample/", - "macos": "macOS/", - "windows": "windows/", -} -PLATFORM_UNIT_GROUPS = { - "Android full": "android", - "iOS UIKit": "ios", - "macOS": "macos", - "Windows": "windows", -} -ROLE_CONTRACTS = { - "contract": [ - "shared scenario, APIs, and non-goals", - "cross-platform behavior contract", - "target project and allowed files for every platform", - "reference contract when required", - ], - "implementation": [ - "target project", - "query-cases and upsert-case results", - "changed files", - "matrix update proposals", - ], - "verification": [ - "findings-first review and parity result", - "entry point and UX consistency", - "exact target command strings and command kinds for JSONL evidence binding", - "target build commands and result", - "skipped checks with reasons", - ], -} - - -def select_execution_unit(units, feature=None, index=0): - filtered = [unit for unit in units if feature is None or unit["feature"] == feature] - if not filtered: - suffix = f" for feature={feature}" if feature else "" - raise ValueError(f"no execution units found{suffix}") - if index < 0 or index >= len(filtered): - raise ValueError(f"execution unit index {index} is out of range; {len(filtered)} unit(s) available") - return filtered[index] - - -def source_case_from_candidate(candidate): - if not candidate: - return "" - return f"{candidate['project'].rstrip('/')}/{candidate['path'].lstrip('/')}" - - -def resolve_source_case(candidate): - source_case = source_case_from_candidate(candidate) - if not source_case: - return "" - direct_path = REPO_ROOT / source_case - if direct_path.exists(): - return source_case - project_root = REPO_ROOT / candidate["project"] - basename = Path(candidate["path"].rstrip("/")).name - if not project_root.exists() or not basename: - return source_case - matches = [ - path - for path in project_root.rglob(basename) - if "build" not in path.relative_to(project_root).parts - ] - if not matches: - return source_case - matches.sort(key=lambda path: (len(path.parts), path.as_posix())) - return matches[0].relative_to(REPO_ROOT).as_posix() - - -def pending_dispatch(name): - return { - "mode": "pending", - "prompt": f"role-prompts/{name}.md", - "artifact": f"role-artifacts/{name}.json", - } - - -def pending_artifact(name, summary, output): - return { - "agent_id": f"{name}-agent-pending", - "dispatch": pending_dispatch(name), - "status": "BLOCKED", - "evidence": f"Pending {name} gate.", - "summary": summary, - "output": output, - } - - -def default_platform_targets(): - return { - platform: { - "required": True, - "target_project": target, - "key_constraints": [ - f"Use the {platform} project rules and keep source changes inside {target}" - ], - "files_allowed": [target], - "waiver_reason": "", - } - for platform, target in DEFAULT_PLATFORM_TARGETS.items() - } - - -def load_repository_profile(profile_path=DEFAULT_REPOSITORY_PROFILE): - path = Path(profile_path) - profile = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(profile, dict): - raise ValueError("repository profile must be a JSON object") - if set(profile) != {"version", "sdk_version_sources"}: - raise ValueError("repository profile must contain only version and sdk_version_sources") - if profile.get("version") != 1: - raise ValueError("repository profile version must be 1") - sources = profile.get("sdk_version_sources") - if not isinstance(sources, dict) or set(sources) != set(PLATFORMS): - raise ValueError("repository profile must define SDK version sources for every platform") - for platform in PLATFORMS: - entries = sources[platform] - if not isinstance(entries, list) or not entries: - raise ValueError(f"repository profile {platform} SDK version sources must be non-empty") - seen_paths = set() - for index, source in enumerate(entries): - if not isinstance(source, dict): - raise ValueError(f"repository profile {platform} source {index} must be an object") - kind = source.get("kind") - expected_fields = PROFILE_SOURCE_FIELDS.get(kind) - if expected_fields is None: - raise ValueError( - f"repository profile {platform} source {index} has unknown kind {kind}" - ) - if set(source) != expected_fields: - raise ValueError( - f"repository profile {platform} source {index} must contain " - + ", ".join(sorted(expected_fields)) - ) - if not all( - isinstance(source[field], str) and source[field] for field in expected_fields - ): - raise ValueError( - f"repository profile {platform} source {index} fields must be non-empty strings" - ) - source_path = Path(source["path"]) - if source_path.is_absolute() or ".." in source_path.parts: - raise ValueError( - "repository profile source path must be repository-relative: " - + source["path"] - ) - normalized_path = source_path.as_posix() - if normalized_path in seen_paths: - raise ValueError( - f"repository profile contains duplicate source path: {normalized_path}" - ) - seen_paths.add(normalized_path) - return profile - - -def resolve_target_sdk_versions(target_sdk_version, platform_sdk_versions=None): - if not target_sdk_version: - raise ValueError("target_sdk_version is required") - if not SDK_VERSION_RE.fullmatch(str(target_sdk_version)): - raise ValueError("target_sdk_version must use x.y.z format") - versions = {platform: target_sdk_version for platform in PLATFORMS} - seen = set() - for value in platform_sdk_versions or []: - if not isinstance(value, str) or value.count("=") != 1: - raise ValueError("platform SDK version override must use platform=x.y.z") - platform, version = value.split("=", 1) - if platform not in PLATFORMS: - raise ValueError(f"unknown platform: {platform}") - if platform in seen: - raise ValueError(f"duplicate platform SDK version: {platform}") - if not SDK_VERSION_RE.fullmatch(version): - raise ValueError(f"{platform} SDK version must use x.y.z format") - seen.add(platform) - versions[platform] = version - return versions - - -def sdk_version_pattern(source): - kind = source["kind"] - if kind == "gradle-property": - return rf"(?m)^\s*{re.escape(source['key'])}\s*=\s*{SEMVER_CAPTURE}\s*$" - if kind == "cocoapods": - # Anchor to the start of a line and reject Ruby comments so a commented-out - # `# pod 'Pkg', 'x.y.z'` line can never be accepted as a live declaration. - package = re.escape(source["package"]) - return ( - rf"(?m)^(?!\s*#)\s*pod\s+['\"]{package}['\"]" - rf"\s*,\s*['\"]{SEMVER_CAPTURE}['\"]" - ) - if kind == "archive-name": - return re.escape(source["prefix"]) + SEMVER_CAPTURE + re.escape(source["suffix"]) - raise ValueError(f"unsupported SDK version source kind: {kind}") - - -def primary_project_version_sources(sources): - selected = {} - for platform, target in DEFAULT_PLATFORM_TARGETS.items(): - target_path = Path(target) - platform_sources = [] - for source in sources[platform]: - source_path = Path(source["path"]) - try: - source_path.relative_to(target_path) - except ValueError: - continue - platform_sources.append(source) - if not platform_sources: - raise ValueError( - f"repository profile has no {platform} SDK version source inside {target}" - ) - selected[platform] = platform_sources - return selected - - -def collect_sdk_version_checks( - target_sdk_versions, - repo_root=REPO_ROOT, - sources=None, - profile_path=DEFAULT_REPOSITORY_PROFILE, -): - if sources is None: - sources = load_repository_profile(profile_path)["sdk_version_sources"] - sources = primary_project_version_sources(sources) - checks = [] - for platform in PLATFORMS: - target_sdk_version = target_sdk_versions[platform] - entries = sources[platform] - actual_versions = {} - problems = [] - for source in entries: - path_text = source["path"] - path = Path(repo_root) / path_text - if not path.exists(): - actual_versions[path_text] = "" - problems.append(f"missing {path_text}") - continue - if source["kind"] == "external-injection": - actual_versions[path_text] = "" - problems.append(f"{path_text}: {source['reason']}") - continue - pattern = sdk_version_pattern(source) - matches = sorted(set(re.findall(pattern, path.read_text(encoding="utf-8")))) - if not matches: - actual_versions[path_text] = "" - problems.append(f"no active SDK version declaration in {path_text}") - continue - if len(matches) > 1: - actual_versions[path_text] = "" - problems.append(f"expected one SDK version in {path_text}, found {len(matches)}") - continue - actual_versions[path_text] = matches[0] - if matches[0] != target_sdk_version: - problems.append(f"{path_text}={matches[0]}") - result = "PASS" if not problems else "BLOCKED" - checks.append( - { - "name": f"sdk-version-{platform}", - "result": result, - "expected_version": target_sdk_version, - "actual_versions": actual_versions, - "evidence": "; ".join( - f"{path}={version}" - for path, version in actual_versions.items() - if version - ), - "reason": "" if not problems else f"Expected {target_sdk_version}; " + "; ".join(problems), - } - ) - return checks - - -def repository_profile_binding(profile_path): - resolved = Path(profile_path).resolve() - try: - relative = resolved.relative_to(REPO_ROOT.resolve()).as_posix() - except ValueError as exc: - raise ValueError("repository profile must be inside the repository") from exc - return relative, hashlib.sha256(resolved.read_bytes()).hexdigest() - - -def build_manifest_seed( - requirement, - source_case, - version_sources, - repository_profile, - repository_profile_sha256, -): - reference_required = bool(source_case) - reference_result = "BLOCKED" if reference_required else "SKIPPED" - targets = default_platform_targets() - contract = pending_artifact( - "contract", - "Pending shared product, architecture, and reference contract.", - { - "scenario": f"Implement {requirement['feature']} consistently across official platforms.", - "key_apis": requirement["key_apis"], - "non_goals": [], - "reference": { - "required": reference_required, - "source_case": source_case, - "contract_result": reference_result, - }, - "cross_platform_requirements": [], - "platform_targets": targets, - }, - ) - platforms = {} - for platform, target in DEFAULT_PLATFORM_TARGETS.items(): - platforms[platform] = { - "implementation": pending_artifact( - f"{platform}-implementation", - f"Pending {platform} implementation.", - { - "target_project": target, - "query_cases": "Pending target project case query.", - "upsert_case": "Pending target project update.", - "files_changed": [], - "matrix_updates": [], - }, - ), - "verification": pending_artifact( - f"{platform}-verification", - f"Pending independent {platform} verification.", - { - "result": "BLOCKED", - "findings": [], - "parity_result": reference_result, - "entry_point": f"Pending {platform} entry point verification.", - "ux_notes": "Pending verification.", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [], - }, - ), - } - return { - "version": 4, - "final_status": "BLOCKED", - "requirement": requirement, - "contract": contract, - "platforms": platforms, - "cross_platform_acceptance": { - "result": "BLOCKED", - "evidence": "Pending required platform verification.", - "differences": [], - }, - "release": { - "required": True, - "repository_profile": repository_profile, - "repository_profile_sha256": repository_profile_sha256, - "target_sdk_versions": dict(requirement["target_sdk_versions"]), - "checks": collect_sdk_version_checks( - requirement["target_sdk_versions"], sources=version_sources - ), - "skipped_checks": [], - }, - "knowledge_updates": [], - } - - -def build_platform_units(units, feature): - result = { - platform: { - "target_project": DEFAULT_PLATFORM_TARGETS[platform], - "matrix_candidates": [], - } - for platform in PLATFORMS - } - for unit in units: - if unit["feature"] != feature: - continue - platform = PLATFORM_UNIT_GROUPS.get(unit["platform_unit"]) - if platform: - result[platform]["matrix_candidates"].append(unit) - return result - - -def prepare_case_execution( - matrix_path, - feature=None, - index=0, - sdk_family=None, - key_apis=None, - target_sdk_version=None, - platform_sdk_versions=None, - repository_profile=DEFAULT_REPOSITORY_PROFILE, -): - target_sdk_versions = resolve_target_sdk_versions( - target_sdk_version, platform_sdk_versions - ) - profile_path = Path(repository_profile).resolve() - profile = load_repository_profile(profile_path) - profile_manifest_path, profile_sha256 = repository_profile_binding(profile_path) - backlog = generate_execution_units(matrix_path) - matching = [ - unit for unit in backlog["execution_units"] if feature is None or unit["feature"] == feature - ] - selected = select_execution_unit(matching, None, index) if matching else None - if selected is None and not (feature and sdk_family and key_apis): - raise ValueError( - "a requirement outside the actionable matrix needs feature, sdk_family, and key_apis" - ) - requirement = { - "feature": feature or selected["feature"], - "sdk_family": sdk_family or selected["sdk_family"], - "key_apis": key_apis or selected["key_apis"], - "target_sdk_versions": target_sdk_versions, - } - source_candidate = None - if selected and selected["reference_candidates"]: - source_candidate = selected["reference_candidates"][0] - source_case = resolve_source_case(source_candidate) - blockers = [] - if not source_case: - blockers.append("No DONE reference candidate found; Contract must identify a source case.") - return { - "requirement": requirement, - "platform_units": build_platform_units(backlog["execution_units"], requirement["feature"]), - "reference_contract": { - "required": bool(source_case), - "source_candidate": source_candidate, - "source_case": source_case, - }, - "role_contracts": ROLE_CONTRACTS, - "execution_steps": [ - "Run one shared Contract for all official platforms.", - "Run Android, iOS, macOS, and Windows Implementation agents independently with attributed deltas.", - "Run independent platform Verification agents in parallel.", - "Record cross-platform differences and final acceptance.", - "Validate the manifest before applying matrix updates.", - ], - "acceptance_manifest_seed": build_manifest_seed( - requirement, - source_case, - profile["sdk_version_sources"], - profile_manifest_path, - profile_sha256, - ), - "validation_command": "python3 docs/ai-engineering/tools/validate_acceptance_manifest.py ", - "blockers": blockers, - } - - -def main(argv=None): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--matrix", default=str(DEFAULT_MATRIX)) - parser.add_argument("--feature", help="Requirement feature; defaults to the highest-priority gap") - parser.add_argument("--sdk-family", help="Required when the feature is not actionable in the matrix") - parser.add_argument("--key-api", action="append", help="Key SDK API; repeat for multiple APIs") - parser.add_argument("--target-sdk-version", required=True) - parser.add_argument( - "--platform-sdk-version", - action="append", - default=[], - help="Override one platform as platform=x.y.z; repeat as needed", - ) - parser.add_argument("--repository-profile", default=str(DEFAULT_REPOSITORY_PROFILE)) - parser.add_argument("--index", type=int, default=0) - parser.add_argument("--output") - args = parser.parse_args(argv) - try: - package = prepare_case_execution( - Path(args.matrix), - args.feature, - index=args.index, - sdk_family=args.sdk_family, - key_apis=args.key_api, - target_sdk_version=args.target_sdk_version, - platform_sdk_versions=args.platform_sdk_version, - repository_profile=Path(args.repository_profile), - ) - except (OSError, ValueError) as exc: - print(f"ERROR: {exc}", file=sys.stderr) - return 1 - payload = json.dumps(package, indent=2, ensure_ascii=False) + "\n" - if args.output: - Path(args.output).write_text(payload, encoding="utf-8") - else: - print(payload, end="") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/ai-engineering/tools/prepare_case_execution_test.py b/docs/ai-engineering/tools/prepare_case_execution_test.py deleted file mode 100644 index bf782825a..000000000 --- a/docs/ai-engineering/tools/prepare_case_execution_test.py +++ /dev/null @@ -1,453 +0,0 @@ -import json -import sys -import tempfile -import textwrap -import unittest -from pathlib import Path - - -TOOLS_DIR = Path(__file__).resolve().parent -if str(TOOLS_DIR) not in sys.path: - sys.path.insert(0, str(TOOLS_DIR)) - -from prepare_case_execution import ( - collect_sdk_version_checks, - load_repository_profile, - prepare_case_execution, - primary_project_version_sources, - resolve_target_sdk_versions, -) -from validate_acceptance_manifest import validate_manifest - - -PLATFORMS = ["android", "ios", "macos", "windows"] - - -class PrepareCaseExecutionTest(unittest.TestCase): - TARGET_SDK_VERSION = "4.6.4" - - def target_sdk_versions(self, **overrides): - versions = {platform: self.TARGET_SDK_VERSION for platform in PLATFORMS} - versions.update(overrides) - return versions - - def write_matrix(self): - handle = tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) - handle.write( - textwrap.dedent( - """ - | Feature | SDK Family | Key APIs | Android full | iOS UIKit | macOS | Windows | Notes | - | --- | --- | --- | --- | --- | --- | --- | --- | - | Join channel audio | Full RTC | `joinChannel`, `setAudioProfile` | `DONE(app/JoinChannelAudio.java)` | `DONE/APIExample/JoinChannelAudio.swift` | `PARTIAL(APIExample/JoinChannelAudio.swift)` | `MISSING` | Keep all official platforms aligned. | - - ## Confirmed Gaps - - | Gap | Affected Units | Severity | - | --- | --- | --- | - | Basic audio-only join channel | Windows | High - missing foundational case | - """ - ) - ) - handle.close() - self.addCleanup(lambda: Path(handle.name).unlink(missing_ok=True)) - return Path(handle.name) - - def test_prepares_one_requirement_with_four_platform_delivery_units(self): - package = prepare_case_execution( - self.write_matrix(), - feature="Join channel audio", - target_sdk_version=self.TARGET_SDK_VERSION, - ) - - manifest = package["acceptance_manifest_seed"] - self.assertEqual(manifest["version"], 4) - self.assertEqual(sorted(manifest["platforms"]), PLATFORMS) - self.assertIn("contract", manifest) - self.assertNotIn("roles", manifest) - self.assertEqual(sorted(package["role_contracts"]), ["contract", "implementation", "verification"]) - self.assertEqual(sorted(manifest["contract"]["output"]["platform_targets"]), PLATFORMS) - self.assertEqual( - manifest["requirement"]["target_sdk_versions"], - self.target_sdk_versions(), - ) - self.assertTrue(manifest["release"]["required"]) - self.assertEqual( - manifest["release"]["repository_profile"], - "docs/ai-engineering/repository-profile.json", - ) - self.assertRegex(manifest["release"]["repository_profile_sha256"], r"^[0-9a-f]{64}$") - self.assertEqual( - manifest["release"]["target_sdk_versions"], - self.target_sdk_versions(), - ) - self.assertNotIn("qa_acceptance", manifest["release"]) - self.assertNotIn("publication_channel", manifest["requirement"]) - self.assertNotIn("publication", manifest["release"]) - for platform in PLATFORMS: - unit = manifest["platforms"][platform] - self.assertEqual(sorted(unit), ["implementation", "verification"]) - self.assertEqual(unit["implementation"]["dispatch"]["mode"], "pending") - self.assertEqual(unit["verification"]["status"], "BLOCKED") - self.assertEqual(validate_manifest(manifest), []) - - def test_platform_defaults_select_official_full_sdk_projects(self): - package = prepare_case_execution( - self.write_matrix(), - feature="Join channel audio", - target_sdk_version=self.TARGET_SDK_VERSION, - ) - targets = package["acceptance_manifest_seed"]["contract"]["output"]["platform_targets"] - - self.assertEqual(targets["android"]["target_project"], "Android/APIExample/") - self.assertEqual(targets["ios"]["target_project"], "iOS/APIExample/") - self.assertEqual(targets["macos"]["target_project"], "macOS/") - self.assertEqual(targets["windows"]["target_project"], "windows/") - self.assertTrue(all(target["required"] for target in targets.values())) - self.assertEqual( - { - platform: unit["target_project"] - for platform, unit in package["platform_units"].items() - }, - { - "android": "Android/APIExample/", - "ios": "iOS/APIExample/", - "macos": "macOS/", - "windows": "windows/", - }, - ) - - def test_selects_highest_priority_feature_when_omitted(self): - package = prepare_case_execution( - self.write_matrix(), target_sdk_version=self.TARGET_SDK_VERSION - ) - - self.assertEqual(package["requirement"]["feature"], "Join channel audio") - self.assertEqual(package["requirement"]["key_apis"], ["joinChannel", "setAudioProfile"]) - - def test_package_is_json_serializable(self): - serialized = json.dumps( - prepare_case_execution( - self.write_matrix(), target_sdk_version=self.TARGET_SDK_VERSION - ) - ) - self.assertIn('"version": 4', serialized) - - def test_prepares_new_requirement_not_yet_present_in_matrix(self): - package = prepare_case_execution( - self.write_matrix(), - feature="Spatial audio", - sdk_family="Full RTC", - key_apis=["enableSpatialAudio"], - target_sdk_version=self.TARGET_SDK_VERSION, - ) - - self.assertEqual(package["requirement"]["feature"], "Spatial audio") - self.assertEqual(package["requirement"]["key_apis"], ["enableSpatialAudio"]) - self.assertEqual(sorted(package["acceptance_manifest_seed"]["platforms"]), PLATFORMS) - - def test_requires_target_sdk_version(self): - with self.assertRaisesRegex(ValueError, "target_sdk_version is required"): - prepare_case_execution(self.write_matrix(), feature="Join channel audio") - - def test_platform_sdk_version_overrides_the_shared_baseline(self): - package = prepare_case_execution( - self.write_matrix(), - feature="Join channel audio", - target_sdk_version="4.6.2", - platform_sdk_versions=["android=4.6.3"], - ) - - expected = self.target_sdk_versions( - android="4.6.3", ios="4.6.2", macos="4.6.2", windows="4.6.2" - ) - manifest = package["acceptance_manifest_seed"] - self.assertEqual(package["requirement"]["target_sdk_versions"], expected) - self.assertEqual(manifest["release"]["target_sdk_versions"], expected) - self.assertEqual( - { - check["name"]: check["expected_version"] - for check in manifest["release"]["checks"] - }, - {f"sdk-version-{platform}": version for platform, version in expected.items()}, - ) - - def test_rejects_invalid_platform_sdk_version_overrides(self): - cases = [ - (["android"], "must use platform=x.y.z"), - (["linux=4.6.3"], "unknown platform: linux"), - (["android=next"], "android SDK version must use x.y.z format"), - (["android=4.6.3", "android=4.6.4"], "duplicate platform SDK version: android"), - ] - - for overrides, message in cases: - with self.subTest(overrides=overrides), self.assertRaisesRegex(ValueError, message): - resolve_target_sdk_versions("4.6.2", overrides) - - def test_rejects_invalid_baseline_sdk_version(self): - with self.assertRaisesRegex(ValueError, "target_sdk_version must use x.y.z format"): - resolve_target_sdk_versions("next") - - def test_collects_live_sdk_version_evidence(self): - for distribution in ["Shengwang", "Agora"]: - with self.subTest(distribution=distribution), tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - profile_path = self.write_repository_profile(root, distribution) - - profile = load_repository_profile(profile_path) - checks = collect_sdk_version_checks( - self.target_sdk_versions(), - repo_root=root, - sources=profile["sdk_version_sources"], - ) - - self.assertTrue(all(check["result"] == "PASS" for check in checks)) - self.assertTrue( - all( - set(check["actual_versions"].values()) == {self.TARGET_SDK_VERSION} - for check in checks - ) - ) - - archive = root / "windows/APIExample/install.ps1" - archive.write_text( - f"{distribution}_Native_SDK_for_Windows_v4.6.2_FULL.zip\n", - encoding="utf-8", - ) - checks = collect_sdk_version_checks( - self.target_sdk_versions(), - repo_root=root, - profile_path=profile_path, - ) - windows = next( - check for check in checks if check["name"] == "sdk-version-windows" - ) - self.assertEqual(windows["result"], "BLOCKED") - self.assertIn("4.6.2", windows["reason"]) - - def test_ignores_non_primary_project_sdk_version_sources(self): - sources = { - "android": [ - { - "path": "Android/APIExample/gradle.properties", - "kind": "gradle-property", - "key": "rtc_sdk_version", - }, - { - "path": "Android/APIExample-Compose/gradle.properties", - "kind": "gradle-property", - "key": "rtc_sdk_version", - }, - ], - "ios": [ - { - "path": "iOS/APIExample/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_iOS", - }, - { - "path": "iOS/APIExample-SwiftUI/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_iOS", - }, - ], - "macos": [ - { - "path": "macOS/Podfile", - "kind": "cocoapods", - "package": "AgoraRtcEngine_macOS", - } - ], - "windows": [ - { - "path": "windows/APIExample/install.ps1", - "kind": "archive-name", - "prefix": "Agora_Native_SDK_for_Windows_v", - "suffix": "_FULL.zip", - } - ], - } - - selected = primary_project_version_sources(sources) - - self.assertEqual(len(selected["android"]), 1) - self.assertEqual( - selected["android"][0]["path"], "Android/APIExample/gradle.properties" - ) - self.assertEqual(len(selected["ios"]), 1) - self.assertEqual(selected["ios"][0]["path"], "iOS/APIExample/Podfile") - - def test_requires_primary_project_sdk_version_source(self): - sources = { - "android": [{"path": "Android/APIExample-Compose/gradle.properties"}], - "ios": [{"path": "iOS/APIExample/Podfile"}], - "macos": [{"path": "macOS/Podfile"}], - "windows": [{"path": "windows/APIExample/install.ps1"}], - } - - with self.assertRaisesRegex(ValueError, "no android SDK version source"): - primary_project_version_sources(sources) - - def test_rejects_commented_out_cocoapods_declaration(self): - for comment_line in [ - " # pod 'AgoraRtcEngine_iOS', '4.6.4'", - "#pod 'AgoraRtcEngine_iOS', '4.6.4'", - "\t# pod 'AgoraRtcEngine_iOS', '4.6.4'", - ]: - with self.subTest(comment_line=comment_line), tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - profile_path = self.write_repository_profile(root, "Agora") - (root / "iOS/APIExample/Podfile").write_text( - f"def common_pods\n{comment_line}\n" - " pod 'sdk', :path => 'sdk.podspec'\nend\n", - encoding="utf-8", - ) - - checks = collect_sdk_version_checks( - self.target_sdk_versions(), - repo_root=root, - profile_path=profile_path, - ) - ios = next(check for check in checks if check["name"] == "sdk-version-ios") - - self.assertEqual(ios["result"], "BLOCKED") - self.assertEqual(ios["actual_versions"]["iOS/APIExample/Podfile"], "") - self.assertIn( - "no active SDK version declaration in iOS/APIExample/Podfile", ios["reason"] - ) - - def test_accepts_cocoapods_declaration_with_trailing_comment(self): - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - profile_path = self.write_repository_profile(root, "Agora") - (root / "iOS/APIExample/Podfile").write_text( - " pod 'AgoraRtcEngine_iOS', '4.6.4' # pinned for this release\n", - encoding="utf-8", - ) - - checks = collect_sdk_version_checks( - self.target_sdk_versions(), - repo_root=root, - profile_path=profile_path, - ) - ios = next(check for check in checks if check["name"] == "sdk-version-ios") - - self.assertEqual(ios["result"], "PASS") - self.assertEqual(ios["actual_versions"]["iOS/APIExample/Podfile"], "4.6.4") - - def test_external_injection_remains_blocked_without_repository_version_evidence(self): - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - profile_path = self.write_repository_profile(root, "Agora") - profile = json.loads(profile_path.read_text(encoding="utf-8")) - profile["sdk_version_sources"]["ios"] = [ - { - "path": "iOS/APIExample/Podfile", - "kind": "external-injection", - "reason": "RTC SDK binaries are injected outside the repository.", - } - ] - profile_path.write_text(json.dumps(profile), encoding="utf-8") - podspec = root / "iOS/APIExample/sdk.podspec" - podspec.parent.mkdir(parents=True, exist_ok=True) - podspec.write_text( - "spec.version = \"4.6.4\"\n", - encoding="utf-8", - ) - - checks = collect_sdk_version_checks( - self.target_sdk_versions(), repo_root=root, profile_path=profile_path - ) - ios = next(check for check in checks if check["name"] == "sdk-version-ios") - self.assertEqual(ios["result"], "BLOCKED") - self.assertEqual(ios["actual_versions"], {"iOS/APIExample/Podfile": ""}) - self.assertEqual(ios["evidence"], "") - self.assertIn("injected outside the repository", ios["reason"]) - - def test_collects_staggered_platform_sdk_versions(self): - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - profile_path = self.write_repository_profile(root, "Agora") - (root / "Android/APIExample/gradle.properties").write_text( - "rtc_sdk_version = 4.6.3\n", encoding="utf-8" - ) - for path_text in [ - "iOS/APIExample/Podfile", - "macOS/Podfile", - "windows/APIExample/install.ps1", - ]: - path = root / path_text - path.write_text( - path.read_text(encoding="utf-8").replace("4.6.4", "4.6.2"), - encoding="utf-8", - ) - - targets = { - "android": "4.6.3", - "ios": "4.6.2", - "macos": "4.6.2", - "windows": "4.6.2", - } - checks = collect_sdk_version_checks( - targets, repo_root=root, profile_path=profile_path - ) - - self.assertTrue(all(check["result"] == "PASS" for check in checks)) - self.assertEqual( - {check["name"]: check["expected_version"] for check in checks}, - {f"sdk-version-{platform}": version for platform, version in targets.items()}, - ) - - def write_repository_profile(self, root, distribution): - sources = { - "android": [ - { - "path": "Android/APIExample/gradle.properties", - "kind": "gradle-property", - "key": "rtc_sdk_version", - } - ], - "ios": [ - { - "path": "iOS/APIExample/Podfile", - "kind": "cocoapods", - "package": f"{distribution}RtcEngine_iOS", - } - ], - "macos": [ - { - "path": "macOS/Podfile", - "kind": "cocoapods", - "package": f"{distribution}RtcEngine_macOS", - } - ], - "windows": [ - { - "path": "windows/APIExample/install.ps1", - "kind": "archive-name", - "prefix": f"{distribution}_Native_SDK_for_Windows_v", - "suffix": "_FULL.zip", - } - ], - } - contents = { - "Android/APIExample/gradle.properties": "rtc_sdk_version = 4.6.4\n", - "iOS/APIExample/Podfile": f"pod '{distribution}RtcEngine_iOS', '4.6.4'\n", - "macOS/Podfile": f"pod '{distribution}RtcEngine_macOS', '4.6.4'\n", - "windows/APIExample/install.ps1": ( - f"{distribution}_Native_SDK_for_Windows_v4.6.4_FULL.zip\n" - ), - } - for path_text, content in contents.items(): - path = root / path_text - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - profile_path = root / "repository-profile.json" - profile_path.write_text( - json.dumps({"version": 1, "sdk_version_sources": sources}), - encoding="utf-8", - ) - return profile_path - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest.py b/docs/ai-engineering/tools/validate_acceptance_manifest.py deleted file mode 100644 index 0bc726ed3..000000000 --- a/docs/ai-engineering/tools/validate_acceptance_manifest.py +++ /dev/null @@ -1,1516 +0,0 @@ -#!/usr/bin/env python3 -"""Validate cross-platform API Examples acceptance manifests.""" - -import hashlib -import json -import re -import shlex -import sys -from pathlib import Path - -from generate_case_backlog import PLATFORM_PROJECTS, parse_matrix_cell -from prepare_case_execution import collect_sdk_version_checks, load_repository_profile - - -REPO_ROOT = Path(__file__).resolve().parents[3] -MANIFEST_VERSION = 4 -PLATFORMS = ["android", "ios", "macos", "windows"] -PLATFORM_ROOTS = { - "android": "Android/", - "ios": "iOS/", - "macos": "macOS/", - "windows": "windows/", -} -MAIN_PLATFORM_TARGETS = { - "android": "Android/APIExample/", - "ios": "iOS/APIExample/", - "macos": "macOS/", - "windows": "windows/", -} -FINAL_STATUSES = {"PASS", "PASS WITH RISKS", "BLOCKED"} -RESULT_STATUSES = {"PASS", "FAIL", "BLOCKED", "SKIPPED"} -ROLE_STATUSES = {"PASS", "FAIL", "BLOCKED", "WAIVED"} -MATRIX_STATUSES = {"DONE", "HIDDEN", "PARTIAL", "MISSING", "N/A", "UNKNOWN"} -REQUIRED_TOP_LEVEL = { - "version", - "final_status", - "requirement", - "contract", - "platforms", - "cross_platform_acceptance", - "release", - "knowledge_updates", -} -REQUIREMENT_FIELDS = {"feature", "sdk_family", "key_apis", "target_sdk_versions"} -RELEASE_FIELDS = { - "required", - "repository_profile", - "repository_profile_sha256", - "target_sdk_versions", - "checks", - "skipped_checks", -} -CONTRACT_OUTPUT_FIELDS = { - "scenario", - "key_apis", - "non_goals", - "reference", - "cross_platform_requirements", - "platform_targets", -} -IMPLEMENTATION_OUTPUT_FIELDS = { - "target_project", - "query_cases", - "upsert_case", - "files_changed", - "matrix_updates", -} -VERIFICATION_OUTPUT_FIELDS = { - "result", - "findings", - "parity_result", - "entry_point", - "ux_notes", - "commands", - "build_result", - "skipped_checks", -} -DISPATCH_MODES = {"codex-exec", "pending"} -COMMAND_KINDS = {"build", "test", "static", "runtime"} -REASONING_EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh"} -MODEL_PROVENANCE_FIELDS = ["model_profile", "model", "reasoning_effort", "codex_version"] -LEAD_AGENT_IDS = {"lead", "lead-agent", "lead_agent", "main", "main-agent", "main_agent", "coordinator"} -KNOWLEDGE_UPDATE_FIELDS = [ - "source", - "impact_platforms", - "symptom", - "root_cause", - "guardrail", - "verification", - "updated_at", -] -DURABLE_KNOWLEDGE_PATHS = { - "docs/ai-engineering/knowledge-index.md", - "docs/ai-engineering/release-iteration-gate.md", - "docs/ai-engineering/release-known-issues.md", - "docs/ai-engineering/case-maintenance-matrix.md", - "docs/ai-engineering/repository-profile.json", - "docs/ai-engineering/role-routing.json", -} -PLACEHOLDER_RE = re.compile(r"<[^>\n]+>") -SHA256_RE = re.compile(r"^[0-9a-f]{64}$") -SDK_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$") - - -def is_non_empty(value): - return value is not None and value != "" and value != [] - - -def validate_no_placeholders(value, path, errors): - if isinstance(value, str) and PLACEHOLDER_RE.search(value): - errors.append(f"{path} contains placeholder value: {value}") - elif isinstance(value, dict): - for key, child in value.items(): - validate_no_placeholders(child, f"{path}.{key}", errors) - elif isinstance(value, list): - for index, child in enumerate(value): - validate_no_placeholders(child, f"{path}[{index}]", errors) - - -def validate_target_sdk_versions(value, path, errors): - if not isinstance(value, dict): - errors.append(f"{path} must be an object") - return - if set(value) != set(PLATFORMS): - errors.append( - f"{path} must define exactly " + ", ".join(PLATFORMS) - ) - for platform in PLATFORMS: - version = value.get(platform) - if not SDK_VERSION_RE.fullmatch(str(version or "")): - errors.append(f"{path}.{platform} must use x.y.z format") - - -def validate_manifest_shape(manifest, errors): - for key in sorted(REQUIRED_TOP_LEVEL - set(manifest)): - errors.append(f"missing top-level field: {key}") - for key in sorted(set(manifest) - REQUIRED_TOP_LEVEL): - errors.append(f"unsupported top-level field: {key}") - if manifest.get("version") != MANIFEST_VERSION: - errors.append(f"version must be {MANIFEST_VERSION}") - if manifest.get("final_status") not in FINAL_STATUSES: - errors.append(f"final_status must be one of {sorted(FINAL_STATUSES)}") - requirement = manifest.get("requirement") - if not isinstance(requirement, dict): - errors.append("requirement must be an object") - else: - for field in sorted(REQUIREMENT_FIELDS): - if not is_non_empty(requirement.get(field)): - errors.append(f"requirement.{field} is required") - for field in sorted(set(requirement) - REQUIREMENT_FIELDS): - errors.append(f"unsupported requirement field: {field}") - target_sdk_versions = requirement.get("target_sdk_versions") - if is_non_empty(target_sdk_versions): - validate_target_sdk_versions( - target_sdk_versions, "requirement.target_sdk_versions", errors - ) - - -def validate_manifest(manifest): - if not isinstance(manifest, dict): - return ["manifest must be a JSON object"] - errors = [] - seen_agent_ids = {} - validate_no_placeholders(manifest, "manifest", errors) - validate_manifest_shape(manifest, errors) - targets = validate_contract(manifest, errors, seen_agent_ids) - validate_platforms(manifest, targets, errors, seen_agent_ids) - validate_unique_run_ids(manifest, errors) - validate_cross_platform_acceptance(manifest, errors) - validate_release(manifest, errors) - validate_knowledge_updates(manifest, errors) - return errors - - -def validate_contract(manifest, errors, seen_agent_ids): - contract = manifest.get("contract") - output = validate_artifact( - contract, - "contract", - CONTRACT_OUTPUT_FIELDS, - manifest.get("final_status"), - True, - errors, - seen_agent_ids, - ) - if not output: - return {} - for field in ["scenario", "key_apis"]: - if not is_non_empty(output.get(field)): - errors.append(f"contract.output.{field} is required") - requirement_apis = manifest.get("requirement", {}).get("key_apis", []) - contract_apis = output.get("key_apis", []) - if isinstance(requirement_apis, list) and isinstance(contract_apis, list): - missing_apis = [api for api in requirement_apis if api not in contract_apis] - if missing_apis: - errors.append( - "contract.output.key_apis must include every requirement.key_apis value: " - + ", ".join(str(api) for api in missing_apis) - ) - if contract.get("status") == "PASS" and not is_non_empty( - output.get("cross_platform_requirements") - ): - errors.append("contract.output.cross_platform_requirements is required when Contract passes") - reference = output.get("reference") - if not isinstance(reference, dict): - errors.append("contract.output.reference must be an object") - else: - required = bool(reference.get("required")) - source_case = reference.get("source_case") - if required: - if not is_non_empty(source_case): - errors.append("contract.output.reference.source_case is required") - elif "://" not in source_case: - validate_paths([source_case], "contract.output.reference.source_case", errors) - result = reference.get("contract_result") - validate_result(result, "contract.output.reference.contract_result", manifest.get("final_status"), errors) - if required and contract.get("status") == "PASS" and result != "PASS": - errors.append("reference-required work needs contract.output.reference.contract_result=PASS") - - targets = output.get("platform_targets") - if not isinstance(targets, dict): - errors.append("contract.output.platform_targets must be an object") - return {} - for platform in PLATFORMS: - if platform not in targets: - errors.append(f"contract.output.platform_targets missing platform: {platform}") - continue - target = targets[platform] - if not isinstance(target, dict): - errors.append(f"contract.output.platform_targets.{platform} must be an object") - continue - for field in [ - "required", - "target_project", - "key_constraints", - "files_allowed", - "waiver_reason", - ]: - if field not in target: - errors.append(f"contract.output.platform_targets.{platform}.{field} is required") - if target.get("required") is False and not is_non_empty(target.get("waiver_reason")): - errors.append( - f"contract.output.platform_targets.{platform}.waiver_reason is required when the platform is not required" - ) - if contract.get("status") == "PASS" and target.get("required") is True: - if not is_non_empty(target.get("key_constraints")): - errors.append( - f"contract.output.platform_targets.{platform}.key_constraints is required when Contract passes" - ) - if not is_non_empty(target.get("files_allowed")): - errors.append( - f"contract.output.platform_targets.{platform}.files_allowed is required when Contract passes" - ) - target_project = target.get("target_project") - expected_target = MAIN_PLATFORM_TARGETS[platform] - if is_non_empty(target_project): - validate_paths( - [target_project], - f"contract.output.platform_targets.{platform}.target_project", - errors, - ) - if target_project != expected_target: - errors.append( - f"contract.output.platform_targets.{platform}.target_project must be exactly {expected_target}" - ) - validate_paths( - target.get("files_allowed", []), - f"contract.output.platform_targets.{platform}.files_allowed", - errors, - ) - allowed_paths = target.get("files_allowed", []) - for allowed_path in allowed_paths if isinstance(allowed_paths, list) else []: - if not path_is_within(allowed_path, MAIN_PLATFORM_TARGETS[platform]): - errors.append( - f"contract.output.platform_targets.{platform}.files_allowed must stay inside " - f"{MAIN_PLATFORM_TARGETS[platform]}: {allowed_path}" - ) - for platform in sorted(set(targets) - set(PLATFORMS)): - errors.append(f"contract.output.platform_targets has unsupported platform: {platform}") - return targets - - -def validate_platforms(manifest, targets, errors, seen_agent_ids): - platforms = manifest.get("platforms") - if not isinstance(platforms, dict): - errors.append("platforms must be an object") - return - for platform in PLATFORMS: - if platform not in platforms: - errors.append(f"platforms missing required platform: {platform}") - continue - unit = platforms[platform] - if not isinstance(unit, dict): - errors.append(f"platforms.{platform} must be an object") - continue - target = targets.get(platform, {}) if isinstance(targets, dict) else {} - required = bool(target.get("required", True)) - implementation = validate_artifact( - unit.get("implementation"), - f"platforms.{platform}.implementation", - IMPLEMENTATION_OUTPUT_FIELDS, - manifest.get("final_status"), - required, - errors, - seen_agent_ids, - ) - verification = validate_artifact( - unit.get("verification"), - f"platforms.{platform}.verification", - VERIFICATION_OUTPUT_FIELDS, - manifest.get("final_status"), - required, - errors, - seen_agent_ids, - ) - validate_platform_implementation(platform, target, implementation, verification, manifest, errors) - verification_dispatch = ( - unit.get("verification", {}).get("dispatch", {}) - if isinstance(unit.get("verification"), dict) - else {} - ) - validate_platform_verification( - platform, required, verification, verification_dispatch, manifest, errors - ) - for role in ["implementation", "verification"]: - artifact = unit.get(role, {}) - dispatch = artifact.get("dispatch", {}) if isinstance(artifact, dict) else {} - working_directory = dispatch.get("working_directory") - expected_working_directory = target.get("target_project") - if ( - is_non_empty(working_directory) - and expected_working_directory - and normalize_manifest_path(working_directory).rstrip("/") - != normalize_manifest_path(expected_working_directory).rstrip("/") - ): - errors.append( - f"platforms.{platform}.{role}.dispatch.working_directory must match Contract target {expected_working_directory}" - ) - if not required: - for role in ["implementation", "verification"]: - artifact = unit.get(role, {}) - if artifact.get("status") != "WAIVED": - errors.append(f"platforms.{platform}.{role}.status must be WAIVED when platform is not required") - for platform in sorted(set(platforms) - set(PLATFORMS)): - errors.append(f"platforms has unsupported platform: {platform}") - - -def validate_artifact(artifact, path, required_output, final_status, required, errors, seen_agent_ids): - if not isinstance(artifact, dict): - errors.append(f"{path} must be an object") - return {} - status = artifact.get("status") - if status not in ROLE_STATUSES: - errors.append(f"{path}.status must be one of {sorted(ROLE_STATUSES)}") - elif required and status == "WAIVED": - errors.append( - f"{path}.status=WAIVED is only valid when the platform is not required" - ) - elif required and status in {"FAIL", "BLOCKED"} and final_status != "BLOCKED": - errors.append(f"{path}.status={status} requires final_status=BLOCKED") - if status == "WAIVED" and not is_non_empty(artifact.get("waiver_reason")): - errors.append(f"{path}.waiver_reason is required when status is WAIVED") - for field in ["agent_id", "evidence", "summary"]: - if not is_non_empty(artifact.get(field)): - errors.append(f"{path}.{field} is required") - agent_id = artifact.get("agent_id") - if is_non_empty(agent_id): - normalized = str(agent_id).lower() - if normalized in LEAD_AGENT_IDS: - errors.append(f"{path}.agent_id cannot be the Lead Agent") - elif normalized in seen_agent_ids: - errors.append(f"{path}.agent_id duplicates {seen_agent_ids[normalized]}.agent_id") - else: - seen_agent_ids[normalized] = path - validate_dispatch(path, artifact.get("dispatch"), status, final_status, required, errors) - if path.endswith(".implementation") and required and final_status != "BLOCKED": - dispatch = artifact.get("dispatch", {}) - if not is_non_empty(dispatch.get("repository_delta")): - errors.append(f"{path}.dispatch.repository_delta is required") - else: - validate_run_relative_path( - dispatch["repository_delta"], f"{path}.dispatch.repository_delta", errors - ) - value = dispatch.get("repository_delta_sha256") - if not isinstance(value, str) or not SHA256_RE.fullmatch(value): - errors.append(f"{path}.dispatch.repository_delta_sha256 must be a lowercase sha256 hex digest") - output = artifact.get("output") - if not isinstance(output, dict): - errors.append(f"{path}.output must be an object") - return {} - for field in sorted(required_output - set(output)): - errors.append(f"{path}.output.{field} is required") - return output - - -def validate_dispatch(path, dispatch, status, final_status, required, errors): - field_name = f"{path}.dispatch" - if not isinstance(dispatch, dict): - errors.append(f"{field_name} is required") - return - mode = dispatch.get("mode") - if mode not in DISPATCH_MODES: - errors.append(f"{field_name}.mode must be one of {sorted(DISPATCH_MODES)}") - for field in ["prompt", "artifact"]: - if not is_non_empty(dispatch.get(field)): - errors.append(f"{field_name}.{field} is required") - else: - validate_run_relative_path(dispatch[field], f"{field_name}.{field}", errors) - if is_non_empty(dispatch.get("input_snapshot")): - validate_run_relative_path(dispatch["input_snapshot"], f"{field_name}.input_snapshot", errors) - if required and mode == "pending" and final_status != "BLOCKED" and status != "WAIVED": - errors.append(f"{field_name}.mode=pending requires final_status=BLOCKED") - if final_status == "BLOCKED" or status == "WAIVED" or not required: - return - if mode != "codex-exec": - errors.append(f"{field_name}.mode={mode} cannot pass final acceptance") - for field in [ - "run_id", - "evidence", - "input_snapshot", - "working_directory", - "command_log", - "host_platform", - *MODEL_PROVENANCE_FIELDS, - ]: - if not is_non_empty(dispatch.get(field)): - errors.append(f"{field_name}.{field} is required when final_status is not BLOCKED") - if is_non_empty(dispatch.get("working_directory")): - validate_paths( - [dispatch["working_directory"]], f"{field_name}.working_directory", errors - ) - if is_non_empty(dispatch.get("command_log")): - validate_run_relative_path( - dispatch["command_log"], f"{field_name}.command_log", errors - ) - effort = dispatch.get("reasoning_effort") - if is_non_empty(effort) and effort not in REASONING_EFFORTS: - errors.append(f"{field_name}.reasoning_effort must be one of {sorted(REASONING_EFFORTS)}") - for field in [ - "prompt_sha256", - "artifact_sha256", - "input_snapshot_sha256", - "command_log_sha256", - ]: - value = dispatch.get(field) - if not is_non_empty(value): - errors.append(f"{field_name}.{field} is required when final_status is not BLOCKED") - elif not isinstance(value, str) or not SHA256_RE.fullmatch(value): - errors.append(f"{field_name}.{field} must be a lowercase sha256 hex digest") - - -def validate_platform_implementation(platform, target, output, verification, manifest, errors): - if not output: - return - expected_target = target.get("target_project") if isinstance(target, dict) else None - if expected_target and output.get("target_project") != expected_target: - errors.append( - f"platforms.{platform}.implementation.output.target_project must match contract target {expected_target}" - ) - for field in ["query_cases", "upsert_case"]: - if not is_non_empty(output.get(field)): - errors.append(f"platforms.{platform}.implementation.output.{field} is required") - validate_paths( - output.get("files_changed", []), - f"platforms.{platform}.implementation.output.files_changed", - errors, - require_exists=False, - ) - allowed = target.get("files_allowed", []) if isinstance(target, dict) else [] - for changed_path in output.get("files_changed", []): - if not any(path_is_within(changed_path, allowed_path) for allowed_path in allowed): - errors.append( - f"platforms.{platform}.implementation.output.files_changed is outside Contract files_allowed: {changed_path}" - ) - updates = output.get("matrix_updates", []) - if not isinstance(updates, list): - errors.append(f"platforms.{platform}.implementation.output.matrix_updates must be an array") - return - for index, update in enumerate(updates): - path = f"platforms.{platform}.implementation.output.matrix_updates[{index}]" - if not isinstance(update, dict): - errors.append(f"{path} must be an object") - continue - for field in ["feature", "platform_unit", "from", "to", "to_cell", "evidence"]: - if not is_non_empty(update.get(field)): - errors.append(f"{path}.{field} is required") - if update.get("feature") != manifest.get("requirement", {}).get("feature"): - errors.append(f"{path}.feature must match requirement.feature") - target_project = target.get("target_project") if isinstance(target, dict) else "" - expected_units = { - unit - for unit, project in PLATFORM_PROJECTS.items() - if path_is_within(target_project, project) or path_is_within(project, target_project) - } - if update.get("platform_unit") not in expected_units: - errors.append( - f"{path}.platform_unit must match Contract target {target_project}; " - f"expected one of {sorted(expected_units)}" - ) - if update.get("from") not in MATRIX_STATUSES: - errors.append(f"{path}.from must be one of {sorted(MATRIX_STATUSES)}") - if update.get("to") not in MATRIX_STATUSES: - errors.append(f"{path}.to must be one of {sorted(MATRIX_STATUSES)}") - to_cell = update.get("to_cell") - if is_non_empty(to_cell): - cell_status, _ = parse_matrix_cell(to_cell) - if cell_status != update.get("to"): - errors.append(f"{path}.to_cell status must match to={update.get('to')}") - if any(character in str(to_cell) for character in "\n\r|`"): - errors.append(f"{path}.to_cell contains unsupported Markdown characters") - if update.get("to") == "DONE": - if manifest.get("final_status") != "PASS": - errors.append(f"{update.get('platform_unit')} matrix update to DONE requires final_status=PASS") - if verification.get("build_result") != "PASS": - errors.append( - f"{update.get('platform_unit')} matrix update to DONE requires build_result=PASS" - ) - if verification.get("parity_result") != "PASS": - errors.append( - f"{update.get('platform_unit')} matrix update to DONE requires parity_result=PASS" - ) - if verification.get("skipped_checks"): - errors.append(f"{update.get('platform_unit')} matrix update to DONE cannot include skipped checks") - if isinstance(target, dict) and target.get("required") is False: - if output.get("files_changed"): - errors.append(f"platforms.{platform}.implementation.output.files_changed must be empty when platform is not required") - if updates: - errors.append(f"platforms.{platform}.implementation.output.matrix_updates must be empty when platform is not required") - - -def is_platform_build_command(platform, command, working_directory=None): - if ( - platform not in MAIN_PLATFORM_TARGETS - or not isinstance(command, str) - or not is_non_empty(working_directory) - or normalize_manifest_path(working_directory).rstrip("/") - != normalize_manifest_path(MAIN_PLATFORM_TARGETS[platform]).rstrip("/") - ): - return False - try: - outer_tokens = shlex.split(command) - except ValueError: - return False - if not outer_tokens: - return False - executable = outer_tokens[0].replace("\\", "/").rsplit("/", 1)[-1].lower() - if executable in {"sh", "bash", "zsh"}: - command_option = next( - ( - index - for index, token in enumerate(outer_tokens[:-1]) - if token.startswith("-") and "c" in token - ), - None, - ) - if command_option is None: - return False - command = outer_tokens[command_option + 1] - elif executable in {"cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe"}: - command_option = next( - ( - index - for index, token in enumerate(outer_tokens[:-1]) - if token.lower() in {"/c", "-command", "-c"} - ), - None, - ) - if command_option is None: - return False - command = " ".join(outer_tokens[command_option + 1 :]) - - try: - lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|") - lexer.whitespace_split = True - lexer.commenters = "" - tokens = list(lexer) - except ValueError: - return False - segments = [[]] - for token in tokens: - if token in {";", "|", "||", "&"}: - return False - if token == "&&": - if not segments[-1]: - return False - segments.append([]) - else: - segments[-1].append(token) - if not segments[-1]: - return False - if len(segments) != 1: - return False - final = segments[-1] - if command_overrides_path(final): - return False - remaining = command_tokens_without_environment(final) - if not remaining: - return False - raw_executable = remaining[0].replace("\\", "/").lower() - final_executable, arguments = command_executable(final), command_arguments(final) - if platform == "android": - if raw_executable not in {"./gradlew", "gradlew.bat", "./gradlew.bat"}: - return False - normalized_arguments = [argument.lower() for argument in arguments] - blocked_modes = { - "--dry-run", - "-m", - "help", - "tasks", - "properties", - "projects", - "dependencies", - "components", - "model", - "buildenvironment", - } - if blocked_modes.intersection(normalized_arguments) or any( - argument.startswith("--dry-run=") for argument in normalized_arguments - ): - return False - if any( - argument in {"-p", "--project-dir"} or argument.startswith("--project-dir=") - for argument in normalized_arguments - ): - return False - return final_executable in {"gradlew", "gradlew.bat"} and any( - re.search(r"(?:^|:)(?:assemble|build|bundle)[A-Za-z0-9_.-]*$", argument, re.I) - for argument in arguments - ) - if platform in {"ios", "macos"}: - if raw_executable not in {"xcodebuild", "/usr/bin/xcodebuild"}: - return False - normalized_arguments = [argument.lower() for argument in arguments] - if not xcodebuild_has_only_build_arguments(arguments): - return False - for option in ["-project", "-workspace"]: - if option in normalized_arguments: - index = normalized_arguments.index(option) - if index + 1 >= len(arguments) or not is_safe_relative_build_path( - arguments[index + 1] - ): - return False - build_actions = {"build", "archive", "test", "build-for-testing"} - normalized_argument_set = set(normalized_arguments) - if normalized_argument_set & build_actions: - return True - if "clean" in normalized_argument_set: - return False - return bool(normalized_argument_set & {"-project", "-workspace", "-scheme"}) - if final_executable in {"msbuild", "msbuild.exe", "devenv", "devenv.com", "devenv.exe"}: - if "/" in raw_executable: - return False - normalized_arguments = [argument.lower() for argument in arguments] - if any( - argument in {"-version", "/version", "-help", "/help", "/?"} - for argument in normalized_arguments - ): - return False - blocked_prefixes = ( - "/preprocess", - "-preprocess", - "/pp", - "-pp", - "/getproperty", - "-getproperty", - "/getitem", - "-getitem", - "/gettargetresult", - "-gettargetresult", - "/targets", - "-targets", - "/ts", - "-ts", - "/validate", - "-validate", - "/val", - "-val", - ) - if any(argument.startswith(blocked_prefixes) for argument in normalized_arguments): - return False - project_arguments = [ - arguments[index] - for index, argument in enumerate(normalized_arguments) - if argument.endswith((".sln", ".slnx", ".vcxproj", ".proj")) - ] - if not project_arguments or not all( - is_safe_relative_build_path(argument) for argument in project_arguments - ): - return False - if final_executable.startswith("devenv"): - return any( - argument in {"/build", "/rebuild"} for argument in normalized_arguments - ) - targets = [ - argument.split(":", 1)[1] - for argument in normalized_arguments - if argument.startswith(("/t:", "-t:", "/target:", "-target:")) - ] - if targets: - target_names = { - name - for value in targets - for name in re.split(r"[;,]", value) - if name - } - return bool(target_names & {"build", "rebuild"}) - return True - if ( - raw_executable not in {"cmake", "cmake.exe"} - or final_executable not in {"cmake", "cmake.exe"} - or "--build" not in arguments - ): - return False - build_index = arguments.index("--build") - if build_index + 1 >= len(arguments) or not is_safe_relative_build_path( - arguments[build_index + 1] - ): - return False - normalized_arguments = [argument.lower() for argument in arguments] - if "--target" in normalized_arguments: - target_index = normalized_arguments.index("--target") - targets = normalized_arguments[target_index + 1 :] - if targets and all(target in {"clean", "help"} for target in targets): - return False - return True - - -def is_safe_relative_build_path(path_text): - if not isinstance(path_text, str) or not path_text: - return False - normalized = path_text.replace("\\", "/") - path = Path(normalized) - return ( - not path.is_absolute() - and not re.match(r"^[A-Za-z]:/", normalized) - and ".." not in path.parts - ) - - -def xcodebuild_has_only_build_arguments(arguments): - actions = {"build", "archive", "test", "build-for-testing", "clean"} - options_with_values = { - "-project", - "-workspace", - "-scheme", - "-target", - "-configuration", - "-destination", - "-arch", - "-sdk", - "-xcconfig", - "-deriveddatapath", - "-archivepath", - "-resultbundlepath", - "-resultstreampath", - "-packagecachepath", - "-clonedsourcepackagesdirpath", - "-jobs", - "-testplan", - "-only-testing", - "-skip-testing", - "-enablecodecoverage", - "-parallel-testing-enabled", - "-maximum-concurrent-test-device-destinations", - "-maximum-concurrent-test-simulator-destinations", - } - flags = { - "-alltargets", - "-parallelizetargets", - "-quiet", - "-verbose", - "-hideshellscriptenvironment", - "-allowprovisioningupdates", - "-allowprovisioningdeviceregistration", - "-skipunavailableactions", - "-disableautomaticpackageresolution", - "-onlyusepackageversionsfromresolvedfile", - "-skippackageupdates", - "-skippackagepluginvalidation", - "-skipmacrovalidation", - "-showbuildtimingsummary", - } - index = 0 - while index < len(arguments): - argument = arguments[index] - normalized = argument.lower() - if normalized in actions or normalized in flags or ( - "=" in argument and not argument.startswith("-") - ): - index += 1 - continue - if normalized in options_with_values: - if index + 1 >= len(arguments): - return False - index += 2 - continue - return False - return True - - -def command_tokens_without_environment(tokens): - remaining = list(tokens) - if remaining and remaining[0] == "env": - remaining.pop(0) - while remaining and (remaining[0].startswith("-") or "=" in remaining[0]): - remaining.pop(0) - while remaining and "=" in remaining[0] and not remaining[0].startswith(("/", "./")): - remaining.pop(0) - return remaining - - -def command_overrides_path(tokens): - remaining = list(tokens) - if remaining and remaining[0] == "env": - remaining.pop(0) - while remaining and remaining[0].startswith("-"): - remaining.pop(0) - for token in remaining: - if "=" not in token or token.startswith(("/", "./")): - break - name, _, _ = token.partition("=") - if name.upper() == "PATH": - return True - return False - - -def command_executable(tokens): - remaining = command_tokens_without_environment(tokens) - if not remaining: - return "" - return remaining[0].replace("\\", "/").rsplit("/", 1)[-1].lower() - - -def command_arguments(tokens): - remaining = command_tokens_without_environment(tokens) - return remaining[1:] if remaining else [] - - -def validate_platform_verification( - platform, required, output, dispatch, manifest, errors -): - if not output: - return - dispatch = dispatch if isinstance(dispatch, dict) else {} - command_log = dispatch.get("command_log") - host_platform = dispatch.get("host_platform") - for field in ["entry_point", "ux_notes"]: - if required and not is_non_empty(output.get(field)): - errors.append(f"platforms.{platform}.verification.output.{field} is required") - for field in ["result", "parity_result", "build_result"]: - validate_result( - output.get(field), - f"platforms.{platform}.verification.output.{field}", - manifest.get("final_status"), - errors, - allow_skipped=not required, - ) - commands = output.get("commands", []) - if not required: - for field in ["result", "parity_result", "build_result"]: - if output.get(field) != "SKIPPED": - errors.append( - f"platforms.{platform}.verification.output.{field} must be SKIPPED when platform is not required" - ) - if commands: - errors.append( - f"platforms.{platform}.verification.output.commands must be empty when platform is not required" - ) - if required and manifest.get("final_status") != "BLOCKED" and not commands: - errors.append(f"platforms.{platform}.verification.output.commands must not be empty") - for index, command in enumerate(commands): - if not isinstance(command, dict): - errors.append(f"platforms.{platform}.verification.output.commands[{index}] must be an object") - continue - if not command.get("command"): - errors.append(f"platforms.{platform}.verification.output.commands[{index}].command is required") - if command.get("kind") not in COMMAND_KINDS: - errors.append( - f"platforms.{platform}.verification.output.commands[{index}].kind must be one of {sorted(COMMAND_KINDS)}" - ) - elif command.get("kind") == "build" and not is_platform_build_command( - platform, command.get("command"), dispatch.get("working_directory") - ): - errors.append( - f"platforms.{platform}.verification.output.commands[{index}] is not a recognized {platform} build command" - ) - validate_result( - command.get("result"), - f"platforms.{platform}.verification.output.commands[{index}].result", - manifest.get("final_status"), - errors, - ) - if command.get("result") in {"PASS", "FAIL"} and not is_non_empty( - command.get("evidence") - ): - errors.append( - f"platforms.{platform}.verification.output.commands[{index}].evidence is required for executed commands" - ) - elif command.get("result") in {"PASS", "FAIL"} and ( - not command_log - or not str(command.get("evidence")).startswith(f"{command_log}#") - or "exit_code=" not in str(command.get("evidence")) - or "cwd=" not in str(command.get("evidence")) - or "cwd_source=" not in str(command.get("evidence")) - ): - errors.append( - f"platforms.{platform}.verification.output.commands[{index}].evidence must bind " - "to dispatch.command_log, exit_code, cwd, and cwd_source" - ) - elif command.get("result") in {"PASS", "FAIL"}: - evidence_fields = parse_command_evidence( - command.get("evidence"), command_log - ) - evidence_cwd = evidence_fields.get("cwd") - cwd_source = evidence_fields.get("cwd_source") - expected_cwd = dispatch.get("working_directory") - if ( - not evidence_cwd - or normalize_manifest_path(evidence_cwd).rstrip("/") - != normalize_manifest_path(expected_cwd).rstrip("/") - ): - errors.append( - f"platforms.{platform}.verification.output.commands[{index}].evidence cwd " - f"must match dispatch.working_directory {expected_cwd}" - ) - if cwd_source not in {"command_event", "dispatch"}: - errors.append( - f"platforms.{platform}.verification.output.commands[{index}].evidence " - "cwd_source must be command_event or dispatch" - ) - for index, skipped in enumerate(output.get("skipped_checks", [])): - if not isinstance(skipped, dict) or not skipped.get("reason"): - errors.append( - f"platforms.{platform}.verification.output.skipped_checks[{index}].reason is required" - ) - if required and manifest.get("final_status") != "BLOCKED": - for field in ["result", "parity_result", "build_result"]: - if output.get(field) != "PASS": - errors.append( - f"non-BLOCKED acceptance requires platforms.{platform}.verification.output.{field}=PASS" - ) - if output.get("build_result") == "PASS" and not any( - command.get("kind") == "build" and command.get("result") == "PASS" - for command in commands - if isinstance(command, dict) - ): - errors.append( - f"platforms.{platform}.verification.output.build_result=PASS requires a bound PASS build command" - ) - required_build_host = { - "ios": "darwin", - "macos": "darwin", - "windows": "win32", - }.get(platform) - has_pass_build_command = any( - isinstance(command, dict) - and command.get("kind") == "build" - and command.get("result") == "PASS" - for command in commands - ) - if ( - required_build_host - and (output.get("build_result") == "PASS" or has_pass_build_command) - and host_platform != required_build_host - ): - errors.append( - f"platforms.{platform}.verification.output.build_result=PASS requires " - f"host_platform={required_build_host}" - ) - if required and manifest.get("final_status") == "PASS" and output.get("skipped_checks"): - errors.append("final_status=PASS cannot include skipped checks") - - -def validate_cross_platform_acceptance(manifest, errors): - acceptance = manifest.get("cross_platform_acceptance") - if not isinstance(acceptance, dict): - errors.append("cross_platform_acceptance must be an object") - return - result = acceptance.get("result") - validate_result(result, "cross_platform_acceptance.result", manifest.get("final_status"), errors) - if not is_non_empty(acceptance.get("evidence")): - errors.append("cross_platform_acceptance.evidence is required") - if manifest.get("final_status") != "BLOCKED" and result != "PASS": - errors.append("non-BLOCKED acceptance requires cross_platform_acceptance.result=PASS") - - -def validate_result(result, field_name, final_status, errors, allow_skipped=False): - if result not in RESULT_STATUSES: - errors.append(f"{field_name} must be one of {sorted(RESULT_STATUSES)}") - elif result in {"FAIL", "BLOCKED"} and final_status != "BLOCKED": - errors.append(f"{field_name}={result} requires final_status=BLOCKED") - elif result == "SKIPPED" and final_status == "PASS" and not allow_skipped: - errors.append(f"{field_name}=SKIPPED cannot use final_status=PASS") - - -def validate_release(manifest, errors): - release = manifest.get("release") - if not isinstance(release, dict): - errors.append("release must be an object") - return - if release.get("required") is not True: - errors.append("release.required must be true") - for field in sorted(set(release) - RELEASE_FIELDS): - errors.append(f"unsupported release field: {field}") - profile_value = release.get("repository_profile") - if not isinstance(profile_value, str) or not profile_value: - errors.append("release.repository_profile is required") - else: - profile_path = Path(profile_value) - if profile_path.is_absolute() or ".." in profile_path.parts: - errors.append("release.repository_profile must be repository-relative") - if not SHA256_RE.fullmatch(str(release.get("repository_profile_sha256") or "")): - errors.append("release.repository_profile_sha256 must be a SHA-256 hash") - requirement = manifest.get("requirement", {}) - target_sdk_versions = requirement.get("target_sdk_versions") - release_target_sdk_versions = release.get("target_sdk_versions") - if is_non_empty(release_target_sdk_versions): - validate_target_sdk_versions( - release_target_sdk_versions, "release.target_sdk_versions", errors - ) - if release_target_sdk_versions != target_sdk_versions: - errors.append( - "release.target_sdk_versions must match requirement.target_sdk_versions" - ) - checks = release.get("checks", []) - if not checks: - errors.append("release.checks must include SDK version checks") - check_names = { - check.get("name") for check in checks if isinstance(check, dict) and check.get("name") - } - expected_sdk_checks = {f"sdk-version-{platform}" for platform in PLATFORMS} - for name in sorted(expected_sdk_checks - check_names): - errors.append(f"release.checks missing required check: {name}") - for index, check in enumerate(checks): - if not isinstance(check, dict): - errors.append(f"release.checks[{index}] must be an object") - continue - if not check.get("name"): - errors.append(f"release.checks[{index}].name is required") - result = check.get("result") - validate_result(result, f"release.checks[{index}].result", manifest.get("final_status"), errors) - if result == "PASS" and not check.get("evidence"): - errors.append(f"release.checks[{index}].evidence is required when result is PASS") - if result == "SKIPPED" and not check.get("reason"): - errors.append(f"release.checks[{index}].reason is required when result is SKIPPED") - if result in {"FAIL", "BLOCKED"} and not check.get("reason"): - errors.append(f"release.checks[{index}].reason is required when result is {result}") - if check.get("name") in expected_sdk_checks: - platform = check["name"].removeprefix("sdk-version-") - target_sdk_version = ( - target_sdk_versions.get(platform) - if isinstance(target_sdk_versions, dict) - else None - ) - if target_sdk_version and check.get("expected_version") != target_sdk_version: - errors.append( - f"{check.get('name')} expected_version must match {target_sdk_version}" - ) - actual_versions = check.get("actual_versions") - if not isinstance(actual_versions, dict) or not actual_versions: - errors.append(f"{check.get('name')} actual_versions must be a non-empty object") - elif target_sdk_version and result == "PASS" and any( - version != target_sdk_version for version in actual_versions.values() - ): - errors.append( - f"{check.get('name')} actual versions must all match {target_sdk_version}" - ) - if manifest.get("final_status") != "BLOCKED" and result != "PASS": - errors.append( - f"non-BLOCKED acceptance requires {check.get('name')}=PASS" - ) - for index, skipped in enumerate(release.get("skipped_checks", [])): - if not isinstance(skipped, dict) or not skipped.get("reason"): - errors.append(f"release.skipped_checks[{index}].reason is required") - if manifest.get("final_status") != "BLOCKED" and release.get("skipped_checks"): - errors.append("non-BLOCKED acceptance cannot include skipped release checks") - - -def validate_repository_release(manifest, repo_root=REPO_ROOT): - errors = [] - release = manifest.get("release") - if not isinstance(release, dict): - return errors - profile_value = release.get("repository_profile") - profile_sha256 = release.get("repository_profile_sha256") - if not isinstance(profile_value, str) or not profile_value: - return errors - relative_path = Path(profile_value) - if relative_path.is_absolute() or ".." in relative_path.parts: - return errors - root = Path(repo_root).resolve() - profile_path = (root / relative_path).resolve() - try: - profile_path.relative_to(root) - except ValueError: - errors.append("release.repository_profile must resolve inside the repository") - return errors - if not profile_path.is_file(): - errors.append(f"release.repository_profile does not exist: {profile_value}") - return errors - actual_sha256 = hashlib.sha256(profile_path.read_bytes()).hexdigest() - if profile_sha256 != actual_sha256: - errors.append("release.repository_profile_sha256 does not match repository profile") - return errors - try: - load_repository_profile(profile_path) - expected_checks = collect_sdk_version_checks( - release.get("target_sdk_versions", {}), - repo_root=root, - profile_path=profile_path, - ) - except (OSError, ValueError, KeyError) as exc: - errors.append(f"failed to evaluate release.repository_profile: {exc}") - return errors - if release.get("checks") != expected_checks: - errors.append( - "release.checks must match live repository SDK checks from the bound repository profile" - ) - return errors - -def validate_knowledge_updates(manifest, errors): - updates = manifest.get("knowledge_updates", []) - for index, update in enumerate(updates): - if not isinstance(update, dict): - errors.append(f"knowledge_updates[{index}] must be an object") - continue - for field in KNOWLEDGE_UPDATE_FIELDS: - if not is_non_empty(update.get(field)): - errors.append(f"knowledge_updates[{index}].{field} is required") - changed_files = [] - platforms = manifest.get("platforms", {}) - if isinstance(platforms, dict): - for platform in PLATFORMS: - implementation = platforms.get(platform, {}).get("implementation", {}) - output = implementation.get("output", {}) if isinstance(implementation, dict) else {} - changed_files.extend(output.get("files_changed", []) if isinstance(output, dict) else []) - if updates and not any(is_durable_knowledge_path(path) for path in changed_files): - errors.append("knowledge_updates require at least one durable knowledge doc or skill in files_changed") - - -def validate_run_relative_path(path_text, field_name, errors): - if not isinstance(path_text, str): - errors.append(f"{field_name} must be a string") - return - path = Path(path_text) - if path.is_absolute() or ".." in path.parts: - errors.append(f"{field_name} must be relative to the execution workspace") - - -def validate_unique_run_ids(manifest, errors): - seen = {} - for artifact_path, artifact in iter_manifest_artifacts(manifest): - dispatch = artifact.get("dispatch") - if not isinstance(dispatch, dict) or dispatch.get("mode") != "codex-exec": - continue - run_id = dispatch.get("run_id") - if not is_non_empty(run_id): - continue - normalized = str(run_id).lower() - if normalized in seen: - errors.append( - f"{artifact_path}.dispatch.run_id duplicates {seen[normalized]}.dispatch.run_id" - ) - else: - seen[normalized] = artifact_path - - -def resolve_repo_path(path_text): - path = Path(path_text) - resolved = path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve() - try: - resolved.relative_to(REPO_ROOT.resolve()) - except ValueError: - return None - return resolved - - -def validate_paths(values, field_name, errors, require_exists=True): - if not isinstance(values, list): - errors.append(f"{field_name} must be an array") - return - for path_text in values: - if not isinstance(path_text, str) or not path_text: - errors.append(f"{field_name} contains an empty or non-string path") - continue - if "://" in path_text: - continue - resolved = resolve_repo_path(path_text) - if resolved is None: - errors.append(f"{field_name} path must be inside the repository: {path_text}") - elif require_exists and not resolved.exists(): - errors.append(f"{field_name} path does not exist: {path_text}") - - -def is_durable_knowledge_path(path_text): - if not isinstance(path_text, str): - return False - normalized = normalize_manifest_path(path_text) - return ( - normalized in DURABLE_KNOWLEDGE_PATHS - or normalized.endswith("/ARCHITECTURE.md") - or normalized.startswith(".agents/skills/") - or "/.agents/skills/" in normalized - ) - - -def normalize_manifest_path(path_text): - normalized = str(path_text).replace("\\", "/") - path = Path(normalized) - if path.is_absolute(): - try: - return path.resolve().relative_to(REPO_ROOT).as_posix() - except ValueError: - return path.as_posix() - try: - return (REPO_ROOT / path).resolve().relative_to(REPO_ROOT.resolve()).as_posix() - except ValueError: - return path.as_posix() - - -def path_is_within(path_text, allowed_text): - path = normalize_manifest_path(path_text).rstrip("/") - allowed = normalize_manifest_path(allowed_text).rstrip("/") - return path == allowed or path.startswith(f"{allowed}/") - - -def parse_command_evidence(evidence, command_log): - if not isinstance(evidence, str) or not isinstance(command_log, str): - return {} - prefix = f"{command_log}#" - if not evidence.startswith(prefix): - return {} - event_id, separator, details = evidence[len(prefix) :].partition("; ") - if not separator or not event_id: - return {} - fields = {"event_id": event_id} - for detail in details.split("; "): - key, equals, value = detail.partition("=") - if equals and key: - fields[key] = value - return fields - - -def extract_command_events(jsonl): - events = [] - for line in jsonl.splitlines(): - try: - event = json.loads(line) - except json.JSONDecodeError: - continue - item = event.get("item") - if event.get("type") != "item.completed" or not isinstance(item, dict): - continue - if item.get("type") != "command_execution" or not isinstance(item.get("exit_code"), int): - continue - events.append( - { - "id": str(item.get("id") or f"command-{len(events)}"), - "command": item.get("command"), - "exit_code": item["exit_code"], - "cwd": item.get("cwd") or item.get("working_directory"), - } - ) - return events - - -def normalize_command(command): - if isinstance(command, list): - command = " ".join(str(part) for part in command) - command = str(command or "") - try: - tokens = shlex.split(command) - except ValueError: - return " ".join(command.split()) - if not tokens: - return "" - executable = tokens[0].replace("\\", "/").rsplit("/", 1)[-1].lower() - if executable in {"sh", "bash", "zsh"}: - command_option = next( - ( - index - for index, token in enumerate(tokens[:-1]) - if token.startswith("-") and "c" in token - ), - None, - ) - if command_option is not None: - command = tokens[command_option + 1] - elif executable in { - "cmd", - "cmd.exe", - "powershell", - "powershell.exe", - "pwsh", - "pwsh.exe", - }: - command_option = next( - ( - index - for index, token in enumerate(tokens[:-1]) - if token.lower() in {"/c", "-command", "-c"} - ), - None, - ) - if command_option is not None: - command = " ".join(tokens[command_option + 1 :]) - return " ".join(command.split()) - - -def command_event_cwd_matches(event_cwd, working_directory): - if not isinstance(event_cwd, str) or not event_cwd.strip(): - return False - normalized_cwd = event_cwd.replace("\\", "/").rstrip("/") - normalized_expected = normalize_manifest_path(working_directory).rstrip("/") - if normalized_cwd == normalized_expected: - return True - return normalized_cwd.endswith(f"/{normalized_expected}") - - -def iter_manifest_artifacts(manifest): - contract = manifest.get("contract") - if isinstance(contract, dict): - yield "contract", contract - platforms = manifest.get("platforms") - if not isinstance(platforms, dict): - return - for platform in PLATFORMS: - unit = platforms.get(platform) - if not isinstance(unit, dict): - continue - for role in ["implementation", "verification"]: - artifact = unit.get(role) - if isinstance(artifact, dict): - yield f"platforms.{platform}.{role}", artifact - - -def sha256_file(path): - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def resolve_evidence_path(root, path_text): - if not isinstance(path_text, str): - return None - path = Path(path_text) - if path.is_absolute() or ".." in path.parts: - return None - resolved = (root / path).resolve() - try: - resolved.relative_to(root.resolve()) - except ValueError: - return None - return resolved - - -def validate_evidence_files(manifest, evidence_root): - errors = [] - root = Path(evidence_root).resolve() - fields = [ - ("prompt", "prompt_sha256"), - ("artifact", "artifact_sha256"), - ("input_snapshot", "input_snapshot_sha256"), - ("command_log", "command_log_sha256"), - ("repository_delta", "repository_delta_sha256"), - ] - for artifact_path, artifact in iter_manifest_artifacts(manifest): - dispatch = artifact.get("dispatch") - if not isinstance(dispatch, dict) or dispatch.get("mode") != "codex-exec": - continue - for path_field, hash_field in fields: - path_text = dispatch.get(path_field) - expected = dispatch.get(hash_field) - if not is_non_empty(path_text) or not is_non_empty(expected): - continue - path = resolve_evidence_path(root, path_text) - field_name = f"{artifact_path}.dispatch.{path_field}" - if path is None: - errors.append(f"{field_name} must resolve inside the execution workspace") - elif not path.is_file(): - errors.append(f"{field_name} does not exist: {path_text}") - elif sha256_file(path) != expected: - errors.append( - f"{artifact_path}.dispatch.{hash_field} does not match {path_text}" - ) - elif path_field == "command_log" and artifact_path.endswith(".verification"): - validate_command_log_evidence( - artifact_path, artifact, path, errors - ) - elif path_field == "repository_delta": - try: - delta = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - errors.append(f"{field_name} is not valid JSON: {exc}") - continue - if not isinstance(delta, dict): - errors.append(f"{field_name} must contain a JSON object") - continue - delta_files = { - normalize_manifest_path(value) - for value in delta.get("changed_files", []) - if isinstance(value, str) - } - output = artifact.get("output", {}) - artifact_files = { - normalize_manifest_path(value) - for value in output.get("files_changed", []) - if isinstance(value, str) - } - if delta_files != artifact_files: - errors.append( - f"{artifact_path}.dispatch.repository_delta changed_files do not match output.files_changed" - ) - return errors - - -def validate_command_log_evidence(artifact_path, artifact, log_path, errors): - try: - events = extract_command_events(log_path.read_text(encoding="utf-8")) - except OSError as exc: - errors.append(f"{artifact_path}.dispatch.command_log cannot be read: {exc}") - return - dispatch = artifact.get("dispatch", {}) - command_log = dispatch.get("command_log") - working_directory = dispatch.get("working_directory") - output = artifact.get("output", {}) - commands = output.get("commands", []) if isinstance(output, dict) else [] - used = set() - for index, command in enumerate(commands): - if not isinstance(command, dict) or command.get("result") not in {"PASS", "FAIL"}: - continue - field_name = f"{artifact_path}.output.commands[{index}].evidence" - fields = parse_command_evidence(command.get("evidence"), command_log) - if not fields: - errors.append(f"{field_name} is not valid bound command evidence") - continue - candidates = [ - event_index - for event_index, event in enumerate(events) - if event_index not in used - and event["id"] == fields.get("event_id") - and normalize_command(event["command"]) - == normalize_command(command.get("command")) - ] - if not candidates: - errors.append(f"{field_name} has no matching command_execution event") - continue - match_index = next( - ( - event_index - for event_index in candidates - if events[event_index].get("cwd") - and command_event_cwd_matches( - events[event_index]["cwd"], working_directory - ) - ), - None, - ) - if match_index is None: - match_index = next( - ( - event_index - for event_index in candidates - if not events[event_index].get("cwd") - ), - None, - ) - if match_index is None: - errors.append( - f"{field_name} command_execution cwd must match " - f"dispatch.working_directory {working_directory}" - ) - continue - event = events[match_index] - used.add(match_index) - expected_cwd_source = "command_event" if event.get("cwd") else "dispatch" - if fields.get("cwd_source") != expected_cwd_source: - errors.append( - f"{field_name} cwd_source must be {expected_cwd_source} for its " - "command_execution event" - ) - if fields.get("exit_code") != str(event["exit_code"]): - errors.append(f"{field_name} exit_code does not match command_execution event") - actual_result = "PASS" if event["exit_code"] == 0 else "FAIL" - if command.get("result") != actual_result: - errors.append(f"{field_name} result does not match command_execution exit_code") - - -def main(argv): - if len(argv) != 2: - print("usage: validate_acceptance_manifest.py ", file=sys.stderr) - return 2 - manifest_path = Path(argv[1]) - try: - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - except OSError as exc: - print(f"failed to read manifest: {exc}", file=sys.stderr) - return 2 - except json.JSONDecodeError as exc: - print(f"invalid JSON: {exc}", file=sys.stderr) - return 2 - errors = validate_manifest(manifest) - errors.extend(validate_repository_release(manifest)) - errors.extend(validate_evidence_files(manifest, manifest_path.parent)) - if errors: - for error in errors: - print(f"ERROR: {error}", file=sys.stderr) - return 1 - print("Acceptance manifest valid") - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py b/docs/ai-engineering/tools/validate_acceptance_manifest_test.py deleted file mode 100644 index 678abf118..000000000 --- a/docs/ai-engineering/tools/validate_acceptance_manifest_test.py +++ /dev/null @@ -1,960 +0,0 @@ -import copy -import hashlib -import json -import sys -import tempfile -import unittest -from pathlib import Path - - -TOOLS_DIR = Path(__file__).resolve().parent -if str(TOOLS_DIR) not in sys.path: - sys.path.insert(0, str(TOOLS_DIR)) - -from validate_acceptance_manifest import ( - is_durable_knowledge_path, - validate_command_log_evidence, - validate_evidence_files, - validate_manifest, - validate_repository_release, -) -from prepare_case_execution import collect_sdk_version_checks - - -REPO_ROOT = Path(__file__).resolve().parents[3] -PLATFORMS = ["android", "ios", "macos", "windows"] -TARGETS = { - "android": "Android/APIExample/", - "ios": "iOS/APIExample/", - "macos": "macOS/", - "windows": "windows/", -} -SNAPSHOT_SHA256 = "c" * 64 -BUILD_COMMANDS = { - "android": "./gradlew assembleDebug", - "ios": "xcodebuild -scheme APIExample build", - "macos": "xcodebuild -scheme APIExample build", - "windows": "msbuild APIExample.sln /t:Build", -} - - -def dispatch_for(name, profile): - platform = name.split("-")[0] - dispatch = { - "mode": "codex-exec", - "run_id": f"run-{name}", - "prompt": f"role-prompts/{name}.md", - "prompt_sha256": "a" * 64, - "artifact": f"role-artifacts/{name}.json", - "artifact_sha256": "b" * 64, - "evidence": f"Independent {name} run.", - "model_profile": profile, - "model": "test-model", - "reasoning_effort": "high" if profile in {"deep", "review"} else "medium", - "codex_version": "codex-cli test", - "input_snapshot": f"input-snapshots/{name}.json", - "input_snapshot_sha256": SNAPSHOT_SHA256, - "command_log": f"dispatch-logs/{name}.jsonl", - "command_log_sha256": "d" * 64, - "working_directory": TARGETS.get(name.split("-")[0], "."), - "host_platform": "win32" if platform == "windows" else "darwin", - } - if name.endswith("-implementation"): - dispatch["repository_delta"] = f"repository-deltas/{name}.json" - dispatch["repository_delta_sha256"] = "e" * 64 - return dispatch - - -def artifact(name, profile, output): - return { - "agent_id": f"{name}-agent-1", - "dispatch": dispatch_for(name, profile), - "status": "PASS", - "evidence": f"{name} evidence", - "summary": f"{name} completed", - "output": output, - } - - -def base_manifest(): - platform_targets = { - platform: { - "required": True, - "target_project": target, - "key_constraints": [f"Keep changes in {target}"], - "files_allowed": [target], - "waiver_reason": "", - } - for platform, target in TARGETS.items() - } - platforms = {} - for platform, target in TARGETS.items(): - platforms[platform] = { - "implementation": artifact( - f"{platform}-implementation", - "deep", - { - "target_project": target, - "query_cases": "No duplicate case found.", - "upsert_case": "Platform case updated.", - "files_changed": [f"{target}AGENTS.md"], - "matrix_updates": [], - }, - ), - "verification": artifact( - f"{platform}-verification", - "review", - { - "result": "PASS", - "findings": ["No blocking findings."], - "parity_result": "PASS", - "entry_point": "Basic > Join channel audio", - "ux_notes": "Matches adjacent examples.", - "commands": [ - { - "kind": "build", - "command": BUILD_COMMANDS[platform], - "result": "PASS", - "evidence": ( - f"dispatch-logs/{platform}-verification.jsonl#build; " - f"exit_code=0; cwd={target}; cwd_source=command_event" - ), - } - ], - "build_result": "PASS", - "skipped_checks": [], - }, - ), - } - return { - "version": 4, - "final_status": "PASS", - "requirement": { - "feature": "Join channel audio", - "sdk_family": "Full RTC", - "key_apis": ["joinChannel", "setAudioProfile"], - "target_sdk_versions": {platform: "4.6.4" for platform in PLATFORMS}, - }, - "contract": artifact( - "contract", - "standard", - { - "scenario": "Keep the official audio-only join sample aligned on every platform.", - "key_apis": ["joinChannel", "setAudioProfile"], - "non_goals": ["Release packaging"], - "reference": { - "required": True, - "source_case": "Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java", - "contract_result": "PASS", - }, - "cross_platform_requirements": ["Equivalent join, leave, mute, and status behavior"], - "platform_targets": platform_targets, - }, - ), - "platforms": platforms, - "cross_platform_acceptance": { - "result": "PASS", - "evidence": "All four required platform verification artifacts passed.", - "differences": [], - }, - "release": { - "required": True, - "repository_profile": "docs/ai-engineering/repository-profile.json", - "repository_profile_sha256": hashlib.sha256( - (REPO_ROOT / "docs/ai-engineering/repository-profile.json").read_bytes() - ).hexdigest(), - "target_sdk_versions": {platform: "4.6.4" for platform in PLATFORMS}, - "checks": [ - { - "name": f"sdk-version-{platform}", - "result": "PASS", - "expected_version": "4.6.4", - "actual_versions": {f"{platform}/version-source": "4.6.4"}, - "evidence": f"{platform} SDK sources match 4.6.4.", - "reason": "", - } - for platform in PLATFORMS - ], - "skipped_checks": [], - }, - "knowledge_updates": [], - } - - -class AcceptanceManifestValidatorTest(unittest.TestCase): - def assert_error_contains(self, manifest, expected): - errors = validate_manifest(manifest) - self.assertTrue(any(expected in error for error in errors), errors) - - def test_accepts_multi_platform_v4_manifest(self): - self.assertEqual(validate_manifest(base_manifest()), []) - - def test_rejects_legacy_single_role_tree(self): - manifest = base_manifest() - manifest["roles"] = {} - - self.assert_error_contains(manifest, "unsupported top-level field: roles") - - def test_requires_all_official_platform_units(self): - manifest = base_manifest() - del manifest["platforms"]["windows"] - - self.assert_error_contains(manifest, "platforms missing required platform: windows") - - def test_required_platform_blocker_forces_global_blocked(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["verification"]["status"] = "BLOCKED" - - self.assert_error_contains( - manifest, "platforms.windows.verification.status=BLOCKED requires final_status=BLOCKED" - ) - - def test_required_role_cannot_be_waived(self): - manifest = base_manifest() - artifact = manifest["platforms"]["android"]["verification"] - artifact["status"] = "WAIVED" - artifact["waiver_reason"] = "No Android worker." - - self.assert_error_contains( - manifest, - "platforms.android.verification.status=WAIVED is only valid when the platform is not required", - ) - - def test_blocked_manifest_accepts_pending_windows_verification(self): - manifest = base_manifest() - manifest["final_status"] = "BLOCKED" - manifest["cross_platform_acceptance"] = { - "result": "BLOCKED", - "evidence": "Windows-host verification is pending.", - "differences": [], - } - verification = manifest["platforms"]["windows"]["verification"] - verification.update( - { - "agent_id": "windows-verification-agent-pending", - "dispatch": { - "mode": "pending", - "prompt": "role-prompts/windows-verification.md", - "artifact": "role-artifacts/windows-verification.json", - }, - "status": "BLOCKED", - "evidence": "Windows target build requires a Windows host.", - "summary": "Windows verification pending.", - "output": { - "result": "BLOCKED", - "findings": [], - "parity_result": "BLOCKED", - "entry_point": "Pending Windows verification.", - "ux_notes": "Pending.", - "commands": [], - "build_result": "BLOCKED", - "skipped_checks": [ - {"name": "Windows MSBuild", "reason": "Current host is macOS."} - ], - }, - } - ) - - self.assertEqual(validate_manifest(manifest), []) - - def test_non_required_platform_accepts_explicit_waiver(self): - manifest = base_manifest() - target = manifest["contract"]["output"]["platform_targets"]["windows"] - target["required"] = False - target["waiver_reason"] = "The requested SDK family is not available on Windows." - for role in ["implementation", "verification"]: - artifact = manifest["platforms"]["windows"][role] - artifact["status"] = "WAIVED" - artifact["waiver_reason"] = target["waiver_reason"] - artifact["dispatch"] = { - "mode": "pending", - "prompt": f"role-prompts/windows-{role}.md", - "artifact": f"role-artifacts/windows-{role}.json", - } - implementation = manifest["platforms"]["windows"]["implementation"]["output"] - implementation["files_changed"] = [] - implementation["matrix_updates"] = [] - verification = manifest["platforms"]["windows"]["verification"]["output"] - verification["result"] = "SKIPPED" - verification["parity_result"] = "SKIPPED" - verification["build_result"] = "SKIPPED" - verification["commands"] = [] - verification["skipped_checks"] = [] - - self.assertEqual(validate_manifest(manifest), []) - - def test_cross_platform_acceptance_must_pass_for_non_blocked_result(self): - manifest = base_manifest() - manifest["cross_platform_acceptance"]["result"] = "BLOCKED" - - self.assert_error_contains(manifest, "non-BLOCKED acceptance requires cross_platform_acceptance.result=PASS") - - def test_contract_and_implementation_target_must_match(self): - manifest = base_manifest() - manifest["platforms"]["ios"]["implementation"]["output"]["target_project"] = "iOS/APIExample-OC/" - - self.assert_error_contains( - manifest, "platforms.ios.implementation.output.target_project must match contract target" - ) - - def test_contract_must_cover_requirement_key_apis(self): - manifest = base_manifest() - manifest["contract"]["output"]["key_apis"] = ["joinChannel"] - - self.assert_error_contains( - manifest, - "contract.output.key_apis must include every requirement.key_apis value: setAudioProfile", - ) - - def test_passing_contract_requires_cross_platform_constraints(self): - manifest = base_manifest() - manifest["contract"]["output"]["cross_platform_requirements"] = [] - - self.assert_error_contains( - manifest, - "contract.output.cross_platform_requirements is required when Contract passes", - ) - - def test_implementation_requires_query_and_upsert_results(self): - manifest = base_manifest() - manifest["platforms"]["android"]["implementation"]["output"]["query_cases"] = "" - - self.assert_error_contains( - manifest, - "platforms.android.implementation.output.query_cases is required", - ) - - def test_required_verification_requires_entry_point_and_ux_notes(self): - manifest = base_manifest() - manifest["platforms"]["ios"]["verification"]["output"]["entry_point"] = "" - - self.assert_error_contains( - manifest, - "platforms.ios.verification.output.entry_point is required", - ) - - def test_duplicate_agent_ids_across_platforms_are_rejected(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["verification"]["agent_id"] = manifest["platforms"]["ios"][ - "verification" - ]["agent_id"] - - self.assert_error_contains(manifest, "duplicates platforms.ios.verification.agent_id") - - def test_duplicate_codex_run_ids_across_roles_are_rejected(self): - manifest = base_manifest() - implementation_run_id = manifest["platforms"]["ios"]["implementation"]["dispatch"][ - "run_id" - ] - manifest["platforms"]["ios"]["verification"]["dispatch"][ - "run_id" - ] = implementation_run_id - - self.assert_error_contains( - manifest, - "platforms.ios.verification.dispatch.run_id duplicates platforms.ios.implementation.dispatch.run_id", - ) - - def test_pass_rejects_skipped_platform_checks(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["verification"]["output"]["skipped_checks"] = [ - {"name": "runtime", "reason": "No device"} - ] - - self.assert_error_contains(manifest, "final_status=PASS cannot include skipped checks") - - def test_done_matrix_update_requires_platform_build_pass(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ - { - "feature": "Join channel audio", - "platform_unit": "Windows", - "from": "MISSING", - "to": "DONE", - "to_cell": "DONE(Basic/JoinChannelAudio/)", - "evidence": "Implemented.", - } - ] - manifest["platforms"]["windows"]["verification"]["output"]["build_result"] = "SKIPPED" - - self.assert_error_contains(manifest, "Windows matrix update to DONE requires build_result=PASS") - - def test_hidden_matrix_update_is_allowed(self): - manifest = base_manifest() - manifest["platforms"]["android"]["implementation"]["output"]["matrix_updates"] = [ - { - "feature": "Join channel audio", - "platform_unit": "Android full", - "from": "DONE", - "to": "HIDDEN", - "to_cell": "HIDDEN(basic/JoinChannelAudio.java)", - "evidence": "The retained case is intentionally hidden.", - } - ] - - self.assertEqual(validate_manifest(manifest), []) - - def test_matrix_update_must_match_current_requirement(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ - { - "feature": "Media metadata", - "platform_unit": "Windows", - "from": "MISSING", - "to": "PARTIAL", - "to_cell": "PARTIAL(pending)", - "evidence": "Unrelated update.", - } - ] - - self.assert_error_contains( - manifest, - "platforms.windows.implementation.output.matrix_updates[0].feature must match requirement.feature", - ) - - def test_matrix_update_must_match_contract_target_project(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["implementation"]["output"]["matrix_updates"] = [ - { - "feature": "Join channel audio", - "platform_unit": "Android full", - "from": "MISSING", - "to": "PARTIAL", - "to_cell": "PARTIAL(wrong platform)", - "evidence": "Wrong platform update.", - } - ] - - self.assert_error_contains( - manifest, - "platforms.windows.implementation.output.matrix_updates[0].platform_unit must match Contract target", - ) - - def test_matrix_update_cell_status_must_match_structured_status(self): - manifest = base_manifest() - manifest["final_status"] = "PASS WITH RISKS" - manifest["platforms"]["android"]["verification"]["output"]["skipped_checks"] = [ - {"name": "device-smoke", "reason": "Device unavailable."} - ] - manifest["platforms"]["android"]["implementation"]["output"]["matrix_updates"] = [ - { - "feature": "Join channel audio", - "platform_unit": "Android full", - "from": "MISSING", - "to": "PARTIAL", - "to_cell": "DONE(fake/path)", - "evidence": "Runtime smoke is pending.", - } - ] - - self.assert_error_contains( - manifest, - "platforms.android.implementation.output.matrix_updates[0].to_cell status must match to=PARTIAL", - ) - - def test_release_cannot_be_disabled(self): - manifest = base_manifest() - manifest["release"]["required"] = False - - self.assert_error_contains(manifest, "release.required must be true") - - def test_rejects_removed_publication_fields(self): - manifest = base_manifest() - manifest["requirement"]["publication_channel"] = "legacy" - manifest["release"]["publication"] = {} - - self.assert_error_contains(manifest, "unsupported requirement field: publication_channel") - self.assert_error_contains(manifest, "unsupported release field: publication") - - def test_release_target_versions_must_match_requirement(self): - manifest = base_manifest() - manifest["release"]["target_sdk_versions"]["android"] = "4.6.3" - - self.assert_error_contains( - manifest, "release.target_sdk_versions must match requirement.target_sdk_versions" - ) - - def test_target_sdk_versions_require_all_platform_semver_triplets(self): - manifest = base_manifest() - manifest["requirement"]["target_sdk_versions"]["android"] = "next" - manifest["release"]["target_sdk_versions"]["android"] = "next" - - self.assert_error_contains( - manifest, - "requirement.target_sdk_versions.android must use x.y.z format", - ) - - manifest = base_manifest() - manifest["requirement"]["target_sdk_versions"]["android"] = "" - manifest["release"]["target_sdk_versions"]["android"] = "" - self.assert_error_contains( - manifest, - "requirement.target_sdk_versions.android must use x.y.z format", - ) - - manifest = base_manifest() - del manifest["requirement"]["target_sdk_versions"]["windows"] - del manifest["release"]["target_sdk_versions"]["windows"] - self.assert_error_contains( - manifest, - "requirement.target_sdk_versions must define exactly android, ios, macos, windows", - ) - - manifest = base_manifest() - manifest["requirement"]["target_sdk_versions"]["linux"] = "4.6.4" - manifest["release"]["target_sdk_versions"]["linux"] = "4.6.4" - self.assert_error_contains( - manifest, - "requirement.target_sdk_versions must define exactly android, ios, macos, windows", - ) - - def test_release_sdk_check_requires_all_sources_to_match_target(self): - manifest = base_manifest() - check = manifest["release"]["checks"][0] - check["actual_versions"]["android/version-source"] = "4.6.3" - - self.assert_error_contains(manifest, "sdk-version-android actual versions must all match 4.6.4") - - def test_blocked_external_sdk_check_is_valid_only_for_blocked_acceptance(self): - manifest = base_manifest() - manifest["final_status"] = "BLOCKED" - ios = next( - check - for check in manifest["release"]["checks"] - if check["name"] == "sdk-version-ios" - ) - ios.update( - { - "result": "BLOCKED", - "actual_versions": {"iOS/APIExample/Podfile": ""}, - "evidence": "", - "reason": "The RTC SDK is injected outside tracked repository files.", - } - ) - - self.assertEqual(validate_manifest(manifest), []) - - for final_status in ["PASS", "PASS WITH RISKS"]: - with self.subTest(final_status=final_status): - manifest["final_status"] = final_status - self.assert_error_contains( - manifest, - "non-BLOCKED acceptance requires sdk-version-ios=PASS", - ) - - def test_repository_release_rejects_forged_external_sdk_pass(self): - manifest = base_manifest() - targets = {platform: "4.7.0" for platform in PLATFORMS} - manifest["requirement"]["target_sdk_versions"] = targets - manifest["release"]["target_sdk_versions"] = targets - manifest["release"]["checks"] = collect_sdk_version_checks(targets) - ios = next( - check - for check in manifest["release"]["checks"] - if check["name"] == "sdk-version-ios" - ) - self.assertEqual(ios["result"], "BLOCKED") - ios.update( - { - "result": "PASS", - "actual_versions": {"iOS/APIExample/Podfile": "4.7.0"}, - "evidence": "Forged repository evidence.", - "reason": "", - } - ) - - errors = validate_repository_release(manifest) - - self.assertIn( - "release.checks must match live repository SDK checks from the bound repository profile", - errors, - ) - - def test_repository_release_rejects_profile_hash_drift(self): - manifest = base_manifest() - manifest["release"]["repository_profile_sha256"] = "0" * 64 - - errors = validate_repository_release(manifest) - - self.assertIn( - "release.repository_profile_sha256 does not match repository profile", - errors, - ) - - def test_rejects_external_ci_and_qa_metadata(self): - manifest = base_manifest() - manifest["release"]["qa_acceptance"] = {} - - self.assert_error_contains(manifest, "unsupported release field: qa_acceptance") - - def test_non_blocked_release_rejects_skipped_checks(self): - manifest = base_manifest() - manifest["final_status"] = "PASS WITH RISKS" - manifest["release"]["skipped_checks"] = [ - {"name": "qa-device-smoke", "reason": "Device unavailable"} - ] - - self.assert_error_contains( - manifest, - "non-BLOCKED acceptance cannot include skipped release checks", - ) - - def test_build_pass_requires_bound_command_evidence(self): - manifest = base_manifest() - del manifest["platforms"]["android"]["verification"]["output"]["commands"][0][ - "evidence" - ] - - self.assert_error_contains( - manifest, - "platforms.android.verification.output.commands[0].evidence is required", - ) - - def test_successful_non_build_command_cannot_satisfy_build_result(self): - manifest = base_manifest() - command = manifest["platforms"]["android"]["verification"]["output"]["commands"][0] - command["command"] = "true" - - self.assert_error_contains( - manifest, - "platforms.android.verification.output.commands[0] is not a recognized android build command", - ) - - def test_build_tool_name_in_echo_cannot_satisfy_build_result(self): - manifest = base_manifest() - command = manifest["platforms"]["android"]["verification"]["output"]["commands"][0] - command["command"] = "echo ./gradlew assembleDebug" - - self.assert_error_contains( - manifest, - "platforms.android.verification.output.commands[0] is not a recognized android build command", - ) - - def test_non_build_tool_modes_cannot_satisfy_build_result(self): - for platform, command_text in [ - ("android", "./gradlew --dry-run assembleDebug"), - ("android", "./gradlew --dry-run=true assembleDebug"), - ("android", "./gradlew help --task assembleDebug"), - ("ios", "xcodebuild -version"), - ("ios", "xcodebuild -scheme APIExample -showTestPlans"), - ("ios", "xcodebuild -scheme APIExample -resolvePackageDependencies"), - ("ios", "xcodebuild -scheme APIExample -checkFirstLaunchStatus"), - ("macos", "xcodebuild -showsdks"), - ("windows", "msbuild -version"), - ("windows", "msbuild APIExample.sln /t:Clean"), - ("windows", "msbuild APIExample.sln /preprocess:out.xml"), - ("windows", "msbuild APIExample.sln /targets"), - ("windows", "msbuild APIExample.sln /validate"), - ("windows", "cmake --build build --target help"), - ("android", "../fake/gradlew assembleDebug"), - ("ios", "/tmp/xcodebuild -scheme APIExample build"), - ("windows", "PATH=./fake msbuild APIExample.sln /t:Build"), - ]: - with self.subTest(platform=platform): - manifest = base_manifest() - command = manifest["platforms"][platform]["verification"]["output"]["commands"][0] - command["command"] = command_text - - self.assert_error_contains( - manifest, - f"platforms.{platform}.verification.output.commands[0] is not a recognized {platform} build command", - ) - - def test_build_command_cannot_escape_contract_working_directory(self): - manifest = base_manifest() - command = manifest["platforms"]["ios"]["verification"]["output"]["commands"][0] - command["command"] = ( - "xcodebuild -project ../APIExample-Audio/APIExample.xcodeproj " - "-scheme APIExample build" - ) - - self.assert_error_contains( - manifest, - "platforms.ios.verification.output.commands[0] is not a recognized ios build command", - ) - - def test_xcode_default_build_command_from_project_rules_is_accepted(self): - manifest = base_manifest() - command = manifest["platforms"]["macos"]["verification"]["output"]["commands"][0] - command["command"] = ( - "xcodebuild -workspace APIExample.xcworkspace -scheme APIExample " - "-configuration Release" - ) - - self.assertEqual(validate_manifest(manifest), []) - - def test_windows_build_pass_requires_windows_host_provenance(self): - manifest = base_manifest() - manifest["platforms"]["windows"]["verification"]["dispatch"]["host_platform"] = "darwin" - - self.assert_error_contains( - manifest, - "platforms.windows.verification.output.build_result=PASS requires host_platform=win32", - ) - - def test_apple_build_pass_requires_darwin_host_provenance(self): - for platform in ["ios", "macos"]: - with self.subTest(platform=platform): - manifest = base_manifest() - manifest["platforms"][platform]["verification"]["dispatch"][ - "host_platform" - ] = "linux" - - self.assert_error_contains( - manifest, - f"platforms.{platform}.verification.output.build_result=PASS requires host_platform=darwin", - ) - - def test_rejects_non_main_project_target_inside_platform_root(self): - manifest = base_manifest() - target = manifest["contract"]["output"]["platform_targets"]["ios"] - target["target_project"] = "iOS/APIExample-OC/" - target["files_allowed"] = ["iOS/APIExample-OC/"] - - self.assert_error_contains( - manifest, - "contract.output.platform_targets.ios.target_project must be exactly iOS/APIExample/", - ) - - def test_rejects_files_allowed_in_sibling_project(self): - manifest = base_manifest() - target = manifest["contract"]["output"]["platform_targets"]["ios"] - target["files_allowed"] = ["iOS/APIExample-OC/"] - - self.assert_error_contains( - manifest, - "contract.output.platform_targets.ios.files_allowed must stay inside iOS/APIExample/", - ) - - def test_command_evidence_cwd_must_match_dispatch_working_directory(self): - manifest = base_manifest() - command = manifest["platforms"]["ios"]["verification"]["output"]["commands"][0] - command["evidence"] = ( - "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " - "cwd=iOS/APIExample-Audio/; cwd_source=command_event" - ) - - self.assert_error_contains( - manifest, - "platforms.ios.verification.output.commands[0].evidence cwd must match " - "dispatch.working_directory iOS/APIExample/", - ) - - def test_rejects_platform_target_outside_its_platform_root(self): - manifest = base_manifest() - target = manifest["contract"]["output"]["platform_targets"]["android"] - target["target_project"] = "iOS/APIExample/" - target["files_allowed"] = ["iOS/APIExample/"] - - self.assert_error_contains( - manifest, - "contract.output.platform_targets.android.target_project must be exactly Android/APIExample/", - ) - - def test_rejects_platform_target_that_escapes_root_with_parent_segments(self): - manifest = base_manifest() - target = manifest["contract"]["output"]["platform_targets"]["android"] - target["target_project"] = "Android/../iOS/APIExample/" - target["files_allowed"] = ["Android/../iOS/APIExample/"] - - self.assert_error_contains( - manifest, - "contract.output.platform_targets.android.target_project must be exactly Android/APIExample/", - ) - - def test_rejects_dispatch_path_outside_workspace(self): - manifest = base_manifest() - manifest["platforms"]["android"]["implementation"]["dispatch"]["prompt"] = "../prompt.md" - - self.assert_error_contains(manifest, "must be relative to the execution workspace") - - def test_files_changed_accepts_deleted_path_inside_contract_scope(self): - manifest = base_manifest() - manifest["platforms"]["android"]["implementation"]["output"]["files_changed"] = [ - "Android/APIExample/deleted-case.java" - ] - - self.assertEqual(validate_manifest(manifest), []) - - def test_knowledge_update_requires_a_durable_file_change(self): - manifest = base_manifest() - for platform in PLATFORMS: - manifest["platforms"][platform]["implementation"]["output"]["files_changed"] = ["AGENTS.md"] - manifest["knowledge_updates"] = [ - { - "source": "pilot", - "impact_platforms": PLATFORMS, - "symptom": "Build blocked", - "root_cause": "Host mismatch", - "guardrail": "Use target CI", - "verification": "CI build", - "updated_at": "2026-07-10", - } - ] - - self.assert_error_contains(manifest, "knowledge_updates require at least one durable knowledge") - - def test_durable_knowledge_accepts_repository_and_project_skill_paths(self): - self.assertTrue(is_durable_knowledge_path(".agents/skills/review-case/SKILL.md")) - self.assertTrue( - is_durable_knowledge_path( - "Android/APIExample/.agents/skills/review-case/SKILL.md" - ) - ) - - def test_durable_knowledge_rejects_legacy_agent_skill_directory(self): - # The repository standardized on `.agents/skills/`; the legacy `.agent/skills/` - # spelling must not silently pass or the naming split can reappear. - self.assertFalse( - is_durable_knowledge_path( - "Android/APIExample/.agent/skills/review-case/SKILL.md" - ) - ) - - def test_evidence_files_are_rehashed_from_manifest_directory(self): - manifest = base_manifest() - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - artifacts = [manifest["contract"]] - for platform in PLATFORMS: - artifacts.extend(manifest["platforms"][platform].values()) - for index, item in enumerate(artifacts): - dispatch = item["dispatch"] - for path_field, hash_field in [ - ("prompt", "prompt_sha256"), - ("artifact", "artifact_sha256"), - ("input_snapshot", "input_snapshot_sha256"), - ("command_log", "command_log_sha256"), - ("repository_delta", "repository_delta_sha256"), - ]: - if path_field not in dispatch: - continue - path = root / dispatch[path_field] - path.parent.mkdir(parents=True, exist_ok=True) - if path_field == "repository_delta": - content = json.dumps( - {"changed_files": item["output"]["files_changed"]} - ).encode() - elif path_field == "command_log" and "commands" in item.get("output", {}): - command = item["output"]["commands"][0] - content = ( - json.dumps( - { - "type": "item.completed", - "item": { - "id": "build", - "type": "command_execution", - "command": command["command"], - "exit_code": 0, - "cwd": item["dispatch"]["working_directory"], - }, - } - ) - + "\n" - ).encode() - else: - content = f"{index}:{path_field}\n".encode() - path.write_bytes(content) - dispatch[hash_field] = hashlib.sha256(content).hexdigest() - - self.assertEqual(validate_evidence_files(manifest, root), []) - prompt = root / manifest["contract"]["dispatch"]["prompt"] - prompt.write_text("tampered\n", encoding="utf-8") - - errors = validate_evidence_files(manifest, root) - self.assertTrue(any("contract.dispatch.prompt_sha256 does not match" in error for error in errors)) - - def test_evidence_validation_rejects_command_event_from_wrong_cwd(self): - manifest = base_manifest() - verification = manifest["platforms"]["ios"]["verification"] - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - log_path = root / verification["dispatch"]["command_log"] - log_path.parent.mkdir(parents=True) - command = verification["output"]["commands"][0] - content = ( - json.dumps( - { - "type": "item.completed", - "item": { - "id": "build", - "type": "command_execution", - "command": f"/bin/zsh -lc '{command['command']}'", - "exit_code": 0, - "cwd": "iOS/APIExample-Audio/", - }, - } - ) - + "\n" - ) - log_path.write_text(content, encoding="utf-8") - verification["dispatch"]["command_log_sha256"] = hashlib.sha256( - content.encode() - ).hexdigest() - - errors = validate_evidence_files(manifest, root) - - self.assertTrue( - any("command_execution cwd must match" in error for error in errors), - errors, - ) - - def test_evidence_validation_accepts_dispatch_cwd_when_event_omits_it(self): - manifest = base_manifest() - verification = manifest["platforms"]["ios"]["verification"] - command = verification["output"]["commands"][0] - command["evidence"] = ( - "dispatch-logs/ios-verification.jsonl#build; exit_code=0; " - "cwd=iOS/APIExample/; cwd_source=dispatch" - ) - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - log_path = root / verification["dispatch"]["command_log"] - log_path.parent.mkdir(parents=True) - content = ( - json.dumps( - { - "type": "item.completed", - "item": { - "id": "build", - "type": "command_execution", - "command": f"/bin/zsh -lc '{command['command']}'", - "exit_code": 0, - }, - } - ) - + "\n" - ) - log_path.write_text(content, encoding="utf-8") - errors = [] - validate_command_log_evidence( - "platforms.ios.verification", verification, log_path, errors - ) - - self.assertEqual(errors, []) - - def test_evidence_validation_rejects_delta_content_mismatch(self): - manifest = base_manifest() - implementation = manifest["platforms"]["android"]["implementation"] - with tempfile.TemporaryDirectory() as tmpdir: - root = Path(tmpdir) - delta_path = root / implementation["dispatch"]["repository_delta"] - delta_path.parent.mkdir(parents=True) - delta_path.write_text( - json.dumps({"changed_files": ["Android/APIExample/other.java"]}), - encoding="utf-8", - ) - implementation["dispatch"]["repository_delta_sha256"] = hashlib.sha256( - delta_path.read_bytes() - ).hexdigest() - - errors = validate_evidence_files(manifest, root) - - self.assertTrue( - any("repository_delta changed_files do not match" in error for error in errors), - errors, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/ai/README.md b/docs/ai/README.md new file mode 100644 index 000000000..dcdef365e --- /dev/null +++ b/docs/ai/README.md @@ -0,0 +1,59 @@ +# AI-Assisted Development + +This directory contains durable guidance for coordinating one requirement across the nine +RTC API Example projects. It is intended for both developers and coding agents and is part +of the repository's normal review process. + +## Entry Points + +| File | Purpose | +| --- | --- | +| [AGENTS.md](../../AGENTS.md) | Repository-wide rules and navigation. | +| [cross-platform-api-change](../../.agents/skills/cross-platform-api-change/SKILL.md) | Executable workflow for cross-platform changes. | +| [change-contract-template.md](change-contract-template.md) | Canonical contract to complete before implementation. | +| [project-matrix.md](project-matrix.md) | Project inventory, SDK sources, and scope rules. | +| [release-risks.md](release-risks.md) | Verified repository-specific build and release risks. | +| [pull_request_template.md](../../.github/pull_request_template.md) | Review record for scope, differences, and validation. | +| [validate_ai_assets.py](../../.github/ci/policy/validate_ai_assets.py) | Local and CI validation for skill metadata, links, entry points, and the nine-project matrix. | + +## Working Agreement + +1. Translate the requirement into one testable change contract. +2. Make an explicit scope decision for every project before implementation. +3. Implement each required project from the same contract. +4. Compare behavior and validation evidence across platforms before completion. +5. Keep the final contract and evidence in the issue or pull request. + +After changing AI guidance, use Python 3.10 or newer in a virtual environment, install the +locked validator dependency, and run both the tests and repository check from the repository root: + +```bash +python3 -m venv .venv +.venv/bin/python -m pip install --requirement .github/ci/policy/requirements.txt +.venv/bin/python -m unittest discover --start-directory .github/ci/policy/tests --pattern 'test_validate_ai_assets.py' +.venv/bin/python .github/ci/policy/validate_ai_assets.py +``` + +On Windows, create the environment with `py -3 -m venv .venv` and use +`.venv/Scripts/python.exe` in place of `.venv/bin/python`. The environment is git-ignored. +Reuse it for later checks; do not install the validator into the system Python environment. + +The same checks run in `.github/workflows/repository-policy.yml` for pushes and pull requests. + +The requirement contract owns shared behavior. Platform implementations may differ only +when the SDK, UI framework, permission model, or operating system requires it, and the +difference must be recorded. + +## Repository Boundaries + +- Treat source code, project `AGENTS.md`, and `ARCHITECTURE.md` files as the current case + inventory. +- Do not maintain generated case catalogs, source hashes, agent routing tables, prompt + snapshots, task transcripts, or run logs in this directory. +- Keep reusable workflow rules in the skill and repository facts in these documents. +- Update `project-matrix.md` when a project, SDK dependency source, or CI owner changes. +- Add an item to `release-risks.md` only after verifying it against current repository code + or release automation. +- Keep the workflow and contract template aligned between the Agora and Shengwang + repositories. Record brand and distribution differences only in repository-specific + sections of the matrix and risk documents. diff --git a/docs/ai/change-contract-template.md b/docs/ai/change-contract-template.md new file mode 100644 index 000000000..97115b8bb --- /dev/null +++ b/docs/ai/change-contract-template.md @@ -0,0 +1,85 @@ +# Cross-Platform Change Contract + +Copy this template into the issue, pull request, or task notes before implementation. Use +concise, testable statements and keep it current when the requirement changes. + +## Requirement + +- Work item or issue: +- Contract owner: +- Target SDK version by platform: +- User-visible scenario: +- Inputs and defaults: +- Success signals: +- Error and recovery behavior: +- Lifecycle and cleanup: +- Permissions, assets, or external dependencies: +- Non-goals: +- Reference documentation or implementation: + +## SDK Contract + +| Concern | Shared intent | Android API | Apple API | Windows API | +| --- | --- | --- | --- | --- | +| Initialization | | | | | +| Main operation | | | | | +| Callback or result | | | | | +| Error handling | | | | | +| Cleanup | | | | | + +## Project Scope + +Use `REQUIRED`, `REVIEW_ONLY`, or `NOT_APPLICABLE`. Every non-required decision needs a +reason. + +| Project | Decision | Required behavior or reason | Expected files or entry point | Planned validation | +| --- | --- | --- | --- | --- | +| Android Views full | | | | | +| Android Views audio | | | | | +| Android Compose | | | | | +| iOS UIKit full | | | | | +| iOS UIKit audio | | | | | +| iOS SwiftUI | | | | | +| iOS Objective-C | | | | | +| macOS AppKit | | | | | +| Windows MFC | | | | | + +## Validation Evidence + +Validation status is independent of the project scope decision. Add one row for every +planned check, especially every check required by a `REQUIRED` project. + +- `PASS`: the check completed and met its acceptance criteria. +- `FAIL`: the check ran and failed. +- `BLOCKED`: the check cannot run yet; record the remaining command and unblock condition. +- `NOT_RUN`: the check was not attempted; record why. + +| Project | Command or behavioral check | Status | Evidence, blocker, or reason | Remaining command and unblock condition | +| --- | --- | --- | --- | --- | +| | | | | | + +## Shared Acceptance + +- [ ] SDK arguments, defaults, and expected callbacks match the contract. +- [ ] Join, leave, resource ownership, and engine destruction are complete. +- [ ] UI updates follow each platform's threading model. +- [ ] Errors and unsupported states are handled consistently. +- [ ] Case registration, resources, permissions, and localization are updated where needed. +- [ ] Every required validation for each `REQUIRED` project is `PASS`. +- [ ] SDK versions and dependency sources match `docs/ai/project-matrix.md`. +- [ ] No credentials, tokens, signing assets, private keys, or private URLs are committed. + +## Intentional Differences + +| Project | Difference | Product, SDK, UI, or OS reason | +| --- | --- | --- | +| | | | + +## Contract Changes + +| Date | Change | Affected projects | Revalidation required | +| --- | --- | --- | --- | +| | | | | + +A contract with `FAIL`, `BLOCKED`, or `NOT_RUN` validation can be handed off, but remains +incomplete until every required validation is `PASS`. diff --git a/docs/ai/project-matrix.md b/docs/ai/project-matrix.md new file mode 100644 index 000000000..1969d08f9 --- /dev/null +++ b/docs/ai/project-matrix.md @@ -0,0 +1,40 @@ +# Project Matrix + +Every cross-platform requirement must review all nine rows. Record the decision in the +change contract; this document defines the stable project inventory and dependency sources. + +| Project | Path | SDK dependency | Project guidance | +| --- | --- | --- | --- | +| Android Views full | `Android/APIExample/` | `io.agora.rtc:full-sdk`; version in `gradle.properties` | `Android/APIExample/AGENTS.md` | +| Android Views audio | `Android/APIExample-Audio/` | `io.agora.rtc:voice-sdk`; version in `gradle.properties` | `Android/APIExample-Audio/AGENTS.md` | +| Android Compose | `Android/APIExample-Compose/` | `io.agora.rtc:full-sdk`; version in `gradle.properties` | `Android/APIExample-Compose/AGENTS.md` | +| iOS UIKit full | `iOS/APIExample/` | Local `sdk.podspec` for release work; published `AgoraRtcEngine_iOS` for compile CI | `iOS/APIExample/AGENTS.md` | +| iOS UIKit audio | `iOS/APIExample-Audio/` | `AgoraAudio_iOS` in `Podfile` | `iOS/APIExample-Audio/AGENTS.md` | +| iOS SwiftUI | `iOS/APIExample-SwiftUI/` | `AgoraRtcEngine_iOS` in `Podfile` | `iOS/APIExample-SwiftUI/AGENTS.md` | +| iOS Objective-C | `iOS/APIExample-OC/` | `AgoraRtcEngine_iOS` in `Podfile` | `iOS/APIExample-OC/AGENTS.md` | +| macOS AppKit | `macOS/` | `AgoraRtcEngine_macOS` in `Podfile` | `macOS/AGENTS.md` | +| Windows MFC | `windows/APIExample/` | Local `../../sdk` or public `Agora_Native_SDK_for_Windows_*_FULL.zip` | `windows/AGENTS.md` | + +## Scope Rules + +- SDK version, dependency, credential plumbing, CI, and packaging changes require review of + every row even when source behavior is unchanged. +- Audio-only behavior normally includes both audio projects and any full-SDK project that + exposes the same case. Video-only behavior may mark audio projects `NOT_APPLICABLE`. +- Compose, SwiftUI, and Objective-C are independent implementations. Coverage does not + inherit from Android Views or iOS UIKit. +- A platform-specific requirement still records decisions for the other projects. +- Read the selected project's `AGENTS.md` and `ARCHITECTURE.md` before editing. + +## Automation Ownership + +| Concern | Source of truth | +| --- | --- | +| Post-release unsigned compilation | `.github/workflows/compile.yml`; covers all nine projects using a placeholder App ID | +| Repository policy checks | `.github/workflows/repository-policy.yml` and local `.git-hooks/` | +| Release packaging | `.github/ci/build/`; executed by Jenkins | +| Gitee synchronization | Not used in the Agora repository | + +Azure Pipelines configuration and dedicated legacy scripts have been removed from this +repository. External Azure definitions and GitHub integrations are managed separately; +removing repository files does not disable them or remove required status checks. diff --git a/docs/ai/release-risks.md b/docs/ai/release-risks.md new file mode 100644 index 000000000..64d1479bf --- /dev/null +++ b/docs/ai/release-risks.md @@ -0,0 +1,17 @@ +# Release Risks + +This file records verified repository-specific conditions that can change implementation or +validation decisions. Jenkins execution state, QA status, artifact URLs, and temporary +incidents belong in the release ticket rather than this document. + +| Area | Verified risk | Required handling | +| --- | --- | --- | +| Public SDK availability | Release branches can reference an SDK version before Maven, CocoaPods, or the Windows download endpoint publishes it. | Use an approved local/internal SDK source for pre-release validation or record the public-source build as blocked. Run `.github/workflows/compile.yml` after the version reaches `main`. | +| iOS UIKit full SDK source | `iOS/APIExample/Podfile` uses a local `sdk.podspec`; the binary is injected outside Git. Compile CI rewrites this dependency to the published `AgoraRtcEngine_iOS` version. | Verify the injected SDK version during release packaging and verify the published pod separately in compile CI. Do not treat one as evidence for the other. | +| Android native extensions | Optional extension projects use SDK headers and native assets outside the normal Maven dependency. | Review headers and matching native assets whenever the RTC SDK version changes. | +| Apple signing | Repository compilation disables signing and cannot prove that Jenkins signing certificates or profiles are valid. | Keep signing verification in the release pipeline and record its evidence in the release ticket. | +| Windows validation | Windows uses architecture-specific files from either `../../sdk` or the public full archive and requires MSBuild. | Run dependency installation and compilation on a Windows worker; a non-Windows source review is not build evidence. | +| Packaging versus compilation | GitHub Actions checks source compilation; `.github/ci/build/` owns distributable artifacts. | Do not report compile CI success as proof that release packages were produced correctly. | + +Update this file only when the condition is reproducible from tracked code or current release +automation. Remove entries when the underlying condition no longer exists. diff --git a/iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md b/iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md index 6b6676b67..2552f9ac2 100644 --- a/iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md +++ b/iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md @@ -3,7 +3,6 @@ name: query-cases description: > Find existing audio API demo cases in the APIExample-Audio project by feature name, API name, or keyword. Use this before creating a new case to avoid duplication. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team diff --git a/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md index c1cb428f8..6a764e7af 100644 --- a/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md @@ -4,7 +4,6 @@ description: > Structured code review for a case in the APIExample-Audio project. Checks engine lifecycle, audio-only constraints, thread safety, permissions, and API correctness. This project uses AgoraAudio_iOS — video APIs must not appear. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -109,6 +108,16 @@ All `AgoraRtcEngineDelegate` callbacks may arrive on a background thread. --- +### 9. Xcode Target Membership + +For a new or moved case, verify `APIExample-Audio.xcodeproj/project.pbxproj` explicitly adds +every Swift file to the `APIExample-Audio` target's Sources build phase and every storyboard, +localized file, or audio asset to its Resources build phase. A file existing in the folder +is not enough. Do not require a project-file edit for an existing build input that was only +modified. + +--- + ## Review Output Format ``` diff --git a/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md index 3e59cbd14..08c6ea5f8 100644 --- a/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -3,8 +3,7 @@ name: upsert-case description: > Add a new audio API demo case or modify an existing one in the APIExample-Audio project. Uses AgoraAudio_iOS SDK — no video APIs available. Covers folder creation, Entry/Main Swift file, - storyboard, MenuItem registration, and Case Index update. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] + storyboard, MenuItem registration, Xcode target membership, and Case Index update. license: MIT metadata: author: APIExample Team @@ -17,7 +16,7 @@ metadata: ## When to Use - **Add**: the feature has no existing case in `Examples/Basic/` or `Examples/Advanced/` -- **Modify**: the case already exists — skip Steps 1–3, go directly to Step 4+ +- **Modify**: the case already exists — follow the Modify Existing Case flow below Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case does not already exist. @@ -28,11 +27,19 @@ Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case do | Scenario | Files | |----------|-------| -| Add new case | New folder + `.swift` file + `.storyboard`, `ViewController.swift` (MenuItem), `ARCHITECTURE.md` (Case Index) | -| Modify existing case | Existing `.swift` file(s), optionally `.storyboard`, `ARCHITECTURE.md` (Case Index) | +| Add new case | New folder + `.swift` file + `.storyboard`, `ViewController.swift` (MenuItem), `APIExample-Audio.xcodeproj/project.pbxproj` (target membership), `ARCHITECTURE.md` (Case Index) | +| Modify existing case | Existing `.swift` file(s), optionally `.storyboard`, `ARCHITECTURE.md` (Case Index); update the project file only for new or moved build inputs | --- +## Modify Existing Case + +1. Locate the existing Swift implementation and change the actual runtime behavior first. +2. Update storyboard outlets, actions, and controller identifiers when the behavior change needs it. +3. Adjust registration in `ViewController.swift` only when the menu or navigation wiring changes. +4. Update Xcode target membership for new or moved build inputs, then synchronize the Case Index. +5. Build and review the changed behavior, lifecycle, and registration. Reusing an existing folder does not replace implementation work. + ## Step 1 — Create the Example Folder ``` @@ -121,7 +128,15 @@ MenuItem(name: "".localized, controller: "") ``` -## Step 5 — Update the Case Index +## Step 5 — Add Files to the Xcode Target + +This project uses explicit Xcode groups and build phases. For a new case, update +`APIExample-Audio.xcodeproj/project.pbxproj` so the Swift file belongs to the +`APIExample-Audio` target's Sources build phase and the storyboard belongs to its Resources +build phase. Add any new localized or audio resources to Resources as well. Existing-file +edits do not require a project-file change unless a build input was added or moved. + +## Step 6 — Update the Case Index Add a row to the `## Case Index` table in `ARCHITECTURE.md`: @@ -139,6 +154,8 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - [ ] Storyboard has correct scene IDs - [ ] No video rendering views in the storyboard - [ ] MenuItem added to `ViewController.swift` +- [ ] New Swift files are in the `APIExample-Audio` target's Sources build phase +- [ ] New storyboards, localized files, and audio assets are in the target's Resources build phase - [ ] `leaveChannel()` + `AgoraRtcEngineKit.destroy()` called in `willMove(toParent:)` when `parent == nil` - [ ] UI updates inside delegate callbacks dispatched to `DispatchQueue.main` - [ ] Microphone permission requested before `joinChannel()` @@ -154,5 +171,6 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - NEVER create `AgoraRtcEngineKit` in the Entry VC - NEVER call `leaveChannel` or `destroy` in `viewDidDisappear` — use `willMove(toParent:)` with `parent == nil` - NEVER update UI directly inside `AgoraRtcEngineDelegate` callbacks — always `DispatchQueue.main.async { }` +- NEVER add a source or resource file without adding it to the `APIExample-Audio` target - NEVER share an `AgoraRtcEngineKit` instance between cases - NEVER skip updating the Case Index in `ARCHITECTURE.md` diff --git a/iOS/APIExample-Audio/ARCHITECTURE.md b/iOS/APIExample-Audio/ARCHITECTURE.md index 632030ca0..65143682a 100644 --- a/iOS/APIExample-Audio/ARCHITECTURE.md +++ b/iOS/APIExample-Audio/ARCHITECTURE.md @@ -81,9 +81,11 @@ struct MenuItem { } ``` -**To add a case, edit exactly two things:** -1. Add a `MenuItem` to the `menus` array in `ViewController.swift` -2. Create the example folder under `Examples/Basic/` or `Examples/Advanced/` with the Swift file(s) and storyboard +A registered case connects the `MenuItem`, its Swift controller(s), and its storyboard. +New source files must belong to the Xcode target's Sources build phase, and storyboards, +localized files, and media assets must belong to Resources. Follow +[upsert-case](.agents/skills/upsert-case/SKILL.md) for the complete change procedure and +update the Case Index when the case changes. ## Entry/Main ViewController Pattern @@ -117,11 +119,15 @@ viewDidLoad → AgoraRtcEngineKit.sharedEngine(withAppId:delegate:) ↓ [AgoraRtcEngineDelegate callbacks — may be on background thread] ↓ -viewDidDisappear / willMove(toParent:) +willMove(toParent:) when parent == nil → engine.leaveChannel() → AgoraRtcEngineKit.destroy() ``` +For navigation-based cases, cleanup runs when the controller is removed from its parent. +Temporary disappearance alone must not destroy the engine; follow the guarded cleanup in +the upsert and review skills. + ## Token Flow ```swift diff --git a/iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md b/iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md index 8817e67f1..7253b3c0e 100644 --- a/iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md +++ b/iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md @@ -3,7 +3,6 @@ name: query-cases description: > Find existing API demo cases in the APIExample-OC project by feature name, API name, or keyword. Use this before creating a new case to avoid duplication. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team diff --git a/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md index 2c9d57145..8972e79e4 100644 --- a/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md @@ -3,7 +3,6 @@ name: review-case description: > Structured code review for a case in the APIExample-OC (Objective-C + UIKit) project. Checks engine lifecycle, thread safety, memory management, permissions, and OC conventions. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -136,6 +135,16 @@ __weak typeof(self) weakSelf = self; --- +### 9. Xcode Target Membership + +For a new or moved case, verify `APIExample-OC.xcodeproj/project.pbxproj` contains file +references for the `.h` and `.m` files, adds the `.m` file to the `APIExample-OC` target's +Sources build phase, and adds every storyboard, localized file, or media asset to its +Resources build phase. A file existing in the folder is not enough. Do not require a +project-file edit for an existing build input that was only modified. + +--- + ## Review Output Format ``` diff --git a/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md index 92a9c1d7c..64c2929c5 100644 --- a/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md @@ -2,8 +2,8 @@ name: upsert-case description: > Add a new API demo case or modify an existing one in the APIExample-OC (Objective-C + UIKit) project. - Covers folder creation, Entry/Main OC files, storyboard, MenuItem registration, and Case Index update. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] + Covers folder creation, Entry/Main OC files, storyboard, MenuItem registration, Xcode target + membership, and Case Index update. license: MIT metadata: author: APIExample Team @@ -16,7 +16,7 @@ metadata: ## When to Use - **Add**: the feature has no existing case in `Examples/Basic/` or `Examples/Advanced/` -- **Modify**: the case already exists — skip Steps 1–3, go directly to Step 4+ +- **Modify**: the case already exists — follow the Modify Existing Case flow below Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case does not already exist. @@ -24,11 +24,19 @@ Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case do | Scenario | Files | |----------|-------| -| Add new case | New folder + `.h/.m` files + `.storyboard`, `ViewController.m` (MenuItem), `ARCHITECTURE.md` (Case Index) | -| Modify existing case | Existing `.h/.m` files, optionally `.storyboard`, `ARCHITECTURE.md` (Case Index) | +| Add new case | New folder + `.h/.m` files + `.storyboard`, `ViewController.m` (MenuItem), `APIExample-OC.xcodeproj/project.pbxproj` (target membership), `ARCHITECTURE.md` (Case Index) | +| Modify existing case | Existing `.h/.m` files, optionally `.storyboard`, `ARCHITECTURE.md` (Case Index); update the project file only for new or moved build inputs | --- +## Modify Existing Case + +1. Locate the existing .h/.m implementation and change the actual runtime behavior first. +2. Update storyboard outlets, actions, and controller identifiers when the behavior change needs it. +3. Adjust registration in `ViewController.m` only when the menu or navigation wiring changes. +4. Update Xcode target membership for new or moved build inputs, then synchronize the Case Index. +5. Build and review the changed behavior, lifecycle, and registration. Reusing an existing folder does not replace implementation work. + ## Step 1 — Create the Example Folder ``` @@ -136,7 +144,16 @@ Add to `+[MenuSection menus]` in `ViewController.m`: controller:@""] ``` -## Step 6 — Update the Case Index +## Step 6 — Add Files to the Xcode Target + +This project uses explicit Xcode groups and build phases. For a new case, update +`APIExample-OC.xcodeproj/project.pbxproj` so the `.m` file belongs to the `APIExample-OC` +target's Sources build phase, the header has a file reference, and the storyboard belongs +to the target's Resources build phase. Add new localized or media resources to Resources +as well. Existing-file edits do not require a project-file change unless a build input was +added or moved. + +## Step 7 — Update the Case Index Add a row to the `## Case Index` table in `ARCHITECTURE.md`: @@ -153,6 +170,8 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - [ ] Main inherits `BaseViewController` and conforms to `AgoraRtcEngineDelegate` - [ ] Storyboard has correct scene IDs - [ ] MenuItem added to `ViewController.m` +- [ ] New Objective-C files are referenced and the `.m` file is in the `APIExample-OC` target's Sources build phase +- [ ] New storyboards, localized files, and media assets are in the target's Resources build phase - [ ] `leaveChannel:` + `[AgoraRtcEngineKit destroy]` called when leaving - [ ] UI updates inside delegate callbacks dispatched via `dispatch_async(dispatch_get_main_queue(), ^{ })` - [ ] `__weak typeof(self) weakSelf = self` used in blocks that capture `self` @@ -168,6 +187,7 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - NEVER use `__unsafe_unretained` for delegate references — use `__weak` - NEVER update UI directly inside `AgoraRtcEngineDelegate` callbacks — always `dispatch_async(dispatch_get_main_queue(), ^{ })` - NEVER add a new scene to `Main.storyboard` — each case must have its own `.storyboard` file +- NEVER add a source or resource file without adding it to the `APIExample-OC` target - NEVER share an `AgoraRtcEngineKit` instance between cases - NEVER call `joinChannelByToken:` before requesting camera/microphone permissions - NEVER skip updating the Case Index in `ARCHITECTURE.md` diff --git a/iOS/APIExample-OC/ARCHITECTURE.md b/iOS/APIExample-OC/ARCHITECTURE.md index b66067743..9c0697014 100644 --- a/iOS/APIExample-OC/ARCHITECTURE.md +++ b/iOS/APIExample-OC/ARCHITECTURE.md @@ -121,12 +121,11 @@ Registration is **manual** via the `+[MenuSection menus]` method in `ViewControl Each example has its own `.storyboard` file. The VC with identifier `entry` (default `"EntryViewController"`) is instantiated directly from that storyboard. -**To add a case, edit exactly two things:** -1. Add a `MenuItem` to the `+[MenuSection menus]` method in `ViewController.m`: - ```objc - [[MenuItem alloc] initWithName:@"My New Case".localized storyboard:@"MyNewCase" controller:@""] - ``` -2. Create the example folder under `Examples/Basic/` or `Examples/Advanced/` with the `.h/.m` files and storyboard +A registered case connects a `MenuItem` in `+[MenuSection menus]`, its `.h/.m` files, and +its storyboard. The implementation must belong to the Xcode target's Sources build phase; +the storyboard and new localized/media assets must belong to Resources. Follow +[upsert-case](.agents/skills/upsert-case/SKILL.md) for the complete change procedure and +update the Case Index when the case changes. ## Entry/Main ViewController Pattern @@ -150,11 +149,15 @@ viewDidLoad → [AgoraRtcEngineKit sharedEngineWithAppId:delegate:] ↓ [AgoraRtcEngineDelegate callbacks — may be on background thread] ↓ -viewDidDisappear / dealloc +viewDidDisappear: when isMovingFromParentViewController is true → [engine leaveChannel:] → [AgoraRtcEngineKit destroy] ``` +Navigation cleanup is guarded by `isMovingFromParentViewController`, as shown in the +upsert and review skills. Merely covering the controller does not end the case; +`dealloc` is not the primary scene-exit hook. + ## Token Flow ```objc diff --git a/iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md index 03f4a9dda..14f6505a9 100644 --- a/iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md +++ b/iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md @@ -3,7 +3,6 @@ name: query-cases description: > Find existing API demo cases in the APIExample-SwiftUI project by feature name, API name, or keyword. Use this before creating a new case to avoid duplication. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team diff --git a/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md index 04c4437b9..5f4d3b929 100644 --- a/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md @@ -3,7 +3,6 @@ name: review-case description: > Structured code review for a case in the APIExample-SwiftUI project. Checks engine lifecycle, SwiftUI state ownership, thread safety, permissions, and API correctness. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -40,21 +39,26 @@ metadata: ### 2. SwiftUI State Ownership **Check:** -- Main view declares RTC object as `@ObservedObject`, not `@StateObject` +- A main view that constructs its RTC object declares it as `@StateObject` +- `@ObservedObject` is used only when a stable owner constructs and injects the RTC object - Entry view does not hold a reference to the RTC object - `@Published` properties used for state that drives UI updates **Correct:** ```swift struct MyCase: View { - @ObservedObject private var rtc = MyCaseRTC() // correct + @StateObject private var rtc = MyCaseRTC() +} + +struct InjectedCase: View { + @ObservedObject var rtc: MyCaseRTC } ``` **Wrong:** ```swift struct MyCase: View { - @StateObject private var rtc = MyCaseRTC() // wrong — SwiftUI owns lifetime, may outlive view + @ObservedObject private var rtc = MyCaseRTC() // recreated view does not own object identity } ``` @@ -118,6 +122,15 @@ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: I --- +### 8. Xcode Target Membership + +For a new or moved case, verify `APIExample-SwiftUI.xcodeproj/project.pbxproj` explicitly +adds every Swift file to the `APIExample-SwiftUI` target's Sources build phase and every new +asset or localized file to its Resources build phase. A file existing in the folder is not +enough. Do not require a project-file edit for an existing build input that was only modified. + +--- + ## Review Output Format ``` diff --git a/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md index 821cff8eb..5a9e25028 100644 --- a/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md @@ -2,8 +2,8 @@ name: upsert-case description: > Add a new API demo case or modify an existing one in the APIExample-SwiftUI project. - Covers folder creation, Entry view, RTC class, MenuItem registration, and Case Index update. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] + Covers folder creation, Entry view, RTC class, MenuItem registration, Xcode target membership, + and Case Index update. license: MIT metadata: author: APIExample Team @@ -16,7 +16,7 @@ metadata: ## When to Use - **Add**: the feature has no existing case in `Examples/Basic/` or `Examples/Advanced/` -- **Modify**: the case already exists — skip Steps 1–3, go directly to Step 4+ +- **Modify**: the case already exists — follow the Modify Existing Case flow below Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case does not already exist. @@ -24,11 +24,19 @@ Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case do | Scenario | Files | |----------|-------| -| Add new case | New folder + `RTC.swift` + `.swift`, `ContentView.swift` (MenuItem), `ARCHITECTURE.md` (Case Index) | -| Modify existing case | Existing `*RTC.swift` and/or `*.swift` view files, `ARCHITECTURE.md` (Case Index) | +| Add new case | New folder + `RTC.swift` + `.swift`, `ContentView.swift` (MenuItem), `APIExample-SwiftUI.xcodeproj/project.pbxproj` (target membership), `ARCHITECTURE.md` (Case Index) | +| Modify existing case | Existing `*RTC.swift` and/or `*.swift` view files, `ARCHITECTURE.md` (Case Index); update the project file only for new or moved build inputs | --- +## Modify Existing Case + +1. Locate the existing RTC class and SwiftUI view files and change the actual runtime behavior first. +2. Update view state, actions, and navigation when the behavior change needs it. +3. Adjust registration in `ContentView.swift` only when the menu or navigation wiring changes. +4. Update Xcode target membership for new or moved build inputs, then synchronize the Case Index. +5. Build and review the changed behavior, lifecycle, and registration. Reusing an existing folder does not replace implementation work. + ## Step 1 — Create the Example Folder ``` @@ -109,7 +117,7 @@ struct Entry: View { struct : View { @State var configs: [String: Any] = [:] - @ObservedObject private var rtc = RTC() + @StateObject private var rtc = RTC() var body: some View { VStack { /* UI here */ } @@ -127,7 +135,15 @@ Add to the `menus` array in `APIExample-SwiftUI/ContentView.swift`: MenuItem(name: "".localized, view: AnyView(Entry())) ``` -## Step 5 — Update the Case Index +## Step 5 — Add Files to the Xcode Target + +This project uses explicit Xcode groups and build phases. For a new case, update +`APIExample-SwiftUI.xcodeproj/project.pbxproj` so both Swift files belong to the +`APIExample-SwiftUI` target's Sources build phase. Add any new assets or localized files to +the same target's Resources build phase. Existing-file edits do not require a project-file +change unless a build input was added or moved. + +## Step 6 — Update the Case Index Add a row to the `## Case Index` table in `ARCHITECTURE.md`: @@ -142,11 +158,13 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - [ ] Folder created under correct category (Basic / Advanced) - [ ] RTC class inherits `NSObject`, conforms to `ObservableObject` and `AgoraRtcEngineDelegate` - [ ] Engine created in `setupRTC`, destroyed in `onDestroy` -- [ ] Main view uses `@ObservedObject` (not `@StateObject`) for the RTC object +- [ ] Main view uses `@StateObject` when it constructs the RTC object; `@ObservedObject` is used only for an externally owned, injected object - [ ] `setupRTC` called in `.onAppear`, `onDestroy` called in `.onDisappear` - [ ] `leaveChannel` + `AgoraRtcEngineKit.destroy()` called in `onDestroy` - [ ] UI updates inside delegate callbacks dispatched to `DispatchQueue.main` - [ ] MenuItem added to `ContentView.swift` +- [ ] New Swift files are in the `APIExample-SwiftUI` target's Sources build phase +- [ ] New assets or localized files are in the target's Resources build phase, when applicable - [ ] Case Index row added/updated in `ARCHITECTURE.md` - [ ] Project builds without errors @@ -155,7 +173,8 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: ## NEVER - NEVER create `AgoraRtcEngineKit` in the Entry view -- NEVER use `@StateObject` for the RTC object in the Main view — the Main view does not own its lifetime +- NEVER construct an RTC object in a view and store it as `@ObservedObject` — use `@StateObject`; reserve `@ObservedObject` for an externally owned, injected object +- NEVER add a source or resource file without adding it to the `APIExample-SwiftUI` target - NEVER call SDK APIs inside SwiftUI `body` — only in `.onAppear`, `.onDisappear`, or explicit user action handlers - NEVER update UI directly inside `AgoraRtcEngineDelegate` callbacks — always `DispatchQueue.main.async { }` - NEVER share an `AgoraRtcEngineKit` instance between cases diff --git a/iOS/APIExample-SwiftUI/AGENTS.md b/iOS/APIExample-SwiftUI/AGENTS.md index a4a466a8e..15bc2f769 100644 --- a/iOS/APIExample-SwiftUI/AGENTS.md +++ b/iOS/APIExample-SwiftUI/AGENTS.md @@ -17,7 +17,8 @@ static let AppId: String = "YOUR_APP_ID" static let Certificate: String? = nil // leave nil if App Certificate is not enabled ``` -To obtain an App ID, see [README.md](README.md#obtain-an-app-id). +Follow the [repository-wide sensitive configuration rules](../../AGENTS.md#sensitive-configuration) +and never stage a real App ID or certificate in this tracked file. ## Skills diff --git a/iOS/APIExample-SwiftUI/ARCHITECTURE.md b/iOS/APIExample-SwiftUI/ARCHITECTURE.md index 8d727305b..f2a691560 100644 --- a/iOS/APIExample-SwiftUI/ARCHITECTURE.md +++ b/iOS/APIExample-SwiftUI/ARCHITECTURE.md @@ -127,12 +127,15 @@ struct MenuItem: Identifiable { Navigation uses SwiftUI `NavigationLink`. Each `MenuItem` holds an `AnyView` wrapping the Entry view. -**To add a case, edit exactly two things:** +**To add a case, update implementation, registration, and Xcode target membership:** 1. Add a `MenuItem` to the `menus` array in `ContentView.swift`: ```swift MenuItem(name: "My New Case".localized, view: AnyView(MyNewCaseEntry())) ``` 2. Create the example folder under `Examples/Basic/` or `Examples/Advanced/` with the Swift files +3. Add the new Swift files to the `APIExample-SwiftUI` target's Sources build phase in + `APIExample-SwiftUI.xcodeproj/project.pbxproj`; add any new assets or localized files to + its Resources build phase ## Entry/RTC Pattern @@ -149,7 +152,8 @@ Every example is split into two parts: - Implements all delegate callbacks **Main View** (` : View`) -- Holds the RTC object as `@ObservedObject` +- Uses `@StateObject` when it creates and owns the RTC object +- Uses `@ObservedObject` only when a stable owner creates and injects the RTC object - Calls `setupRTC()` in `.onAppear` - Calls `onDestroy()` in `.onDisappear` diff --git a/iOS/APIExample/.agents/skills/query-cases/SKILL.md b/iOS/APIExample/.agents/skills/query-cases/SKILL.md index f35362ee4..ebe425a63 100644 --- a/iOS/APIExample/.agents/skills/query-cases/SKILL.md +++ b/iOS/APIExample/.agents/skills/query-cases/SKILL.md @@ -3,7 +3,6 @@ name: query-cases description: > Find existing API demo cases in the APIExample project by feature name, API name, or keyword. Use this before creating a new case to avoid duplication. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team diff --git a/iOS/APIExample/.agents/skills/review-case/SKILL.md b/iOS/APIExample/.agents/skills/review-case/SKILL.md index 70e70d6eb..816203da2 100644 --- a/iOS/APIExample/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample/.agents/skills/review-case/SKILL.md @@ -3,7 +3,6 @@ name: review-case description: > Structured code review for a case in the APIExample (UIKit + Swift) project. Checks engine lifecycle, thread safety, permissions, error handling, API correctness, and code conventions. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -131,6 +130,15 @@ AgoraAudioSession.sharedInstance().requestRecordPermission { [weak self] granted --- +### 8. Xcode Target Membership + +For a new or moved case, verify `APIExample.xcodeproj/project.pbxproj` explicitly adds every +Swift file to the `APIExample` target's Sources build phase and every storyboard, localized +file, or media asset to its Resources build phase. A file existing in the folder is not +enough. Do not require a project-file edit for an existing build input that was only modified. + +--- + ## Review Output Format For each issue found, report: diff --git a/iOS/APIExample/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample/.agents/skills/upsert-case/SKILL.md index 9e1c531d2..7fd7363af 100644 --- a/iOS/APIExample/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample/.agents/skills/upsert-case/SKILL.md @@ -2,8 +2,8 @@ name: upsert-case description: > Add a new API demo case or modify an existing one in the APIExample (UIKit + Swift) project. - Covers folder creation, Entry/Main Swift file, storyboard, MenuItem registration, and Case Index update. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] + Covers folder creation, Entry/Main Swift file, storyboard, MenuItem registration, Xcode target + membership, and Case Index update. license: MIT metadata: author: APIExample Team @@ -24,8 +24,8 @@ Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case do | Scenario | Files | |----------|-------| -| Add new case | New folder + `.swift` file + `Base.lproj/.storyboard`, `ViewController.swift` (MenuItem), `ARCHITECTURE.md` (Case Index) | -| Modify existing case | Existing `.swift` file(s), optionally `Base.lproj/.storyboard`, `ViewController.swift` if registration/wiring changed, `ARCHITECTURE.md` (Case Index) | +| Add new case | New folder + `.swift` file + `Base.lproj/.storyboard`, `ViewController.swift` (MenuItem), `APIExample.xcodeproj/project.pbxproj` (target membership), `ARCHITECTURE.md` (Case Index) | +| Modify existing case | Existing `.swift` file(s), optionally `Base.lproj/.storyboard`, `ViewController.swift` if registration/wiring changed, `ARCHITECTURE.md` (Case Index); update the project file only for new or moved build inputs | --- @@ -146,7 +146,15 @@ MenuItem(name: "".localized, Place it in the correct section (Basic / Advanced). -## Step 5 — Update the Case Index +## Step 5 — Add Files to the Xcode Target + +This project uses explicit Xcode groups and build phases. For a new case, update +`APIExample.xcodeproj/project.pbxproj` so the Swift file belongs to the `APIExample` +target's Sources build phase and the storyboard belongs to its Resources build phase. +Add any new localized or media resources to Resources as well. Existing-file edits do not +require a project-file change unless a build input was added or moved. + +## Step 6 — Update the Case Index Add a row to the `## Case Index` table in `ARCHITECTURE.md`: @@ -166,6 +174,8 @@ Key APIs: list 2–5 core SDK methods the case demonstrates. Do not list `joinCh - [ ] Storyboard has correct scene IDs - [ ] Entry scene follows default UI convention (channel input + Join button), unless the user requested otherwise - [ ] MenuItem added to `ViewController.swift` +- [ ] New Swift files are in the `APIExample` target's Sources build phase +- [ ] New storyboards, localized files, and media assets are in the target's Resources build phase - [ ] `leaveChannel()` + `AgoraRtcEngineKit.destroy()` called in `willMove(toParent:)` when `parent == nil` - [ ] UI updates inside delegate callbacks dispatched to `DispatchQueue.main` - [ ] Camera/microphone permissions requested before `joinChannel()` @@ -180,6 +190,7 @@ Key APIs: list 2–5 core SDK methods the case demonstrates. Do not list `joinCh - NEVER call `leaveChannel` or `destroy` in `viewDidDisappear` — use `willMove(toParent:)` with `parent == nil` - NEVER update UI directly inside `AgoraRtcEngineDelegate` callbacks — always `DispatchQueue.main.async { }` - NEVER add a new scene to `Main.storyboard` — each case must have its own `.storyboard` file +- NEVER add a source or resource file without adding it to the `APIExample` target - NEVER share an `AgoraRtcEngineKit` instance between cases - NEVER call `joinChannel` before requesting camera/microphone permissions - NEVER skip updating the Case Index in `ARCHITECTURE.md` diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index 21429b325..809a1bb05 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -164,9 +164,11 @@ struct MenuItem { Most examples use path 2 (their own storyboard). -**To add a case, edit exactly two things:** -1. Add a `MenuItem` to the `menus` array in `ViewController.swift` -2. Create the example folder under `Examples/Basic/` or `Examples/Advanced/` with the Swift file(s) and storyboard +A registered case connects the `MenuItem`, its Swift controller(s), and its storyboard. +New source files must belong to the Xcode target's Sources build phase, and storyboards, +localized files, and media assets must belong to Resources. Follow +[upsert-case](.agents/skills/upsert-case/SKILL.md) for the complete change procedure and +update the Case Index when the case changes. ## Entry/Main ViewController Pattern @@ -190,11 +192,15 @@ viewDidLoad → AgoraRtcEngineKit.sharedEngine(withAppId:delegate:) ↓ [AgoraRtcEngineDelegate callbacks — may be on background thread] ↓ -viewDidDisappear / willMove(toParent:) +willMove(toParent:) when parent == nil → engine.leaveChannel() → AgoraRtcEngineKit.destroy() ``` +For navigation-based cases, cleanup runs when the controller is removed from its parent. +Temporary disappearance alone must not destroy the engine; follow the guarded cleanup in +the upsert and review skills. + ## Token Flow ```swift diff --git a/macOS/.agents/skills/review-case/SKILL.md b/macOS/.agents/skills/review-case/SKILL.md index 92d813ec2..bba3eb4ef 100644 --- a/macOS/.agents/skills/review-case/SKILL.md +++ b/macOS/.agents/skills/review-case/SKILL.md @@ -3,7 +3,6 @@ name: review-case description: > Code review for API examples. Ensures examples follow project conventions, handle lifecycle correctly, manage threads safely, and use APIs properly. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -11,370 +10,100 @@ metadata: platform: macOS --- -# Review Case Skill — macOS +# Review Case — macOS -## When to Use +Review the selected AppKit case against its behavior contract, current SDK headers, and +live source. Use `APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift`, +`APIExample/Common/NetworkManager/NetworkManager.swift`, and the project `AGENTS.md` as +references. Report findings first; do not silently rewrite the case during review. -Use this skill when you need to: -- Review a new or modified example for correctness -- Ensure the example follows project conventions -- Verify lifecycle management and thread safety -- Check API usage and error handling +## Lifecycle and Ownership -## Review Dimensions (Priority Order) +- Create the case engine once with `AgoraRtcEngineKit.sharedEngine(with:delegate:)` and an + `AgoraRtcEngineConfig` containing `KeyCenter.AppId`. +- The main controller inherits `BaseViewController`; the menu host calls + `viewWillBeRemovedFromSplitView()` when replacing its content. +- On removal, invalidate pending Token/join work, leave the channel, stop case-owned media, + call `AgoraRtcEngineKit.destroy()`, and clear the engine reference. Repeated cleanup must + be safe. A late asynchronous response must not use the destroyed engine. +- Complete destruction before another case creates an engine. Never destroy from an SDK + delegate callback. -### 1. Engine Lifecycle (CRITICAL) - -**Check:** -- [ ] Engine is created in `initializeAgoraEngine()` or similar -- [ ] Engine is initialized with `AgoraRtcEngineConfig` -- [ ] `leaveChannel()` is called before `destroy()` -- [ ] `destroy()` is called in `viewWillClose()` or cleanup method -- [ ] No engine leaks (engine not recreated on every join) - -**Correct Pattern:** -```swift -override func viewDidLoad() { - super.viewDidLoad() - initializeAgoraEngine() // Create once -} - -override func viewWillClose() { - leaveChannel() - agoraKit.destroy() - super.viewWillClose() -} - -func joinChannel() { - agoraKit.joinChannel(byToken: token, channelName: channel, info: nil, uid: 0) -} - -func leaveChannel() { - agoraKit.leaveChannel(nil) -} -``` - -**Incorrect Pattern:** -See `references/incorrect-lifecycle.swift` for common mistakes. - ---- - -### 2. Thread Safety (CRITICAL) - -**Check:** -- [ ] All UI updates in delegate callbacks use `DispatchQueue.main.async` -- [ ] No direct UI updates from background threads -- [ ] Video/audio frame callbacks dispatch to main thread before updating UI - -**Correct Pattern:** -```swift -func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - // Callback may arrive on background thread - DispatchQueue.main.async { - self.statusLabel.stringValue = "Joined channel" - } -} -``` - -**Incorrect Pattern:** -See `references/incorrect-thread-safety.swift` for common mistakes. - ---- - -### 3. Permission Handling (HIGH) - -**Check:** -- [ ] Microphone permission requested before `enableAudio()` -- [ ] Camera permission requested before `enableVideo()` -- [ ] Permissions checked before accessing devices -- [ ] Review guidance stays macOS-specific and does not suggest iOS-only APIs such as `AVAudioSession.sharedInstance().requestRecordPermission` - -**Correct Pattern:** -```swift -func initializeAgoraEngine() { - // Request permissions first - AVCaptureDevice.requestAccess(for: .video) { granted in - if granted { - self.agoraKit.enableVideo() - } - } - - AVCaptureDevice.requestAccess(for: .audio) { granted in - if granted { - self.agoraKit.enableAudio() - } - } -} -``` - ---- - -### 4. Error Handling (HIGH) - -**Check:** -- [ ] `joinChannel()` failures are handled -- [ ] Token expiration is handled -- [ ] Network errors are logged or displayed -- [ ] Invalid parameters are validated - -**Correct Pattern:** ```swift -func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - DispatchQueue.main.async { - self.showError("Error: \(errorCode.rawValue)") +override func viewWillBeRemovedFromSplitView() { + // Invalidate outstanding requests using the case's request ID or cancellation state. + agoraKit?.leaveChannel(nil) + if agoraKit != nil { + AgoraRtcEngineKit.destroy() + agoraKit = nil } -} - -func rtcEngine(_ engine: AgoraRtcEngineKit, tokenPrivilegeWillExpire token: String) { - // Refresh token before expiration - let newToken = KeyCenter.Token(channelName: self.channelName) - self.agoraKit.renewToken(newToken) -} -``` - ---- - -### 5. Code Convention (MEDIUM) - -**Check:** -- [ ] Class name follows pattern: `Main` -- [ ] Extends `BaseViewController` -- [ ] File name matches class name (PascalCase) -- [ ] Properties are properly declared with `@IBOutlet` or `var` -- [ ] Methods are organized with `// MARK:` sections -- [ ] Comments explain non-obvious logic - -**Correct Pattern:** -```swift -class ScreenShareMain: BaseViewController { - - var agoraKit: AgoraRtcEngineKit! - var remoteUid: UInt = 0 - - @IBOutlet weak var Container: AGEVideoContainer! - - // MARK: - Lifecycle - override func viewDidLoad() { ... } - - // MARK: - Agora Engine Setup - func initializeAgoraEngine() { ... } - - // MARK: - Actions - @IBAction func joinButtonTapped(_ sender: Any) { ... } + super.viewWillBeRemovedFromSplitView() } ``` ---- - -### 6. API Usage Correctness (MEDIUM) - -**Check:** -- [ ] SDK methods called in correct order -- [ ] Required parameters are provided -- [ ] Optional parameters are used correctly -- [ ] Return values are checked where necessary -- [ ] Deprecated APIs are not used - -**Correct Pattern:** -```swift -// Correct order: enable -> setup -> join -agoraKit.enableVideo() -agoraKit.setupLocalVideo(AgoraRtcVideoCanvas(uid: 0)) -agoraKit.joinChannel(byToken: token, channelName: channel, info: nil, uid: 0) -``` - -**Incorrect Pattern:** -```swift -// ❌ Wrong order -agoraKit.joinChannel(...) // Join first -agoraKit.enableVideo() // Enable after join (too late) -``` - ---- - -### 7. Resource Cleanup (MEDIUM) - -**Check:** -- [ ] Audio files are stopped and released -- [ ] Video captures are stopped -- [ ] Custom audio/video sources are cleaned up -- [ ] Observers are unregistered -- [ ] Timers are invalidated - -**Correct Pattern:** -```swift -func leaveChannel() { - agoraKit.stopAudioMixing() // Stop audio - agoraKit.stopScreenCapture() // Stop screen share - agoraKit.leaveChannel(nil) -} - -override func viewWillClose() { - leaveChannel() - agoraKit.destroy() - super.viewWillClose() -} -``` - ---- - -## Review Output Format - -When reviewing, provide feedback in this format: - -``` -## Review Results - -### ✅ Passed -- Engine lifecycle correctly managed -- Thread safety ensured with DispatchQueue.main.async -- Permissions requested before device access - -### ⚠️ Issues Found - -**[HIGH] Thread Safety Issue** -- File: `ScreenShare.swift` -- Line: 45 -- Issue: UI update in delegate callback without DispatchQueue.main.async -- Suggestion: Wrap UI update with `DispatchQueue.main.async { ... }` - -**[MEDIUM] Missing Error Handling** -- File: `ScreenShare.swift` -- Line: 78 -- Issue: joinChannel() result not checked -- Suggestion: Implement `rtcEngine(_:didOccurError:)` delegate method - -### 🔧 Recommendations -- Add logging for debugging -- Consider adding retry logic for network failures -``` - ---- - -## Platform-Specific Checks - -### macOS-Specific - -**Check:** -- [ ] Using Cocoa (AppKit) — not UIKit or SwiftUI -- [ ] Window/view lifecycle properly handled -- [ ] No Combine or async/await unless already in codebase -- [ ] Storyboard/XIB files properly configured if used - -**Correct Pattern:** -```swift -// macOS: Use Cocoa -import Cocoa -import AgoraRtcKit - -class ExampleMain: BaseViewController { - @IBOutlet weak var Container: AGEVideoContainer! - // Cocoa-based UI -} -``` - -**Incorrect Pattern:** -```swift -// ❌ iOS patterns in macOS -import UIKit // Wrong framework -class ExampleMain: UIViewController { } // Wrong base class -``` - ---- - -## NEVER List - -**Do NOT accept:** -- Engine not destroyed (memory leak) -- UI updates from background threads without DispatchQueue.main.async -- Multiple engine instances in one example -- Hardcoded App ID or token (must use KeyCenter) -- Missing `leaveChannel()` before `destroy()` -- Objective-C files (Swift only) -- UIKit or SwiftUI (Cocoa only) -- Examples outside `APIExample/Examples/[Basic|Advanced]/` structure -- Missing delegate implementation for event handling -- No error handling for joinChannel failures - ---- - -## Review Checklist - -Use this checklist when reviewing an example: - -**Lifecycle:** -- [ ] Engine created once in initialization -- [ ] `leaveChannel()` called before `destroy()` -- [ ] `destroy()` called in cleanup -- [ ] No engine leaks - -**Thread Safety:** -- [ ] All UI updates use `DispatchQueue.main.async` -- [ ] No direct UI updates from callbacks -- [ ] Frame callbacks dispatch to main thread - -**Permissions:** -- [ ] Microphone permission requested -- [ ] Camera permission requested -- [ ] Permissions checked before use - -**Error Handling:** -- [ ] joinChannel failures handled -- [ ] Token expiration handled -- [ ] Network errors logged - -**Code Quality:** -- [ ] Follows naming conventions -- [ ] Properly organized with MARK sections -- [ ] Comments explain non-obvious logic -- [ ] No hardcoded credentials - -**API Usage:** -- [ ] Methods called in correct order -- [ ] Required parameters provided -- [ ] Return values checked -- [ ] No deprecated APIs - -**Resources:** -- [ ] Audio/video properly stopped -- [ ] Observers unregistered -- [ ] Timers invalidated -- [ ] No resource leaks - -**Platform:** -- [ ] Using Cocoa (AppKit) -- [ ] No UIKit or SwiftUI -- [ ] Window lifecycle handled -- [ ] No modern C++ patterns unless existing - ---- - -## Common Issues and Fixes - -### Issue: "Engine not initialized" -**Cause:** `destroy()` called without `leaveChannel()` first -**Fix:** Always call `leaveChannel()` before `destroy()` - -### Issue: "UI updates crash the app" -**Cause:** Direct UI update from background thread -**Fix:** Wrap with `DispatchQueue.main.async { ... }` - -### Issue: "Memory leak detected" -**Cause:** `destroy()` not called or engine recreated -**Fix:** Ensure `destroy()` in `viewWillClose()` and create engine once - -### Issue: "Token expired error" -**Cause:** No token refresh handling -**Fix:** Implement `tokenPrivilegeWillExpire()` delegate method - -### Issue: "No audio/video" -**Cause:** Permissions not requested -**Fix:** Request permissions before `enableAudio()` / `enableVideo()` - ---- - -## References - -- **Agora RTC SDK for macOS:** [Documentation](https://docs.agora.io/en/video-calling/reference/macos-sdk) -- **Existing examples:** Review `APIExample/Examples/Basic/JoinChannelVideo/` for reference -- **BaseViewController:** Check `APIExample/Common/` for base class implementation +## Threading and Permissions + +- Dispatch AppKit mutations to the main queue; SDK callbacks may arrive on another thread. + Avoid routing expensive frame processing through the UI queue merely to update a label. +- Request camera/microphone access with `AVCaptureDevice.requestAccess(for:)` before using + the corresponding device; handle denial without starting capture or joining anyway. +- Do not suggest iOS-only permission APIs such as AVAudioSession on macOS. +- Check object/request validity again inside asynchronous permission and Token callbacks. + +## Token and Error Handling + +`KeyCenter` contains AppId and Certificate; it has no Token method. Use the existing +`NetworkManager.shared.generateToken(channelName:uid:success:)` flow or the case's explicit +Token input. Store the active channel and UID in the case, and use the same values when +refreshing. The helper can return nil; do not treat a failed Token request as successful +authentication when the project requires a certificate. + +- Check join/configuration return codes and implement `rtcEngine(_:didOccurError:)`. +- Handle `rtcEngine(_:tokenPrivilegeWillExpire:)` when using expiring tokens. After fetching + a replacement, validate that the case/request is still active before calling + `renewToken(_:)`; do not rejoin the channel solely to renew a token. +- Keep logs and errors useful without exposing App Certificates or tokens. + +## API and Media Correctness + +- Compare exact method names, argument labels, enums, defaults and return values with the + selected SDK. The channel join overload used by the current basic case is + `joinChannel(byToken:channelId:uid:mediaOptions:)`. +- Configure channel profile, client role, capture, canvases and encoder settings as needed + by the contract before joining. Audio-only behavior must not enable video capture. +- Construct a canvas with `AgoraRtcVideoCanvas()`, then assign uid/view/renderMode. Do not + invent convenience initializers. +- On exit stop any audio mixing, preview, screen/camera capture, custom media, player, timer + or observer owned by this case before destroying the engine. + +## Structure and Registration + +- Keep the implementation under `APIExample/Examples/Basic/` or `Advanced/`, using Swift + and AppKit. Follow existing threading and language conventions in the edited code. +- Verify `APIExample/ViewController.swift` supplies the right MenuItem, storyboard name + and controller identifier; confirm the storyboard actually loads that controller. +- New source files must belong to the APIExample Xcode target's Sources build phase; + storyboards, localized files and media assets must belong to Resources. Existing-file + edits need no project-file change unless a build input was added or moved. +- Update the Case Index when paths, behavior or key APIs change. Check source before relying + on an index entry to establish implementation or registration completeness. + +## Verification and Output + +Run the project's documented build or the strongest applicable static check for the change. +Use `PASS`, `FAIL`, `BLOCKED` or `NOT_RUN`, recording the command and evidence. Compilation +alone does not establish device permissions, RTC connectivity or lifecycle behavior. + +For each actionable finding provide severity, file/line, trigger, impact and a concrete +correction. Include unresolved validation and intentional platform differences. Do not +approve leaked engines, UI mutations off the main thread, missing cleanup, invalid target +membership or hardcoded credentials. + +## Reference Snippets + +These are illustrative fragments to adapt to the selected case and its real ownership: + +- `correct-lifecycle`: [snippet](references/correct-lifecycle.swift) +- `incorrect-lifecycle`: [snippet](references/incorrect-lifecycle.swift) +- `correct-thread-safety`: [snippet](references/correct-thread-safety.swift) +- `incorrect-thread-safety`: [snippet](references/incorrect-thread-safety.swift) diff --git a/macOS/.agents/skills/review-case/references/correct-lifecycle.swift b/macOS/.agents/skills/review-case/references/correct-lifecycle.swift index 853b274f6..ba7327aa5 100644 --- a/macOS/.agents/skills/review-case/references/correct-lifecycle.swift +++ b/macOS/.agents/skills/review-case/references/correct-lifecycle.swift @@ -1,20 +1,20 @@ -// Correct Engine Lifecycle Pattern +// Fragments from the case controller; request tracking is part of the controller state. +// See the upsert-case example-template.swift for a complete skeleton. override func viewDidLoad() { super.viewDidLoad() - initializeAgoraEngine() // Create once + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) } -override func viewWillClose() { - leaveChannel() - super.viewWillClose() -} - -func joinChannel() { - agoraKit.joinChannel(byToken: token, channelName: channel, info: nil, uid: 0) -} - -func leaveChannel() { - agoraKit.leaveChannel(nil) - agoraKit.destroy() +override func viewWillBeRemovedFromSplitView() { + tokenRequestID += 1 // Late Token callbacks must check this ID and the live engine. + agoraKit?.leaveChannel(nil) + // Stop case-owned media, timers and observers before destruction. + if agoraKit != nil { + AgoraRtcEngineKit.destroy() + agoraKit = nil + } + super.viewWillBeRemovedFromSplitView() } diff --git a/macOS/.agents/skills/review-case/references/correct-thread-safety.swift b/macOS/.agents/skills/review-case/references/correct-thread-safety.swift index 6e46a7b49..921e57dd8 100644 --- a/macOS/.agents/skills/review-case/references/correct-thread-safety.swift +++ b/macOS/.agents/skills/review-case/references/correct-thread-safety.swift @@ -2,7 +2,8 @@ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { // Callback may arrive on background thread - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } self.statusLabel.stringValue = "Joined channel" } } diff --git a/macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift b/macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift index 81180c567..e1951fba0 100644 --- a/macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift +++ b/macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift @@ -5,7 +5,8 @@ func joinChannel() { } // ❌ INCORRECT: Not calling destroy -override func viewWillClose() { +override func viewWillBeRemovedFromSplitView() { agoraKit.leaveChannel(nil) - // Missing: agoraKit.destroy() + // Missing: AgoraRtcEngineKit.destroy() + super.viewWillBeRemovedFromSplitView() } diff --git a/macOS/.agents/skills/upsert-case/SKILL.md b/macOS/.agents/skills/upsert-case/SKILL.md index 4e7b61909..c6e2f2d37 100644 --- a/macOS/.agents/skills/upsert-case/SKILL.md +++ b/macOS/.agents/skills/upsert-case/SKILL.md @@ -2,8 +2,8 @@ name: upsert-case description: > Add a new API example or modify an existing one. Covers both creation and modification scenarios, - including file structure, per-example storyboard creation, registration, and ARCHITECTURE.md updates. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] + including file structure, per-example storyboard creation, registration, Xcode target membership, + and ARCHITECTURE.md updates. license: MIT metadata: author: APIExample Team @@ -32,8 +32,9 @@ Use this skill when you need to: 3. Create the Swift implementation file 4. Create the example storyboard 5. Register the example in `ViewController.swift` -6. Update `ARCHITECTURE.md` Case Index -7. Verify compilation and functionality +6. Add new source and resource files to the `APIExample` Xcode target +7. Update `ARCHITECTURE.md` Case Index +8. Verify compilation and functionality ### Scenario 2: Modify an Existing Example @@ -57,6 +58,7 @@ Use this skill when you need to: | `APIExample/Examples/[Basic\|Advanced]//.swift` | Create | Main implementation file | | `APIExample/Examples/[Basic\|Advanced]//Base.lproj/.storyboard` | Create | Example UI and controller identifier | | `APIExample/ViewController.swift` | Modify | Register example in menu/list | +| `APIExample.xcodeproj/project.pbxproj` | Modify | Add source and resources to the `APIExample` target | | `ARCHITECTURE.md` | Modify | Add entry to Case Index | ### Modify Existing Example @@ -109,7 +111,15 @@ MenuItem(name: "Example Name".localized, Place it in the correct section (Basic / Advanced). -### Step 6: Update ARCHITECTURE.md +### Step 6: Add Files to the Xcode Target + +This project uses explicit Xcode groups and build phases. Update +`APIExample.xcodeproj/project.pbxproj` so the new Swift file belongs to the `APIExample` +target's Sources build phase and the storyboard belongs to its Resources build phase. Add +new localized or media resources to Resources as well. Existing-file edits do not require +a project-file change unless a build input was added or moved. + +### Step 7: Update ARCHITECTURE.md Add a new row to the Case Index table in `ARCHITECTURE.md`: @@ -119,14 +129,16 @@ Add a new row to the Case Index table in `ARCHITECTURE.md`: **Key APIs column:** List 2-5 core SDK methods used in this example. -### Step 7: Verify +### Step 8: Verify - [ ] Code compiles without errors - [ ] Example appears in the menu/list - [ ] Example can join channel and receive callbacks -- [ ] `leaveChannel()` and `destroy()` are called on close +- [ ] `leaveChannel()` and `AgoraRtcEngineKit.destroy()` are called in `viewWillBeRemovedFromSplitView()` - [ ] UI updates happen on main thread - [ ] Storyboard loads with the expected controller identifier +- [ ] New Swift files are in the `APIExample` target's Sources build phase +- [ ] New storyboards, localized files, and media assets are in the target's Resources build phase - [ ] ARCHITECTURE.md Case Index is updated --- @@ -147,11 +159,12 @@ See `references/` directory for code patterns: - Create multiple engine instances in one example — use a single shared instance - Use Objective-C files — Swift only - Use UIKit or SwiftUI — Cocoa (AppKit) only -- Hardcode App ID or token — use `KeyCenter` +- Hardcode App ID or token — read App ID from `KeyCenter` and obtain a Token through the existing asynchronous helper or explicit user input - Forget to implement `AgoraRtcEngineDelegate` for event handling - Leave the channel without calling `leaveChannel()` first - Modify examples outside the `APIExample/Examples/[Basic|Advanced]/` structure - Register a new menu item without also creating the per-example storyboard under the same example folder +- Add a source or resource file without adding it to the `APIExample` target - Forget to update `ARCHITECTURE.md` Case Index after adding/modifying an example --- @@ -166,9 +179,10 @@ After completing the upsert, verify: - [ ] Storyboard exists at `APIExample/Examples/[Basic|Advanced]//Base.lproj/.storyboard` - [ ] Storyboard identifier matches the `controller` value registered in `ViewController.swift` - [ ] Example is registered in `ViewController.swift` +- [ ] New source and resource files belong to the `APIExample` target - [ ] `initializeAgoraEngine()` creates engine with correct config -- [ ] `joinChannel()` uses token from `KeyCenter` -- [ ] `leaveChannel()` and `destroy()` are called in `viewWillClose()` +- [ ] `joinChannel()` uses the active channel/UID and a Token from `NetworkManager` or explicit user input; late responses are invalidated on exit +- [ ] `leaveChannel()` and `AgoraRtcEngineKit.destroy()` are called in `viewWillBeRemovedFromSplitView()` - [ ] All delegate callbacks dispatch UI updates to main thread - [ ] `ARCHITECTURE.md` Case Index includes new/updated example - [ ] Code compiles without warnings or errors diff --git a/macOS/.agents/skills/upsert-case/references/example-template.swift b/macOS/.agents/skills/upsert-case/references/example-template.swift index 1c0c8327e..3c2e792d3 100644 --- a/macOS/.agents/skills/upsert-case/references/example-template.swift +++ b/macOS/.agents/skills/upsert-case/references/example-template.swift @@ -1,62 +1,78 @@ import Cocoa import AgoraRtcKit -import AGEVideoLayout +// Skeleton: replace and wire the case UI/storyboard and permissions. +// This minimal join publishes microphone audio. Configure video/canvases only if needed. class Main: BaseViewController { - - var agoraKit: AgoraRtcEngineKit! - var remoteUid: UInt = 0 - - @IBOutlet weak var Container: AGEVideoContainer! - - // MARK: - Lifecycle - + var agoraKit: AgoraRtcEngineKit? + private var tokenRequestID = 0 + override func viewDidLoad() { super.viewDidLoad() - initializeAgoraEngine() - } - - override func viewWillClose() { - leaveChannel() - super.viewWillClose() - } - - // MARK: - Agora Engine Setup - - func initializeAgoraEngine() { let config = AgoraRtcEngineConfig() config.appId = KeyCenter.AppId - config.areaCode = .global - - agoraKit = AgoraRtcEngineKit(config: config, delegate: self) - agoraKit.enableVideo() - agoraKit.enableAudio() + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) } - - func joinChannel() { - let token = KeyCenter.Token(channelName: "test") - agoraKit.joinChannel(byToken: token, channelName: "test", info: nil, uid: 0) + + // Call on the main queue after the case has obtained microphone permission. + func joinChannel(channelName: String) { + guard !channelName.isEmpty, agoraKit != nil else { return } + tokenRequestID += 1 + let requestID = tokenRequestID + NetworkManager.shared.generateToken(channelName: channelName, uid: 0) { [weak self] token in + DispatchQueue.main.async { [weak self] in + guard let self = self, self.tokenRequestID == requestID, + let engine = self.agoraKit else { return } + if !(KeyCenter.Certificate ?? "").isEmpty && (token ?? "").isEmpty { + LogUtils.log(message: "Token request failed", level: .error) + return + } + let options = AgoraRtcChannelMediaOptions() + options.channelProfile = .liveBroadcasting + options.clientRoleType = .broadcaster + options.publishMicrophoneTrack = true + options.publishCameraTrack = false + let result = engine.joinChannel(byToken: token, channelId: channelName, + uid: 0, mediaOptions: options) + if result != 0 { + LogUtils.log(message: "joinChannel failed: \(result)", level: .error) + } + } + } } - + func leaveChannel() { - agoraKit.leaveChannel(nil) - agoraKit.destroy() + tokenRequestID += 1 // Discard Token responses after leave or scene removal. + agoraKit?.leaveChannel(nil) } -} -// MARK: - AgoraRtcEngineDelegate + override func viewWillBeRemovedFromSplitView() { + leaveChannel() + // Stop any case-owned preview, capture, player, timer or observer here. + if agoraKit != nil { + AgoraRtcEngineKit.destroy() + agoraKit = nil + } + super.viewWillBeRemovedFromSplitView() + } +} extension Main: AgoraRtcEngineDelegate { - - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - // Handle join success + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, + withUid uid: UInt, elapsed: Int) { + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Joined channel with UID \(uid)", level: .info) + } } - - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - // Handle remote user joined - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - // Handle remote user offline + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "RTC error: \(errorCode.rawValue)", level: .error) + } } + + // For expiring tokens, implement tokenPrivilegeWillExpire using the same channel/UID, + // validate the pending request, then call renewToken(_:). See review-case/SKILL.md. } diff --git a/macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift b/macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift index 3821b2c3f..139789b4c 100644 --- a/macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift +++ b/macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift @@ -1,13 +1,23 @@ -// Proper Engine Lifecycle Pattern +// Lifecycle fragments inside a BaseViewController + AgoraRtcEngineDelegate case. +// See example-template.swift for the complete skeleton and asynchronous Token guards. -// Create +// Create once when the case controller loads. let config = AgoraRtcEngineConfig() config.appId = KeyCenter.AppId -agoraKit = AgoraRtcEngineKit(config: config, delegate: self) +agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) -// Use -agoraKit.joinChannel(byToken: token, channelName: channel, info: nil, uid: 0) +// After permission/Token resolution, verify the case and request are still active. +// token, channelName, uid and options come from the case's current request. +let result = agoraKit?.joinChannel(byToken: token, channelId: channelName, + uid: uid, mediaOptions: options) +// Handle a nonzero result before updating UI to a joined state. -// Cleanup -agoraKit.leaveChannel(nil) -agoraKit.destroy() +// In viewWillBeRemovedFromSplitView(), on the main queue: +tokenRequestID += 1 +agoraKit?.leaveChannel(nil) +// Stop case-owned media, timers and observers before destruction. +if agoraKit != nil { + AgoraRtcEngineKit.destroy() + agoraKit = nil +} +// Call super.viewWillBeRemovedFromSplitView() last. diff --git a/macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift b/macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift index 6658ba75e..b6da0aba2 100644 --- a/macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift +++ b/macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift @@ -3,7 +3,8 @@ extension ExampleMain: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { // Callbacks may arrive on background thread - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } self.updateUI() } } diff --git a/macOS/AGENTS.md b/macOS/AGENTS.md index e12785572..70da4fa2d 100644 --- a/macOS/AGENTS.md +++ b/macOS/AGENTS.md @@ -25,13 +25,15 @@ Configure your Agora App ID in `APIExample/Common/KeyCenter.swift`: struct KeyCenter { static let AppId: String = "<#YOUR_APP_ID#>" - // Token is optional for testing, but required for production - static func Token(channelName: String) -> String { - return "<#YOUR_TOKEN#>" - } + static let Certificate: String? = nil } ``` +The current KeyCenter has AppId and Certificate only. Cases obtain tokens through +`NetworkManager.shared.generateToken(channelName:uid:success:)` in +`APIExample/Common/NetworkManager/NetworkManager.swift`. Never stage real credentials in +this tracked configuration file. + ## Architecture Red Lines **Do NOT:** diff --git a/macOS/ARCHITECTURE.md b/macOS/ARCHITECTURE.md index 230634b4d..a911585a0 100644 --- a/macOS/ARCHITECTURE.md +++ b/macOS/ARCHITECTURE.md @@ -69,7 +69,7 @@ All examples are registered in `APIExample/ViewController.swift` via a menu or l ### Common Utilities All examples share utilities from `APIExample/Common/`: -- `KeyCenter` — App ID and token +- `KeyCenter` — App ID and optional Certificate - `GlobalSettings` — Shared runtime configuration - `LogUtils` — SDK log path - `Util` — Privatization configuration @@ -116,39 +116,28 @@ All examples share utilities from `APIExample/Common/`: ## Engine Lifecycle -``` -1. Create Engine - createAgoraRtcEngine() - -2. Initialize Engine - initialize(AgoraRtcEngineConfig) - -3. Enable Features (optional) - enableVideo(), enableAudio() - -4. Setup Local Media (optional) - setupLocalVideo(), startAudioMixing() - -5. Join Channel - joinChannel(token, channelName, uid) - -6. Handle Callbacks - onJoinChannelSuccess(), onUserJoined(), onUserOffline() - -7. Leave Channel - leaveChannel() - -8. Destroy Engine - destroy() -``` +The main case controller creates its engine using +`AgoraRtcEngineKit.sharedEngine(with:delegate:)` and `AgoraRtcEngineConfig`. After media +configuration and permission checks, it joins with +`joinChannel(byToken:channelId:uid:mediaOptions:)`. -## Token Flow +The host calls `viewWillBeRemovedFromSplitView()` when switching cases. Cleanup invalidates +pending Token/join requests, stops case-owned media, calls `leaveChannel(nil)` and +`AgoraRtcEngineKit.destroy()`, then clears the engine reference. SDK delegate callbacks may +arrive on background threads; AppKit updates go to the main queue. -Token is obtained from `KeyCenter.swift` and passed to `joinChannel()`: +See [upsert-case](.agents/skills/upsert-case/SKILL.md) and +[review-case](.agents/skills/review-case/SKILL.md) for implementation and verification steps. -```swift -let token = KeyCenter.Token(channelName: channelName) -agoraKit.joinChannel(byToken: token, channelName: channelName, info: nil, uid: 0) -``` +## Token Flow + +`KeyCenter` supplies AppId and optional Certificate. The basic case uses +`NetworkManager.shared.generateToken(channelName:uid:success:)` to obtain a Token for the +same channel and UID passed to `joinChannel(byToken:channelId:uid:mediaOptions:)`. A case may +also expose explicit Token input. There is no `KeyCenter.Token` API. -For production, tokens should be generated server-side and refreshed before expiration. +When Certificate is absent, the helper returns nil for projects that do not require Token +authentication. When authentication is required, handle a failed request explicitly, and +refresh expiring tokens using `renewToken(_:)`. Before using an asynchronous response, +confirm the request still belongs to the active case and engine. Production tokens should +be generated on a server. diff --git a/windows/.agents/skills/review-case/SKILL.md b/windows/.agents/skills/review-case/SKILL.md index ead1ccdfa..2122619aa 100644 --- a/windows/.agents/skills/review-case/SKILL.md +++ b/windows/.agents/skills/review-case/SKILL.md @@ -3,7 +3,6 @@ name: review-case description: > Code review for API examples. Ensures examples follow project conventions, handle lifecycle correctly, manage threads safely, and use APIs properly. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -11,451 +10,98 @@ metadata: platform: Windows --- -# Review Case Skill — Windows - -## When to Use - -Use this skill when you need to: -- Review a new or modified example for correctness -- Ensure the example follows project conventions -- Verify lifecycle management and thread safety -- Check API usage and error handling - -## Review Dimensions (Priority Order) - -### 1. Engine Lifecycle (CRITICAL) - -**Check:** -- [ ] Engine is created in `InitializeAgoraEngine()` or similar -- [ ] Engine is initialized with `RtcEngineContext` -- [ ] `leaveChannel()` is called before `release()` -- [ ] `release()` is called in the case's real cleanup path -- [ ] No engine leaks (engine not recreated on every join) - -**Correct Pattern A — standalone dialog teardown:** -```cpp -BOOL CExampleDlg::OnInitDialog() { - CDialogEx::OnInitDialog(); - InitializeAgoraEngine(); // Create once - return TRUE; -} - -void CExampleDlg::PostNcDestroy() { - LeaveChannel(); - if (m_rtcEngine) { - m_rtcEngine->release(); - m_rtcEngine = nullptr; - } - CDialogEx::PostNcDestroy(); - delete this; -} - -void CExampleDlg::JoinChannel() { - if (!m_rtcEngine) return; - m_rtcEngine->joinChannel(token, channelName, "", 0); -} - -void CExampleDlg::LeaveChannel() { - if (!m_rtcEngine) return; - m_rtcEngine->leaveChannel(); -} -``` - -**Correct Pattern B — scene-switching dialog teardown:** -```cpp -bool CExampleDlg::InitAgora() { - m_rtcEngine = createAgoraRtcEngine(); - // initialize once when the scene becomes active - return m_rtcEngine != nullptr; -} - -void CExampleDlg::UnInitAgora() { - if (!m_rtcEngine) return; - if (m_joinChannel) { - m_rtcEngine->leaveChannel(); - } - m_rtcEngine->release(nullptr); - m_rtcEngine = nullptr; -} - -void CExampleDlg::OnShowWindow(BOOL bShow, UINT nStatus) { - CDialogEx::OnShowWindow(bShow, nStatus); - if (!bShow) { - UnInitAgora(); - } -} -``` - -Accept either pattern as long as the dialog follows one lifecycle consistently and does not leak the engine across scene switches. - -**Incorrect Pattern:** -See `references/incorrect-lifecycle.cpp` for common mistakes. - ---- - -### 2. Thread Safety (CRITICAL) - -**Check:** -- [ ] All UI updates in event handler use message map pattern -- [ ] Event handler posts messages to main thread via `PostMessage()` -- [ ] No direct UI updates from background threads -- [ ] Message handlers update UI on main thread - -**Correct Pattern:** -```cpp -// Event handler (may be called from background thread) -void CExampleRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) { - if (m_hMsgHandler) { - // Post message to main thread - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), (WPARAM)uid, 0); - } -} - -// Message handler (runs on main thread) -LRESULT CExampleDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { - // Safe to update UI here - m_statusText.SetWindowText(_T("Joined channel")); - return 0; -} -``` - -**Incorrect Pattern:** -See `references/incorrect-thread-safety.cpp` for common mistakes. - ---- - -### 3. Permission Handling (HIGH) - -**Check:** -- [ ] Microphone permission checked before `enableAudio()` -- [ ] Camera permission checked before `enableVideo()` -- [ ] Device availability verified - -**Correct Pattern:** -```cpp -void CExampleDlg::InitializeAgoraEngine() { - m_rtcEngine = createAgoraRtcEngine(); - if (!m_rtcEngine) return; - - RtcEngineContext context; - context.appId = CConfig::GetAppId(); - context.eventHandler = &m_eventHandler; - m_eventHandler.SetMsgReceiver(m_hWnd); - - m_rtcEngine->initialize(context); - - // Check device availability - if (m_rtcEngine->enableVideo() == 0) { - // Video enabled successfully - } - if (m_rtcEngine->enableAudio() == 0) { - // Audio enabled successfully - } -} -``` - ---- - -### 4. Error Handling (HIGH) - -**Check:** -- [ ] `joinChannel()` return value checked -- [ ] Token expiration is handled -- [ ] Network errors are logged or displayed -- [ ] Invalid parameters are validated -- [ ] `onError()` callback implemented - -**Correct Pattern:** -```cpp -void CExampleDlg::JoinChannel() { - if (!m_rtcEngine) return; - - const char* token = CConfig::GetToken("test"); - int ret = m_rtcEngine->joinChannel(token, "test", "", 0); - if (ret != 0) { - // Handle error - MessageBox(_T("Failed to join channel"), _T("Error")); - } -} - -void CExampleRtcEngineEventHandler::onError(int err) { - if (m_hMsgHandler) { - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_ERROR), (WPARAM)err, 0); - } -} - -LRESULT CExampleDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { - if (wParam == EID_ERROR) { - int errorCode = (int)lParam; - // Handle error - } - return 0; -} -``` - ---- - -### 5. Code Convention (MEDIUM) - -**Check:** -- [ ] Dialog class name follows pattern: `CDlg` -- [ ] Event handler class name: `CRtcEngineEventHandler` -- [ ] File names match class names (PascalCase with C prefix) -- [ ] Member variables use `m_` prefix -- [ ] Message map properly defined -- [ ] Comments explain non-obvious logic - -**Correct Pattern:** -```cpp -// Header: CScreenShareDlg.h -class CScreenShareRtcEngineEventHandler : public IRtcEngineEventHandler { - // ... -}; - -class CScreenShareDlg : public CDialogEx { - DECLARE_DYNAMIC(CScreenShareDlg) - -private: - IRtcEngine* m_rtcEngine = nullptr; - CScreenShareRtcEngineEventHandler m_eventHandler; - uid_t m_remoteUid = 0; - bool m_isJoined = false; - - BEGIN_MESSAGE_MAP(CScreenShareDlg, CDialogEx) - ON_BN_CLICKED(IDC_BUTTON_JOIN, &CScreenShareDlg::OnBnClickedButtonJoin) - END_MESSAGE_MAP() -}; -``` - ---- - -### 6. API Usage Correctness (MEDIUM) - -**Check:** -- [ ] SDK methods called in correct order -- [ ] Required parameters are provided -- [ ] Optional parameters are used correctly -- [ ] Return values are checked where necessary -- [ ] Deprecated APIs are not used - -**Correct Pattern:** -```cpp -// Correct order: create -> initialize -> enable -> join -m_rtcEngine = createAgoraRtcEngine(); -m_rtcEngine->initialize(context); -m_rtcEngine->enableVideo(); -m_rtcEngine->enableAudio(); -m_rtcEngine->joinChannel(token, channelName, "", 0); -``` - -**Incorrect Pattern:** -```cpp -// ❌ Wrong order -m_rtcEngine->joinChannel(...); // Join first -m_rtcEngine->enableVideo(); // Enable after join (too late) -``` - ---- - -### 7. Resource Cleanup (MEDIUM) - -**Check:** -- [ ] Audio files are stopped and released -- [ ] Video captures are stopped -- [ ] Custom audio/video sources are cleaned up -- [ ] Observers are unregistered -- [ ] Timers are killed - -**Correct Pattern:** -```cpp -void CExampleDlg::LeaveChannel() { - if (!m_rtcEngine) return; - - m_rtcEngine->stopAudioMixing(); // Stop audio - m_rtcEngine->stopScreenCapture(); // Stop screen share - m_rtcEngine->leaveChannel(); - m_isJoined = false; -} - -void CExampleDlg::PostNcDestroy() { - LeaveChannel(); - if (m_rtcEngine) { - m_rtcEngine->release(); - m_rtcEngine = nullptr; - } - CDialogEx::PostNcDestroy(); - delete this; -} -``` - ---- - -## Review Output Format - -When reviewing, provide feedback in this format: - -``` -## Review Results - -### ✅ Passed -- Engine lifecycle correctly managed -- Thread safety ensured with message map pattern -- Error handling implemented for joinChannel - -### ⚠️ Issues Found - -**[HIGH] Thread Safety Issue** -- File: `CScreenShareDlg.cpp` -- Line: 45 -- Issue: Direct UI update in event handler without PostMessage -- Suggestion: Use PostMessage to post event to main thread - -**[MEDIUM] Missing Error Handling** -- File: `CScreenShareDlg.cpp` -- Line: 78 -- Issue: joinChannel() return value not checked -- Suggestion: Check return value and handle errors - -### 🔧 Recommendations -- Add logging for debugging -- Consider adding retry logic for network failures -``` - ---- - -## Platform-Specific Checks - -### Windows-Specific - -**Check:** -- [ ] Using MFC — not WinForms or WPF -- [ ] Using C++ — not C# -- [ ] Following MFC naming conventions (C prefix, m_ prefix) -- [ ] Message map properly defined -- [ ] Dialog resource properly configured -- [ ] No modern C++ patterns unless already in codebase - -**Correct Pattern:** -```cpp -// Windows: Use MFC -#include "stdafx.h" -#include "APIExample.h" - -class CExampleDlg : public CDialogEx { - DECLARE_DYNAMIC(CExampleDlg) - - BEGIN_MESSAGE_MAP(CExampleDlg, CDialogEx) - ON_BN_CLICKED(IDC_BUTTON_JOIN, &CExampleDlg::OnBnClickedButtonJoin) - END_MESSAGE_MAP() -}; -``` - -**Incorrect Pattern:** -```cpp -// ❌ Non-MFC patterns -using namespace std; // Avoid in MFC -auto ptr = std::make_unique(); // Modern C++ not typical in MFC -``` - ---- - -## NEVER List - -**Do NOT accept:** -- Engine not released (memory leak) -- Direct UI updates from event handler without PostMessage -- Multiple engine instances in one example -- Hardcoded App ID or token (must use CConfig) -- Missing `leaveChannel()` before `release()` -- C# or other languages (C++ only) -- WinForms or WPF (MFC only) -- Examples outside `APIExample/APIExample/[Basic|Advanced]/` structure -- Missing event handler implementation -- No error handling for joinChannel failures -- Deviation from MFC naming conventions - ---- - -## Review Checklist - -Use this checklist when reviewing an example: - -**Lifecycle:** -- [ ] Engine created once in initialization -- [ ] `leaveChannel()` called before `release()` -- [ ] `release()` called in `PostNcDestroy()` -- [ ] No engine leaks - -**Thread Safety:** -- [ ] All UI updates use message map pattern -- [ ] Event handler posts messages via `PostMessage()` -- [ ] No direct UI updates from callbacks -- [ ] Message handlers run on main thread - -**Permissions:** -- [ ] Microphone availability checked -- [ ] Camera availability checked -- [ ] Device errors handled - -**Error Handling:** -- [ ] joinChannel return value checked -- [ ] Token expiration handled -- [ ] Network errors logged -- [ ] onError() callback implemented - -**Code Quality:** -- [ ] Follows MFC naming conventions -- [ ] Message map properly defined -- [ ] Comments explain non-obvious logic -- [ ] No hardcoded credentials - -**API Usage:** -- [ ] Methods called in correct order -- [ ] Required parameters provided -- [ ] Return values checked -- [ ] No deprecated APIs - -**Resources:** -- [ ] Audio/video properly stopped -- [ ] Observers unregistered -- [ ] Timers killed -- [ ] No resource leaks - -**Platform:** -- [ ] Using MFC (not WinForms/WPF) -- [ ] Using C++ (not C#) -- [ ] Following MFC conventions -- [ ] No modern C++ patterns unless existing - ---- - -## Common Issues and Fixes - -### Issue: "Engine not initialized" -**Cause:** `release()` called without `leaveChannel()` first -**Fix:** Always call `leaveChannel()` before `release()` - -### Issue: "UI crashes or doesn't update" -**Cause:** Direct UI update from event handler -**Fix:** Use PostMessage to post event to main thread - -### Issue: "Memory leak detected" -**Cause:** `release()` not called or engine recreated -**Fix:** Ensure `release()` in `PostNcDestroy()` and create engine once - -### Issue: "Token expired error" -**Cause:** No token refresh handling -**Fix:** Implement token refresh in error handler - -### Issue: "No audio/video" -**Cause:** Device not available or not enabled -**Fix:** Check return values of `enableAudio()` / `enableVideo()` - ---- - -## References - -- **Agora RTC SDK for Windows:** [Documentation](https://docs.agora.io/en/video-calling/reference/windows-sdk) -- **Existing examples:** Review `APIExample/APIExample/Basic/JoinChannelVideoByToken/` for reference -- **MFC Documentation:** [Microsoft Foundation Classes](https://docs.microsoft.com/en-us/cpp/mfc/mfc-desktop-applications) -- **Message Map:** [MFC Message Maps](https://docs.microsoft.com/en-us/cpp/mfc/message-maps) +# Review Case — Windows + +Review the selected MFC case against its behavior contract, current SDK headers and live +source. Read `APIExample/APIExample/APIExampleDlg.cpp` for scene ownership and +`APIExample/APIExample/Basic/JoinChannelVideoByToken/` for a nearby implementation. +Report findings first; do not silently rewrite the case during review. + +## Lifecycle and Ownership + +- The host precreates dialogs in `InitSceneDialog()`. `OnInitDialog()` initializes UI only. + A scene creates its engine in public `InitAgora()`, called from `CreateScene()`. +- `ReleaseScene()` calls `UnInitAgora()` before hiding the dialog. Cleanup must leave the + channel, stop case-owned media, release the engine and clear the pointer even if joining + failed or is still pending. Make cleanup idempotent and finish it before another scene + creates an engine. +- For the current SDK, `release(nullptr)` is synchronous; a non-null callback selects + asynchronous release. Never destroy from an SDK callback. If using async release, keep + the event handler alive until completion and serialize the next engine creation. +- Keep the callback receiver stable until synchronous release completes, then detach it + and discard queued messages belonging to the old engine. Prevent late Token requests + from joining after a scene has exited. +- A destructor is a fallback. `PostNcDestroy()` is sufficient only for a standalone window + whose actual exit path destroys it; it cannot replace `UnInitAgora()` for hidden scenes. + Do not add `delete this` to a precreated dialog without verifying host ownership. + +## Threading and Event Messages + +- Implement `IRtcEngineEventHandler`; post SDK events to the UI thread using `PostMessage`. + Never mutate MFC controls or block on a UI-thread response inside SDK callbacks. +- Use event constants from `stdafx.h`, including `EID_JOINCHANNEL_SUCCESS` and `EID_ERROR`. + Each message-map handler must interpret the payload actually posted by its callback. + An error posted in wParam is an error code, not an event ID; the message ID selects the + handler. The SDK error callback signature is `onError(int err, const char* msg)`. +- Copy callback data before asynchronous use; SDK string/struct pointers may expire on + callback return. Free owned payloads on post failure and when discarding queued events. +- Put `DECLARE_MESSAGE_MAP()` inside the class declaration, and `BEGIN_MESSAGE_MAP` / + `END_MESSAGE_MAP` at file scope in the .cpp implementation. + +## Configuration, Tokens and Errors + +- Obtain App ID through `GET_APP_ID`, which wraps + `cs2utf8(CConfig::GetInstance()->GetAPP_ID())`. Keep the UTF-8 string alive through + `initialize()`. CConfig does not provide GetAppId or GetToken static methods. +- Use the case's Token input or an existing asynchronous Token helper with the same + channel/UID as the join. Never embed real credentials in source, examples or logs. +- Verify current join overloads, such as `joinChannel(token, channelId, info, uid)` or + `joinChannel(token, channelId, uid, options)`, against the SDK in this project. +- Check initialization, configuration and join return codes. Release a partially + initialized engine on failure. A zero join return value means the request was accepted; + wait for the success callback before marking the UI joined. +- For expiring tokens, handle the SDK Token callbacks, fetch a replacement for the active + request and call `renewToken()` only while that scene/engine is still active. +- Log/display asynchronous errors without confusing every SDK error with a join failure. + Provide a recoverable UI state for denied permissions, invalid input and network failure. + +## Devices, Media and Cleanup + +- Check the devices needed by the case and handle Windows privacy/permission failures. + A successful `enableAudio()` or `enableVideo()` call alone does not prove a device exists + or capture is authorized. Avoid applying mobile permission APIs to an MFC desktop app. +- Configure role/profile, tracks, capture and video canvases for the contract. Do not + enable camera capture for an audio-only case. +- Stop and release case-owned capture, mixing, media players, custom sources, device + collections, observers, timers and worker threads before releasing their dependencies. + +## Structure and Registration + +- Use C++/MFC, the `CDlg` naming pattern and `m_` member prefixes; keep files + under `APIExample/APIExample/Basic/` or `Advanced/` and follow adjacent conventions. +- Verify dialog resources/control IDs, message maps, header includes and host pointers. +- Check all three host paths: `InitSceneDialog()`, `CreateScene()` and `ReleaseScene()`. + Register the label through `Language.h`, `stdafx.cpp`, `en.ini` and `zh-cn.ini`. +- Confirm new source/header files belong to `APIExample.vcxproj` and `.filters`, and that + the dialog resource is included in `APIExample.rc`. Update the Case Index when needed; + use source to verify API names and registration rather than trusting the index alone. + +## Verification and Output + +Run the project's documented build or the strongest applicable static check for the +change. Record commands and `PASS`, `FAIL`, `BLOCKED` or `NOT_RUN`. A header/API check on +another OS does not establish a successful Windows MFC build or RTC session. + +For each actionable finding provide severity, file/line, trigger, impact and a concrete +correction. Include unresolved validation and intentional platform differences. Do not +approve leaked engines, UI mutations from SDK callbacks, stale callback access, invalid +project/resource membership or hardcoded credentials. + +## Reference Snippets + +These are illustrative fragments to adapt to the selected case and its real ownership: + +- `correct-lifecycle`: [snippet](references/correct-lifecycle.cpp) +- `incorrect-lifecycle`: [snippet](references/incorrect-lifecycle.cpp) +- `correct-thread-safety`: [snippet](references/correct-thread-safety.cpp) +- `incorrect-thread-safety`: [snippet](references/incorrect-thread-safety.cpp) diff --git a/windows/.agents/skills/review-case/references/correct-lifecycle.cpp b/windows/.agents/skills/review-case/references/correct-lifecycle.cpp index 1e2b464ac..e610c89f3 100644 --- a/windows/.agents/skills/review-case/references/correct-lifecycle.cpp +++ b/windows/.agents/skills/review-case/references/correct-lifecycle.cpp @@ -1,27 +1,20 @@ -// Correct Engine Lifecycle Pattern - +// Methods inside the case dialog; InitAgora() is paired with this cleanup. +// APIExampleDlg precreates the window, then calls InitAgora() only on scene selection. BOOL CExampleDlg::OnInitDialog() { CDialogEx::OnInitDialog(); - InitializeAgoraEngine(); // Create once + // Initialize controls only. return TRUE; } -void CExampleDlg::PostNcDestroy() { - LeaveChannel(); +// ReleaseScene() must call this BEFORE ShowWindow(SW_HIDE). +void CExampleDlg::UnInitAgora() { + // Invalidate pending Token/permission requests first. if (m_rtcEngine) { - m_rtcEngine->release(); + m_rtcEngine->leaveChannel(); + // Stop case-owned media, timers and observers before destruction. + m_rtcEngine->release(nullptr); // Synchronous; keep m_eventHandler alive until return. m_rtcEngine = nullptr; } - CDialogEx::PostNcDestroy(); - delete this; -} - -void CExampleDlg::JoinChannel() { - if (!m_rtcEngine) return; - m_rtcEngine->joinChannel(token, channelName, "", 0); -} - -void CExampleDlg::LeaveChannel() { - if (!m_rtcEngine) return; - m_rtcEngine->leaveChannel(); + m_eventHandler.SetMsgReceiver(nullptr); + DiscardEngineMessages(); // See the upsert-case template for the owned message IDs. } diff --git a/windows/.agents/skills/review-case/references/correct-thread-safety.cpp b/windows/.agents/skills/review-case/references/correct-thread-safety.cpp index 487cc5b7c..46a94aa26 100644 --- a/windows/.agents/skills/review-case/references/correct-thread-safety.cpp +++ b/windows/.agents/skills/review-case/references/correct-thread-safety.cpp @@ -1,15 +1,17 @@ +// Keep receiver stable until synchronous release completes; discard queued events on exit. // Correct: Thread-safe UI updates via message map // Event handler (may be called from background thread) -void CExampleRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) { +void CExampleRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed) { if (m_hMsgHandler) { // Post message to main thread - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), (WPARAM)uid, 0); + ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, 0); } } // Message handler (runs on main thread) LRESULT CExampleDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { + if (!m_rtcEngine) return 0; // Safe to update UI here m_statusText.SetWindowText(_T("Joined channel")); return 0; diff --git a/windows/.agents/skills/upsert-case/SKILL.md b/windows/.agents/skills/upsert-case/SKILL.md index f809c4b4a..85da7cd32 100644 --- a/windows/.agents/skills/upsert-case/SKILL.md +++ b/windows/.agents/skills/upsert-case/SKILL.md @@ -4,7 +4,6 @@ description: > Add a new API example or modify an existing one. Covers both creation and modification scenarios, including dialog class structure, registration in APIExampleDlg, localization wiring, and ARCHITECTURE.md updates. -compatibility: [Cursor, Kiro, Windsurf, Claude, Copilot] license: MIT metadata: author: APIExample Team @@ -105,6 +104,14 @@ Create `APIExample/APIExample/[Basic|Advanced]//CDlg.c Use the template from `references/example-template.cpp` as a starting point. Replace `` with your example name. +Provide public `InitAgora()` and `UnInitAgora()` for host scene activation/exit. +`OnInitDialog()` initializes UI only because the host precreates these windows. Cleanup +must be idempotent, invalidate pending work, leave the channel, stop case-owned media and +release the engine before the next scene starts. Keep the event receiver alive through +release and discard old queued messages. The current SDK's `release(nullptr)` is +synchronous; never release in an SDK callback. Adapt the skeleton's microphone-only join, +device/permission checks, Token renewal and UI state to the selected case. + ### Step 5: Register in APIExampleDlg Do not edit `CSceneDialog.cpp` for case registration. In this project, scene ownership lives in the main dialog: @@ -144,7 +151,7 @@ Add a new row to the Case Index table in `ARCHITECTURE.md`: - [ ] Code compiles without errors - [ ] Example appears in the scene list - [ ] Example can join channel and receive callbacks -- [ ] `leaveChannel()` and `release()` are called on close +- [ ] `UnInitAgora()` cleans up when switching scenes, including a pending or failed join - [ ] UI updates happen on main thread (via message map) - [ ] Localized labels resolve correctly in both `en.ini` and `zh-cn.ini` - [ ] ARCHITECTURE.md Case Index is updated @@ -192,9 +199,9 @@ After completing the upsert, verify: - [ ] Example is registered in `APIExampleDlg.h` and `APIExampleDlg.cpp` - [ ] Scene label is declared in `Language.h` and initialized in `stdafx.cpp` - [ ] Scene label has entries in both `en.ini` and `zh-cn.ini` -- [ ] `InitializeAgoraEngine()` creates engine with correct config -- [ ] `JoinChannel()` uses token from `CConfig` -- [ ] `LeaveChannel()` and `release()` are called in `PostNcDestroy()` +- [ ] `InitAgora()` creates/initializes the engine only on scene activation using `GET_APP_ID` +- [ ] `JoinChannel()` uses the case's Token input or existing helper for the active channel/UID +- [ ] `UnInitAgora()` leaves, stops case-owned media and releases on scene exit before hide; a destructor is only a fallback - [ ] All engine events posted to main thread via `PostMessage()` - [ ] `APIExample.vcxproj` and `.filters` include the new files when they were added outside the IDE - [ ] `ARCHITECTURE.md` Case Index includes new/updated example diff --git a/windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp b/windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp index e4dc26afc..04c8c4a0c 100644 --- a/windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp +++ b/windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp @@ -1,13 +1,14 @@ +// Keep receiver stable until synchronous release completes; discard queued events on exit. // Event Handler Pattern - Post messages to main thread class CExampleRtcEngineEventHandler : public IRtcEngineEventHandler { public: void SetMsgReceiver(HWND hWnd) { m_hMsgHandler = hWnd; } - virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override { + virtual void onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed) override { if (m_hMsgHandler) { // Post message to main thread - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), (WPARAM)uid, 0); + ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, 0); } } diff --git a/windows/.agents/skills/upsert-case/references/example-template.cpp b/windows/.agents/skills/upsert-case/references/example-template.cpp index 4d53ce765..661e6ba93 100644 --- a/windows/.agents/skills/upsert-case/references/example-template.cpp +++ b/windows/.agents/skills/upsert-case/references/example-template.cpp @@ -3,43 +3,31 @@ #include "CDlg.h" #include "CConfig.h" +// Skeleton: wire these resource/control IDs in APIExample.rc, host scene registration, +// localization and project membership. Add case-specific media, permission/device checks, +// connection UI state and Token renewal. This join publishes microphone audio only. IMPLEMENT_DYNAMIC(CDlg, CDialogEx) -// Event Handler Implementation -void CRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) { +void CRtcEngineEventHandler::onJoinChannelSuccess( + const char* channel, agora::rtc::uid_t uid, int elapsed) { if (m_hMsgHandler) { - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); } } -void CRtcEngineEventHandler::onUserJoined(uid_t uid, int elapsed) { - if (m_hMsgHandler) { - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); - } -} - -void CRtcEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) { - if (m_hMsgHandler) { - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); - } -} - -void CRtcEngineEventHandler::onError(int err) { +void CRtcEngineEventHandler::onError(int err, const char* msg) { if (m_hMsgHandler) { + // Copy only the numeric code; the SDK-owned msg pointer expires after return. ::PostMessage(m_hMsgHandler, WM_MSGID(EID_ERROR), (WPARAM)err, 0); } } -// Dialog Implementation CDlg::CDlg(CWnd* pParent) : CDialogEx(IDD_DIALOG_, pParent) { } CDlg::~CDlg() { - if (m_rtcEngine) { - m_rtcEngine->release(); - m_rtcEngine = nullptr; - } + UnInitAgora(); // Fallback; ReleaseScene must clean up before hiding this window. } void CDlg::DoDataExchange(CDataExchange* pDX) { @@ -47,63 +35,100 @@ void CDlg::DoDataExchange(CDataExchange* pDX) { } BEGIN_MESSAGE_MAP(CDlg, CDialogEx) - ON_BN_CLICKED(IDC_BUTTON_JOIN, &CDlg::OnBnClickedButtonJoin) - ON_BN_CLICKED(IDC_BUTTON_LEAVE, &CDlg::OnBnClickedButtonLeave) - ON_MESSAGE(WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), &CDlg::OnMsgEngineEvent) - ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CDlg::OnMsgEngineEvent) - ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CDlg::OnMsgEngineEvent) - ON_MESSAGE(WM_MSGID(EID_ERROR), &CDlg::OnMsgEngineEvent) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CDlg::OnBnClickedButtonJoin) + ON_BN_CLICKED(IDC_BUTTON_LEAVE_CHANNEL, &CDlg::OnBnClickedButtonLeave) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CDlg::OnMsgJoinChannel) + ON_MESSAGE(WM_MSGID(EID_ERROR), &CDlg::OnMsgError) END_MESSAGE_MAP() BOOL CDlg::OnInitDialog() { CDialogEx::OnInitDialog(); - InitializeAgoraEngine(); + // UI only: the host precreates all scenes, including inactive ones. return TRUE; } -void CDlg::PostNcDestroy() { - LeaveChannel(); - CDialogEx::PostNcDestroy(); - delete this; -} - -void CDlg::InitializeAgoraEngine() { +bool CDlg::InitAgora() { + if (m_rtcEngine) return true; + m_appId = GET_APP_ID; + if (m_appId.empty()) return false; m_rtcEngine = createAgoraRtcEngine(); - if (!m_rtcEngine) return; - + if (!m_rtcEngine) return false; + + m_eventHandler.SetMsgReceiver(m_hWnd); RtcEngineContext context; - context.appId = CConfig::GetAppId(); + context.appId = m_appId.c_str(); context.eventHandler = &m_eventHandler; - m_eventHandler.SetMsgReceiver(m_hWnd); - - m_rtcEngine->initialize(context); - m_rtcEngine->enableVideo(); - m_rtcEngine->enableAudio(); + int result = m_rtcEngine->initialize(context); + if (result != 0) { + TRACE(_T("initialize failed: %d\n"), result); + UnInitAgora(); + return false; + } + return true; +} + +void CDlg::UnInitAgora() { + // Invalidate any case-specific asynchronous Token/permission work before cleanup. + if (m_rtcEngine) { + LeaveChannel(); // Also covers an accepted join that has not completed yet. + // Stop case-owned capture, players, timers and observers here. + m_rtcEngine->release(nullptr); // Current SDK: synchronous, never from an SDK callback. + m_rtcEngine = nullptr; + } + // No more SDK callbacks after synchronous release; the receiver can now be cleared. + m_eventHandler.SetMsgReceiver(nullptr); + DiscardEngineMessages(); +} + +void CDlg::DiscardEngineMessages() { + if (!::IsWindow(m_hWnd)) return; + MSG message; + const UINT ids[] = { WM_MSGID(EID_JOINCHANNEL_SUCCESS), WM_MSGID(EID_ERROR) }; + for (UINT id : ids) { + while (::PeekMessage(&message, m_hWnd, id, id, PM_REMOVE)) {} + } + // These messages contain scalar values only. Free owned payloads if adding any. } void CDlg::JoinChannel() { if (!m_rtcEngine) return; - - const char* token = CConfig::GetToken("test"); - m_rtcEngine->joinChannel(token, "test", "", 0); + CString channelInput, tokenInput; + GetDlgItemText(IDC_EDIT_CHANNELNAME, channelInput); + GetDlgItemText(IDC_EDIT_TOKEN, tokenInput); + if (channelInput.IsEmpty()) return; + const std::string channelName = cs2utf8(channelInput); + const std::string token = cs2utf8(tokenInput); + // Obtain microphone permission/device access before wiring this action. + // The supplied Token must match this channel and uid (0 here). + ChannelMediaOptions options; + options.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING; + options.clientRoleType = CLIENT_ROLE_BROADCASTER; + options.publishMicrophoneTrack = true; + options.publishCameraTrack = false; + int result = m_rtcEngine->joinChannel(token.c_str(), channelName.c_str(), 0, options); + if (result != 0) { + TRACE(_T("joinChannel failed: %d\n"), result); + } } void CDlg::LeaveChannel() { if (!m_rtcEngine) return; - - m_rtcEngine->leaveChannel(); - m_isJoined = false; + int result = m_rtcEngine->leaveChannel(); + if (result != 0) TRACE(_T("leaveChannel failed: %d\n"), result); } -void CDlg::OnBnClickedButtonJoin() { - JoinChannel(); -} +void CDlg::OnBnClickedButtonJoin() { JoinChannel(); } +void CDlg::OnBnClickedButtonLeave() { LeaveChannel(); } -void CDlg::OnBnClickedButtonLeave() { - LeaveChannel(); +LRESULT CDlg::OnMsgJoinChannel(WPARAM wParam, LPARAM lParam) { + if (!m_rtcEngine) return 0; + TRACE(_T("Join callback: uid=%u, elapsed=%d\n"), (unsigned int)wParam, (int)lParam); + // Update the case's connection UI state here, accounting for leave/rejoin requests. + return 0; } -LRESULT CDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { - // Handle engine events +LRESULT CDlg::OnMsgError(WPARAM wParam, LPARAM lParam) { + if (!m_rtcEngine) return 0; + TRACE(_T("RTC error: %d\n"), (int)wParam); return 0; } diff --git a/windows/.agents/skills/upsert-case/references/example-template.h b/windows/.agents/skills/upsert-case/references/example-template.h index d84095b53..44fac10d1 100644 --- a/windows/.agents/skills/upsert-case/references/example-template.h +++ b/windows/.agents/skills/upsert-case/references/example-template.h @@ -1,51 +1,46 @@ #pragma once -#include "AGVideoWnd.h" -#include #include -// Event handler for Agora engine callbacks +// Included after stdafx.h, as in existing MFC cases. +// Replace / and register the dialog resource and controls. class CRtcEngineEventHandler : public IRtcEngineEventHandler { public: + // Set before initialize, then keep stable until synchronous release completes. void SetMsgReceiver(HWND hWnd) { m_hMsgHandler = hWnd; } - - virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; - virtual void onUserJoined(uid_t uid, int elapsed) override; - virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; - virtual void onError(int err) override; - + void onJoinChannelSuccess(const char* channel, agora::rtc::uid_t uid, int elapsed) override; + void onError(int err, const char* msg) override; + private: HWND m_hMsgHandler = nullptr; }; -// Main dialog class class CDlg : public CDialogEx { DECLARE_DYNAMIC(CDlg) - + public: CDlg(CWnd* pParent = nullptr); virtual ~CDlg(); - enum { IDD = IDD_DIALOG_ }; - + + // APIExampleDlg calls these from CreateScene() / ReleaseScene(). + bool InitAgora(); + void UnInitAgora(); + protected: - virtual BOOL OnInitDialog(); - virtual void DoDataExchange(CDataExchange* pDX); - virtual void PostNcDestroy(); - + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; afx_msg void OnBnClickedButtonJoin(); afx_msg void OnBnClickedButtonLeave(); - afx_msg LRESULT OnMsgEngineEvent(WPARAM wParam, LPARAM lParam); - + afx_msg LRESULT OnMsgJoinChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnMsgError(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() - + private: - void InitializeAgoraEngine(); void JoinChannel(); void LeaveChannel(); - void UpdateUI(); - + void DiscardEngineMessages(); + IRtcEngine* m_rtcEngine = nullptr; CRtcEngineEventHandler m_eventHandler; - uid_t m_remoteUid = 0; - bool m_isJoined = false; + std::string m_appId; }; diff --git a/windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp b/windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp index 506c9a003..2c09c6772 100644 --- a/windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp +++ b/windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp @@ -1,18 +1,30 @@ -// Proper Engine Lifecycle Pattern +// Scene lifecycle fragments; see example-template.h/.cpp for the complete skeleton. -// Create -m_rtcEngine = createAgoraRtcEngine(); +// OnInitDialog(): UI only. APIExampleDlg precreates inactive dialogs. -// Initialize +// InitAgora(), invoked by CreateScene(): +m_appId = GET_APP_ID; // Member-owned UTF-8 value survives initialize(). +m_rtcEngine = createAgoraRtcEngine(); +if (!m_rtcEngine) return false; +m_eventHandler.SetMsgReceiver(m_hWnd); RtcEngineContext context; -context.appId = CConfig::GetAppId(); +context.appId = m_appId.c_str(); context.eventHandler = &m_eventHandler; -m_rtcEngine->initialize(context); +if (m_rtcEngine->initialize(context) != 0) { + UnInitAgora(); + return false; +} -// Use -m_rtcEngine->joinChannel(token, channelName, "", 0); +// Join after device/permission checks; token/channelName/uid belong to the active case. +int result = m_rtcEngine->joinChannel(token.c_str(), channelName.c_str(), "", uid); +// Handle nonzero result; success is reported asynchronously. -// Cleanup -m_rtcEngine->leaveChannel(); -m_rtcEngine->release(); -m_rtcEngine = nullptr; +// UnInitAgora(), invoked by ReleaseScene() before hiding the window: +// Invalidate pending requests and stop case-owned media before release. +if (m_rtcEngine) { + m_rtcEngine->leaveChannel(); + m_rtcEngine->release(nullptr); // Synchronous in this SDK; never in an SDK callback. + m_rtcEngine = nullptr; +} +m_eventHandler.SetMsgReceiver(nullptr); +DiscardEngineMessages(); // Drop scalar callback messages left by the old engine. diff --git a/windows/.agents/skills/upsert-case/references/message-map-pattern.cpp b/windows/.agents/skills/upsert-case/references/message-map-pattern.cpp index 211e774c9..80919c419 100644 --- a/windows/.agents/skills/upsert-case/references/message-map-pattern.cpp +++ b/windows/.agents/skills/upsert-case/references/message-map-pattern.cpp @@ -1,17 +1,17 @@ -// Message Map Pattern - Thread-safe UI updates +// In the dialog class declaration (.h): +// afx_msg LRESULT OnMsgJoinChannel(WPARAM wParam, LPARAM lParam); +// afx_msg LRESULT OnMsgError(WPARAM wParam, LPARAM lParam); +// DECLARE_MESSAGE_MAP() -// Header +// At file scope in the .cpp, not inside the class declaration: BEGIN_MESSAGE_MAP(CExampleDlg, CDialogEx) - ON_BN_CLICKED(IDC_BUTTON_JOIN, &CExampleDlg::OnBnClickedButtonJoin) - ON_MESSAGE(WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), &CExampleDlg::OnMsgEngineEvent) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CExampleDlg::OnMsgJoinChannel) + ON_MESSAGE(WM_MSGID(EID_ERROR), &CExampleDlg::OnMsgError) END_MESSAGE_MAP() -// Implementation -void CExampleDlg::OnBnClickedButtonJoin() { - JoinChannel(); -} - -LRESULT CExampleDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { - // Handle engine events on main thread +LRESULT CExampleDlg::OnMsgError(WPARAM wParam, LPARAM lParam) { + if (!m_rtcEngine) return 0; + // The message ID selects this handler; wParam is the error code posted by onError. + TRACE(_T("RTC error: %d\n"), (int)wParam); return 0; } diff --git a/windows/AGENTS.md b/windows/AGENTS.md index a9c728ab4..799328356 100644 --- a/windows/AGENTS.md +++ b/windows/AGENTS.md @@ -17,17 +17,16 @@ start APIExample.sln ## App ID Configuration -Configure your Agora App ID in `APIExample/APIExample/CConfig.h` and `CConfig.cpp`: +For compile-time configuration, replace the `APP_ID` placeholder in +`APIExample/APIExample/stdafx.h`: ```cpp -// CConfig.h -class CConfig { -public: - static const char* GetAppId() { return "<#YOUR_APP_ID#>"; } - static const char* GetToken(const char* channelName) { return "<#YOUR_TOKEN#>"; } -}; +#define APP_ID "<#YOUR_APP_ID#>" ``` +When that placeholder remains, `CConfig.cpp` reads `AppID.ini` beside the built executable +and creates the file if needed. `stdafx.h` is tracked; never stage a real App ID in it. + ## Architecture Red Lines **Do NOT:** @@ -38,7 +37,7 @@ public: - Forget to call `leaveChannel()` and `release()` when closing an example - Update UI from background threads — always post messages to the main thread - Share engine instances between examples — each example manages its own lifecycle -- Forget to implement `IAgoraRtcEngineEventHandler` for event handling +- Forget to implement `IRtcEngineEventHandler` for event handling ## Rules @@ -46,8 +45,8 @@ public: All work must conform to the rules defined in `ARCHITECTURE.md`: - Every example is a dialog class inheriting from `CDialogEx` or `CDialog` -- Each example implements `IAgoraRtcEngineEventHandler` interface -- Each example manages its own Agora engine lifecycle +- Each example implements `IRtcEngineEventHandler` interface +- Each example manages its engine through `InitAgora()` / `UnInitAgora()` on scene activation/exit - Message handlers are defined via `BEGIN_MESSAGE_MAP` / `END_MESSAGE_MAP` - All examples are registered in `APIExampleDlg.h` and `APIExampleDlg.cpp` - Configuration is managed centrally via `CConfig` class diff --git a/windows/ARCHITECTURE.md b/windows/ARCHITECTURE.md index 53cdd2212..90049abad 100644 --- a/windows/ARCHITECTURE.md +++ b/windows/ARCHITECTURE.md @@ -37,7 +37,6 @@ windows/ │ │ ├── CSceneDialog.cpp # Shared dialog helper, not the case registration source │ │ └── CSceneDialog.h │ ├── APIExample.sln # Visual Studio solution -│ ├── cicd/ # CI/CD scripts │ └── .vscode/ # VS Code configuration ├── .agents/skills/query-cases/ # Read-only case and registration discovery ├── .agents/skills/upsert-case/ # Add or modify a case @@ -60,7 +59,7 @@ Each example is a dialog class that: - Inherits from `CDialogEx` or `CDialog` - Implements message handlers via `BEGIN_MESSAGE_MAP` / `END_MESSAGE_MAP` - Manages its own Agora engine lifecycle -- Implements `IAgoraRtcEngineEventHandler` interface +- Implements `IRtcEngineEventHandler` interface - Owns all UI controls and state for that example ### Naming Convention @@ -76,15 +75,15 @@ All examples are registered in `APIExampleDlg.h` and `APIExampleDlg.cpp`. The lo ### Configuration Management -Configuration is centralized in `CConfig` class: -- App ID management -- Token generation -- Global settings (resolution, frame rate, etc.) +`CConfig` supplies the App ID and localized strings. `GET_APP_ID` expands to +`cs2utf8(CConfig::GetInstance()->GetAPP_ID())`; keep that UTF-8 value alive while initializing +the engine. Media settings belong to the selected case. Token input/generation is described +below and is not a static CConfig API. ### Common Utilities All examples share utilities: -- `CConfig` — App ID, token, and global settings +- `CConfig` — App ID and localized strings - `VideoExtractor` — Video frame extraction - `YUVReader` — YUV file reading - DirectShow and DirectSound wrappers @@ -132,39 +131,38 @@ All examples share utilities: ## Engine Lifecycle -``` -1. Create Engine - createAgoraRtcEngine() - -2. Initialize Engine - initialize(RtcEngineContext) - -3. Enable Features (optional) - enableVideo(), enableAudio() - -4. Setup Local Media (optional) - setupLocalVideo(), startAudioMixing() - -5. Join Channel - joinChannel(token, channelName, uid) - -6. Handle Callbacks - onJoinChannelSuccess(), onUserJoined(), onUserOffline() - -7. Leave Channel - leaveChannel() - -8. Release Engine - release() -``` +`APIExampleDlg.cpp` owns the scene lifecycle: + +1. `InitSceneDialog()` precreates each dialog. `OnInitDialog()` sets up UI only. +2. `CreateScene()` calls the selected dialog's `InitAgora()` and shows it. Create the engine + with `createAgoraRtcEngine()`, initialize it with `RtcEngineContext`, and configure the + media required by the case before joining. +3. SDK callbacks post messages to the dialog's UI thread through `IRtcEngineEventHandler`. +4. `ReleaseScene()` calls `UnInitAgora()` and hides the dialog. Invalidate pending requests, + leave the channel, stop owned media, release the engine and clear its pointer here. + +The current SDK's `release(nullptr)` completes destruction synchronously. Keep the callback +receiver alive until release finishes, then detach it and discard stale queued messages. +Do not release inside an SDK callback. Complete cleanup before the next scene initializes. +Hidden scene dialogs are not destroyed, so `PostNcDestroy()` or a destructor alone cannot +provide scene-exit cleanup. Standalone windows may use destruction hooks only when their +actual exit path destroys the window and matches host ownership. + +See [upsert-case](.agents/skills/upsert-case/SKILL.md) and +[review-case](.agents/skills/review-case/SKILL.md) for registration and validation details. ## Token Flow -Token is obtained from `CConfig` and passed to `joinChannel()`: +Use the case's explicit Token input (as in `Basic/JoinChannelVideoByToken/`) or its existing +asynchronous helper. CConfig has no GetToken method. Pass the same channel and UID used to +obtain the Token to one of the SDK's actual join overloads: ```cpp -const char* token = CConfig::GetToken(channelName); -m_rtcEngine->joinChannel(token, channelName, "", 0); +// token, channelName and uid are values owned by the active case. +int result = m_rtcEngine->joinChannel(token.c_str(), channelName.c_str(), "", uid); +// Check result, then wait for onJoinChannelSuccess before showing a joined state. ``` -For production, tokens should be generated server-side and refreshed before expiration. +Handle request failure and expiring tokens explicitly; renew only while the original +scene/engine is active. Production tokens should be generated server-side. Never stage +real App IDs, App Certificates or tokens. diff --git a/windows/cicd/script/downloadsdk1.ps1 b/windows/cicd/script/downloadsdk1.ps1 deleted file mode 100644 index 44ecd029a..000000000 --- a/windows/cicd/script/downloadsdk1.ps1 +++ /dev/null @@ -1,18 +0,0 @@ -param($SDKURL, $LocalFileName) - -add-type @" -using System.Net; -using System.Security.Cryptography.X509Certificates; -public class TrustAllCertsPolicy : ICertificatePolicy { - public bool CheckValidationResult( - ServicePoint srvPoint, X509Certificate certificate, - WebRequest request, int certificateProblem) { - return true; - } -} -"@ -$AllProtocols = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12' -[System.Net.ServicePointManager]::SecurityProtocol = $AllProtocols -[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - -Invoke-WebRequest -Uri "$SDKURL" -OutFile "$LocalFileName" -TimeoutSec 10; diff --git a/windows/cicd/templates/build-Windows.yml b/windows/cicd/templates/build-Windows.yml deleted file mode 100644 index 62d60b7d6..000000000 --- a/windows/cicd/templates/build-Windows.yml +++ /dev/null @@ -1,53 +0,0 @@ -# .NET Desktop -# Build and run tests for .NET Desktop or Windows classic desktop solutions. -# Add steps that publish symbols, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net - -parameters: - displayName: '' - workingDirectory: '' - scheme: '' - sdkVersion: '' - faceUnitySDKVersion: '' - Machine: '' - solutionName: '' - release: '' -jobs: - - job: ${{ parameters.displayName }}Build - displayName: ${{ parameters.displayName }} - - pool: - vmImage: 'windows-2019' - - variables: - - group: AgoraKeys - - name: solution - value: '${{ parameters.workingDirectory }}/${{ parameters.solutionName }}' - - name: buildPlatform - value: '${{ parameters.Machine }}' - - name: buildConfiguration - value: 'Release' - steps: - - script: echo "$(WindowsRTCSDK)" && ls - - script: cd ${{ parameters.workingDirectory }} && installThirdParty.bat - - task: VSBuild@1 - inputs: - solution: '$(solution)' - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' - vsVersion: '15.0' - msbuildArchitecture: '${{ parameters.Machine }}' - - script: cd ${{ parameters.workingDirectory }}/${{ parameters.release }} && ls - - task: ArchiveFiles@2 - inputs: - rootFolderOrFile: ${{ parameters.workingDirectory }}/${{ parameters.release }} - includeRootFolder: true - archiveType: 'zip' - archiveFile: ${{ parameters.workingDirectory }}/${{ parameters.displayName }}.zip - replaceExistingArchive: true - - - task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: ${{ parameters.workingDirectory }}/${{ parameters.displayName }}.zip - ArtifactName: ${{ parameters.displayName }} - From 2cf62f61c71790ad11ebc988083e4edaf920d7e4 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 9 Sep 2026 11:44:25 +0800 Subject: [PATCH 41/49] refactor(ci): remove obsolete cloud project scripts --- .github/ci/README.md | 2 +- iOS/APIExample-Audio/cloud_project.sh | 10 --------- iOS/APIExample-OC/cloud_project.sh | 10 --------- iOS/APIExample-SwiftUI/cloud_project.sh | 10 --------- iOS/APIExample/cloud_project.sh | 27 ------------------------- macOS/cloud_project.sh | 10 --------- 6 files changed, 1 insertion(+), 68 deletions(-) delete mode 100755 iOS/APIExample-Audio/cloud_project.sh delete mode 100755 iOS/APIExample-OC/cloud_project.sh delete mode 100755 iOS/APIExample-SwiftUI/cloud_project.sh delete mode 100755 iOS/APIExample/cloud_project.sh delete mode 100755 macOS/cloud_project.sh diff --git a/.github/ci/README.md b/.github/ci/README.md index e0ea41117..18554653b 100644 --- a/.github/ci/README.md +++ b/.github/ci/README.md @@ -6,7 +6,7 @@ policy helpers and the release packaging scripts loaded by Jenkins. | Path | Caller and responsibility | Validation | | --- | --- | --- | | `Jenkinsfile_bitbucket.groovy` | External Jenkins shared library loads the release pipeline. | Verify job configuration and release evidence in the release ticket. | -| `build/` | Jenkins platform Groovy entry points call packaging scripts, which use each project's `cloud_build.*` / `cloud_project.*` adapters. IPA entry points also depend on external Jenkins configuration. | Run the signing-preflight unit tests below; actual signing, private SDK injection and artifact verification belong to Jenkins. | +| `build/` | Jenkins platform Groovy entry points call packaging scripts, which use each project's `cloud_build.*` adapters. IPA entry points also depend on external Jenkins configuration. | Run the signing-preflight unit tests below; actual signing, private SDK injection and artifact verification belong to Jenkins. | | `policy/` | `repository-policy.yml` runs AI asset validation and its regression tests; `.git-hooks/` supplies the local credential and commit-message checks. | Follow [AI validation](../../docs/ai/README.md) for venv setup and policy checks. | After creating the venv described above, run the packaging helper tests from the repository root: diff --git a/iOS/APIExample-Audio/cloud_project.sh b/iOS/APIExample-Audio/cloud_project.sh deleted file mode 100755 index 6a4762ac0..000000000 --- a/iOS/APIExample-Audio/cloud_project.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh - -PROJECT_PATH=$PWD - -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -fi - - -cd ${PROJECT_PATH} && pod install || exit 1 diff --git a/iOS/APIExample-OC/cloud_project.sh b/iOS/APIExample-OC/cloud_project.sh deleted file mode 100755 index 6a4762ac0..000000000 --- a/iOS/APIExample-OC/cloud_project.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh - -PROJECT_PATH=$PWD - -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -fi - - -cd ${PROJECT_PATH} && pod install || exit 1 diff --git a/iOS/APIExample-SwiftUI/cloud_project.sh b/iOS/APIExample-SwiftUI/cloud_project.sh deleted file mode 100755 index 6a4762ac0..000000000 --- a/iOS/APIExample-SwiftUI/cloud_project.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh - -PROJECT_PATH=$PWD - -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -fi - - -cd ${PROJECT_PATH} && pod install || exit 1 diff --git a/iOS/APIExample/cloud_project.sh b/iOS/APIExample/cloud_project.sh deleted file mode 100755 index fb56e5d1f..000000000 --- a/iOS/APIExample/cloud_project.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env sh - -PROJECT_PATH=$PWD - -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -fi - -# Download beauty resource -echo "start download sense resource : $sense_lib" -curl -L -O "$sense_lib" -unzip -o vender_sense_iOS.zip -rm -f vender_sense_iOS.zip - -echo "start download fu resource : $fu_lib" -curl -L -O "$fu_lib" -unzip -o vender_fu_iOS.zip -rm -f vender_fu_iOS.zip - -# Enable third-party player configuration -perl -i -pe "s#\# pod 'ijkplayer'# pod 'ijkplayer'#g" Podfile - -# Enable third-party beauty filters -perl -i -pe "s#\#pod 'SenseLib'#pod 'SenseLib'#g" Podfile -perl -i -pe "s#\#pod 'fuLib'#pod 'fuLib'#g" Podfile - -pod install --repo-update || exit 1 diff --git a/macOS/cloud_project.sh b/macOS/cloud_project.sh deleted file mode 100755 index 6a4762ac0..000000000 --- a/macOS/cloud_project.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env sh - -PROJECT_PATH=$PWD - -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -fi - - -cd ${PROJECT_PATH} && pod install || exit 1 From 10afcd3d4514c7e9f8533c4b0c5448f0cf613da6 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Wed, 9 Sep 2026 16:40:00 +0800 Subject: [PATCH 42/49] docs: refresh RTC documentation links --- Android/APIExample-Audio/README.md | 2 +- Android/APIExample-Audio/README.zh.md | 2 +- .../examples/advanced/PlayAudioFiles.java | 12 +++++----- .../examples/advanced/PreCallTest.java | 4 ++-- .../advanced/ProcessAudioRawData.java | 12 +++++----- .../examples/advanced/RhythmPlayer.java | 12 +++++----- .../examples/advanced/SpatialSound.java | 12 +++++----- .../examples/advanced/VoiceEffects.java | 12 +++++----- .../customaudio/CustomAudioRender.java | 12 +++++----- .../customaudio/CustomAudioSource.java | 12 +++++----- .../example/examples/audio/AudioWaveform.java | 12 +++++----- .../examples/basic/JoinChannelAudio.java | 12 +++++----- .../basic/JoinChannelAudioByToken.java | 8 +++---- Android/APIExample-Compose/README.md | 2 +- Android/APIExample-Compose/README.zh.md | 2 +- Android/APIExample/README.md | 2 +- Android/APIExample/README.zh.md | 2 +- .../api/example/common/BaseVbFragment.java | 2 +- .../examples/advanced/AgoraBeauty.java | 12 +++++----- .../examples/advanced/ChannelEncryption.java | 12 +++++----- .../examples/advanced/ContentInspect.java | 12 +++++----- .../advanced/CustomRemoteVideoRender.java | 12 +++++----- .../examples/advanced/HostAcrossChannel.java | 12 +++++----- .../advanced/JoinMultipleChannel.java | 12 +++++----- .../examples/advanced/LiveStreaming.java | 12 +++++----- .../advanced/LocalVideoTranscoding.java | 12 +++++----- .../examples/advanced/MediaMetadata.java | 12 +++++----- .../examples/advanced/MediaPlayer.java | 12 +++++----- .../examples/advanced/MediaRecorder.java | 12 +++++----- .../advanced/MultiVideoSourceTracks.java | 12 +++++----- .../example/examples/advanced/Multipath.java | 12 +++++----- .../examples/advanced/PictureInPicture.java | 12 +++++----- .../examples/advanced/PlayAudioFiles.java | 12 +++++----- .../examples/advanced/PreCallTest.java | 4 ++-- .../advanced/ProcessAudioRawData.java | 12 +++++----- .../examples/advanced/ProcessRawData.java | 12 +++++----- .../advanced/PushExternalVideoYUV.java | 10 ++++---- .../examples/advanced/RTMPStreaming.java | 16 ++++++------- .../examples/advanced/RhythmPlayer.java | 12 +++++----- .../examples/advanced/ScreenSharing.java | 12 +++++----- .../examples/advanced/SendDataStream.java | 14 +++++------ .../examples/advanced/SimpleExtension.java | 12 +++++----- .../example/examples/advanced/Simulcast.java | 12 +++++----- .../examples/advanced/SpatialSound.java | 12 +++++----- .../advanced/SwitchCameraScreenShare.java | 12 +++++----- .../advanced/TransparentRendering.java | 12 +++++----- .../advanced/VideoProcessExtension.java | 12 +++++----- .../examples/advanced/VideoQuickSwitch.java | 12 +++++----- .../examples/advanced/VoiceEffects.java | 14 +++++------ .../customaudio/CustomAudioRender.java | 12 +++++----- .../customaudio/CustomAudioSource.java | 12 +++++----- .../examples/audio/AudioRouterPlayerExo.java | 12 +++++----- .../examples/audio/AudioRouterPlayerIjk.java | 12 +++++----- .../audio/AudioRouterPlayerNative.java | 12 +++++----- .../example/examples/audio/AudioWaveform.java | 12 +++++----- .../examples/basic/JoinChannelAudio.java | 12 +++++----- .../examples/basic/JoinChannelVideo.java | 12 +++++----- .../basic/JoinChannelVideoByToken.java | 8 +++---- README.kr.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../Common/GlobalSettings.swift | 2 +- .../Advanced/AudioMixing/AudioMixing.swift | 8 +++---- .../CustomAudioRender/CustomAudioRender.swift | 8 +++---- .../CustomAudioSource/CustomAudioSource.swift | 8 +++---- .../CustomPcmAudioSource.swift | 8 +++---- .../Advanced/PrecallTest/PrecallTest.swift | 4 ++-- .../Advanced/RawAudioData/RawAudioData.swift | 6 ++--- .../Advanced/RhythmPlayer/RhythmPlayer.swift | 8 +++---- .../Advanced/VoiceChanger/VoiceChanger.swift | 8 +++---- .../JoinChannelAudioToken.swift | 8 +++---- .../JoinChannelAudio/JoinChannelAudio.swift | 8 +++---- iOS/APIExample-Audio/README.md | 4 ++-- iOS/APIExample-Audio/README.zh.md | 4 ++-- iOS/APIExample-Audio/sdk.podspec | 2 +- .../StreamEncryption/StreamEncryption.m | 4 ++-- iOS/APIExample-OC/README.md | 4 ++-- iOS/APIExample-OC/README.zh.md | 4 ++-- iOS/APIExample-OC/sdk.podspec | 2 +- .../Common/Settings/GlobalSettings.swift | 2 +- .../Examples/Advanced/ARKit/ARKitRTC.swift | 4 ++-- .../Advanced/AudioMixing/AudioMixingRTC.swift | 4 ++-- .../AudioWaveform/AudioWaveformRTC.swift | 4 ++-- .../ContentInspect/ContentInspectRTC.swift | 4 ++-- .../CreateDataStreamRTC.swift | 4 ++-- .../CustomAudioRenderRTC.swift | 4 ++-- .../CustomPCMAudioSourceRTC.swift | 4 ++-- .../LiveStreaming/LiveStreamingRTC.swift | 4 ++-- .../LocalCompositeGraphRTC.swift | 4 ++-- .../MediaChannelRelayRTC.swift | 4 ++-- .../Advanced/MediaPlayer/MediaPlayerRTC.swift | 4 ++-- .../Advanced/MutliCamera/MutliCameraRTC.swift | 4 ++-- .../QuickSwitchChannelRTC.swift | 4 ++-- .../Advanced/RTMPStream/RTMPStreamRTC.swift | 4 ++-- .../RhythmPlayer/RhythmPlayerRTC.swift | 4 ++-- .../Advanced/ScreenShare/ScreenShareRTC.swift | 4 ++-- .../SpatialAudio/SpatialAudioRTC.swift | 4 ++-- .../StreamEncryptionRTC.swift | 8 +++---- .../VideoMetadata/VideoMetaDataRTC.swift | 4 ++-- .../VideoProcess/VideoProcessRTC.swift | 4 ++-- .../VoiceChanger/VoiceChangerRTC.swift | 4 ++-- .../JoinChannelAudioRTC.swift | 4 ++-- .../JoinChannelVideoRecorderRTC.swift | 4 ++-- .../JoinChannelVideoTokenRTC.swift | 4 ++-- .../JoinChannelVideoRTC.swift | 4 ++-- iOS/APIExample-SwiftUI/sdk.podspec | 2 +- .../APIExample/Common/GlobalSettings.swift | 2 +- .../Examples/Advanced/ARKit/ARKit.swift | 8 +++---- .../Advanced/AgoraBeauty/AgoraBeauty.swift | 4 ++-- .../Advanced/AudioMixing/AudioMixing.swift | 4 ++-- .../AudioRouterPlayer/AudioRouterPlayer.swift | 4 ++-- .../AudioWaveform/AudioWaveform.swift | 4 ++-- .../ContentInspect/ContentInspect.swift | 2 +- .../CreateDataStream/CreateDataStream.swift | 4 ++-- .../CustomAudioRender/CustomAudioRender.swift | 4 ++-- .../CustomAudioSource/CustomAudioSource.swift | 4 ++-- .../CustomPcmAudioSource.swift | 4 ++-- .../CustomVideoRender/CustomVideoRender.swift | 4 ++-- .../CustomVideoSourcePush.swift | 4 ++-- .../CustomVideoSourcePushMulti.swift | 4 ++-- .../LiveStreaming/LiveStreaming.swift | 4 ++-- .../LocalCompositeGraph.swift | 4 ++-- .../MediaChannelRelay/MediaChannelRelay.swift | 4 ++-- .../Advanced/MediaPlayer/MediaPlayer.swift | 4 ++-- .../Advanced/MutliCamera/MutliCamera.swift | 4 ++-- .../QuickSwitchChannel.swift | 4 ++-- .../RTMPStreaming/RTMPStreaming.swift | 4 ++-- .../Advanced/RawAudioData/RawAudioData.swift | 2 +- .../Advanced/RawMediaData/RawMediaData.swift | 12 +++++----- .../Advanced/RawVideoData/RawVideoData.swift | 2 +- .../Advanced/RhythmPlayer/RhythmPlayer.swift | 4 ++-- .../Advanced/ScreenShare/ScreenShare.swift | 4 ++-- .../Advanced/SimpleFilter/SimpleFilter.swift | 4 ++-- .../StreamEncryption/StreamEncryption.swift | 8 +++---- .../Advanced/VideoChat/VideoChat.swift | 4 ++-- .../VideoMetadata/VideoMetadata.swift | 4 ++-- .../Advanced/VideoProcess/VideoProcess.swift | 4 ++-- .../Advanced/VoiceChanger/VoiceChanger.swift | 4 ++-- .../JoinChannelAudio/JoinChannelAudio.swift | 4 ++-- .../JoinChannelVideoRecorder.swift | 4 ++-- .../JoinChannelVideoToken.swift | 6 ++--- .../JoinChannelVideo/JoinChannelVideo.swift | 4 ++-- iOS/APIExample/README.md | 4 ++-- iOS/APIExample/README.zh.md | 4 ++-- iOS/APIExample/fu.podspec | 2 +- iOS/APIExample/sdk.podspec | 2 +- iOS/APIExample/sense.podspec | 2 +- macOS/APIExample/Common/GlobalSettings.swift | 2 +- .../Advanced/AgoraBeauty/AgoraBeauty.swift | 12 +++++----- .../Advanced/AudioMixing/AudioMixing.swift | 12 +++++----- .../ChannelMediaRelay/ChannelMediaRelay.swift | 12 +++++----- .../ContentInspect/ContentInspect.swift | 9 ++++--- .../CreateDataStream/CreateDataStream.swift | 12 +++++----- .../CustomAudioRender/CustomAudioRender.swift | 12 +++++----- .../CustomAudioSource/CustomAudioSource.swift | 12 +++++----- .../CustomVideoRender/CustomVideoRender.swift | 12 +++++----- .../CustomVideoSourceMediaIO.swift | 12 +++++----- .../CustomVideoSourcePush.swift | 12 +++++----- .../CustomVideoSourcePushMulti.swift | 12 +++++----- .../JoinMultiChannel/JoinMultiChannel.swift | 24 +++++++++---------- .../LiveStreaming/LiveStreaming.swift | 12 +++++----- .../LocalVideoTranscoding.swift | 8 +++---- .../Advanced/MediaPlayer/MediaPlayer.swift | 4 ++-- .../MultiCameraSourece.swift | 16 ++++++------- .../Advanced/Multipath/Multipath.swift | 12 +++++----- .../Advanced/PrecallTest/PrecallTest.swift | 12 +++++----- .../QuickSwitchChannel.swift | 8 +++---- .../RTMPStreaming/RTMPStreaming.swift | 12 +++++----- .../Advanced/RawAudioData/RawAudioData.swift | 12 +++++----- .../Advanced/RawMediaData/RawMediaData.swift | 12 +++++----- .../Advanced/RawVideoData/RawVideoData.swift | 10 ++++---- .../Advanced/RtePlayer/RtePlayer.swift | 8 +++---- .../Advanced/ScreenShare/ScreenShare.swift | 20 ++++++++-------- .../Advanced/SimpleFilter/SimpleFilter.swift | 12 +++++----- .../Advanced/Simulcast/Simulcast.swift | 12 +++++----- .../StreamEncryption/StreamEncryption.swift | 16 ++++++------- .../Advanced/VideoProcess/VideoProcess.swift | 12 +++++----- .../Advanced/VoiceChanger/VoiceChanger.swift | 12 +++++----- .../JoinChannelAudio/JoinChannelAudio.swift | 12 +++++----- .../JoinChannelVideoRecorder.swift | 12 +++++----- .../JoinChannelVideoToken.swift | 12 +++++----- .../JoinChannelVideo/JoinChannelVideo.swift | 12 +++++----- macOS/README.md | 2 +- macOS/README.zh.md | 2 +- macOS/sdk.podspec | 2 +- .../APIExample/APIExample/APIExampleDlg.cpp | 2 +- windows/README.md | 2 +- windows/README.zh.md | 2 +- 188 files changed, 707 insertions(+), 708 deletions(-) diff --git a/Android/APIExample-Audio/README.md b/Android/APIExample-Audio/README.md index 9324018bd..3254f8010 100644 --- a/Android/APIExample-Audio/README.md +++ b/Android/APIExample-Audio/README.md @@ -40,7 +40,7 @@ You are all set. Now connect your Android device and run the project. ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/Android/APIExample-Audio/README.zh.md b/Android/APIExample-Audio/README.zh.md index d4a355903..7bab1e8a2 100644 --- a/Android/APIExample-Audio/README.zh.md +++ b/Android/APIExample-Audio/README.zh.md @@ -39,7 +39,7 @@ ## 联系我们 -- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq) +- 如果你遇到了困难,可以先参阅 [常见问题](https://doc.shengwang.cn/faq/list) - 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO) - 如果你想了解 Agora SDK 在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase) - 如果你想了解 Agora 的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community) 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 9431b7863..d3ed6cfbb 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 @@ -158,7 +158,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /** - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /** The channel profile. @@ -335,9 +335,9 @@ private void joinChannel(String channelId) { /** * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -348,8 +348,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -367,7 +367,7 @@ private void joinChannel(String channelId) { /** * Error code description can be found at: * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 d1cb48f8f..1b39961ca 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 @@ -80,7 +80,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /** - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /** The channel profile. @@ -213,7 +213,7 @@ else if(num >= MAX_COUNT_DOWN) { /** * Error code description can be found at: * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 aae62913a..cce12f36e 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 @@ -147,7 +147,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /** - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /** The channel profile. @@ -273,9 +273,9 @@ private void joinChannel(String channelId) { /** * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { ChannelMediaOptions option = new ChannelMediaOptions(); option.autoSubscribeAudio = true; @@ -284,8 +284,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -370,7 +370,7 @@ public AudioParams getEarMonitoringAudioParams() { /** * Error code description can be found at: * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 e64171380..17cbfe9b4 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 @@ -100,7 +100,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /** - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /** The channel profile. @@ -232,9 +232,9 @@ private void joinChannel(String channelId) { /** * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /** Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -251,8 +251,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -270,7 +270,7 @@ private void joinChannel(String channelId) { /** * Error code description can be found at: * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 2fabbf920..80ca98b02 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 @@ -100,7 +100,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { try { /*Creates an RtcEngine instance. * @param context The context of Android Activity - * @param appId The App ID issued to you by Agora. See + * @param appId The App ID issued to you by Agora. See * How to get the App ID * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events.*/ @@ -221,9 +221,9 @@ private void joinChannel() { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { ChannelMediaOptions option = new ChannelMediaOptions(); @@ -235,8 +235,8 @@ private void joinChannel() { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -621,7 +621,7 @@ public void onJoinChannelSuccess(String channel, int uid, int elapsed) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 f57175a21..53357b38f 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 @@ -247,7 +247,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -402,9 +402,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -416,8 +416,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -435,7 +435,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 5c240eb8d..ab415cfd4 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 @@ -104,7 +104,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -238,9 +238,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { ChannelMediaOptions option = new ChannelMediaOptions(); @@ -255,8 +255,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -274,7 +274,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 208080a5d..44a733ffe 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 @@ -122,7 +122,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /** - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /** The channel profile. @@ -283,9 +283,9 @@ private void joinChannel(String channelId) { /** * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /** Allows a user to join a channel. @@ -294,8 +294,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -313,7 +313,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 9f6293d1f..1466d7a7d 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 @@ -56,7 +56,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -173,9 +173,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -184,8 +184,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -205,7 +205,7 @@ private void joinChannel(String channelId) { /** * Error code description can be found at: * en: ... - * cn: ... + * cn: ... */ @Override public void onError(int error) { 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 039f4441b..70a802c13 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 @@ -250,7 +250,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -469,9 +469,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -480,8 +480,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -500,7 +500,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int error) { diff --git a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java index 15c2f3459..9d5db6164 100755 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudioByToken.java @@ -97,7 +97,7 @@ private boolean createRtcEngine(String appId) { */ config.mContext = requireContext().getApplicationContext(); /** - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = appId; /** The channel profile. @@ -217,8 +217,8 @@ private void joinChannel(String channelId, String token) engine.leaveChannel(); // Usually happens with invalid parameters // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -234,7 +234,7 @@ private void joinChannel(String channelId, String token) /** * Error code description can be found at: * en: https://api-ref.agora.io/en/voice-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror - * cn: https://docs.agora.io/cn/voice-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { diff --git a/Android/APIExample-Compose/README.md b/Android/APIExample-Compose/README.md index 89964170d..c555d4003 100644 --- a/Android/APIExample-Compose/README.md +++ b/Android/APIExample-Compose/README.md @@ -39,7 +39,7 @@ You are all set. Now connect your Android device and run the project. ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/Android/APIExample-Compose/README.zh.md b/Android/APIExample-Compose/README.zh.md index 08f2d7ee4..96d04f9ec 100644 --- a/Android/APIExample-Compose/README.zh.md +++ b/Android/APIExample-Compose/README.zh.md @@ -38,7 +38,7 @@ ## 联系我们 -- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq) +- 如果你遇到了困难,可以先参阅 [常见问题](https://doc.shengwang.cn/faq/list) - 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO) - 如果你想了解 Agora SDK 在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase) - 如果你想了解 Agora 的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community) diff --git a/Android/APIExample/README.md b/Android/APIExample/README.md index 245e1a183..1f6a42f2a 100644 --- a/Android/APIExample/README.md +++ b/Android/APIExample/README.md @@ -146,7 +146,7 @@ Android/APIExample/agora-stream-encrypt/src/main/cpp ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/Android/APIExample/README.zh.md b/Android/APIExample/README.zh.md index 5b012cc10..0a1e94fd7 100644 --- a/Android/APIExample/README.zh.md +++ b/Android/APIExample/README.zh.md @@ -136,7 +136,7 @@ Android/APIExample/agora-stream-encrypt/src/main/cpp ## 联系我们 -- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq) +- 如果你遇到了困难,可以先参阅 [常见问题](https://doc.shengwang.cn/faq/list) - 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO) - 如果你想了解 Agora SDK 在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase) - 如果你想了解 Agora 的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community) 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 a66efa218..f909d6664 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 @@ -59,7 +59,7 @@ protected RtcEngine initRtcEngine(IRtcEngineEventHandler engineEventHandler) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. 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 75b7f7d13..4e536023b 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 @@ -286,7 +286,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -399,9 +399,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -414,8 +414,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -1053,7 +1053,7 @@ public void onStopTrackingTouch(SeekBar seekBar) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 017eb6662..b313f4cb6 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 @@ -99,7 +99,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -292,9 +292,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -302,8 +302,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -321,7 +321,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 5a6594ee8..ed81f0d3d 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 @@ -94,7 +94,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -236,9 +236,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, new TokenUtils.OnTokenGenCallback() { @Override public void onTokenGen(String ret) { @@ -254,8 +254,8 @@ public void onTokenGen(String ret) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -275,7 +275,7 @@ public void onTokenGen(String ret) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 f28123cbc..3e14af009 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 @@ -130,7 +130,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -278,9 +278,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, new TokenUtils.OnTokenGenCallback() { @Override public void onTokenGen(String ret) { @@ -295,8 +295,8 @@ public void onTokenGen(String ret) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -315,7 +315,7 @@ public void onTokenGen(String ret) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 7763089fb..672f9b092 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 @@ -99,7 +99,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -266,9 +266,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -276,8 +276,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -296,7 +296,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 6271f8638..7aa31fca4 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 @@ -103,7 +103,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -274,9 +274,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -290,8 +290,8 @@ private void joinChannel(String channelId) { engine.stopPreview(); // 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -469,7 +469,7 @@ public void onSnapshotTaken(int uid, String filePath, int width, int height, int /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 3bc537b37..f9b15d93f 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) { /* * Creates an RtcEngine instance. * @param context The context of Android Activity - * @param appId The App ID issued to you by Agora. See + * @param appId The App ID issued to you by Agora. See * How to get the App ID * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events.*/ @@ -567,9 +567,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, myUid, token -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -589,8 +589,8 @@ private void joinChannel(String channelId) { engine.stopPreview(); // 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -697,7 +697,7 @@ private void takeSnapshot(int uid) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 9744c34f1..b18d2819b 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 @@ -99,7 +99,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -285,9 +285,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -296,8 +296,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -322,7 +322,7 @@ public void onLocalVideoTranscoderError(LocalTranscoderConfiguration.Transcoding /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 1b0b8d803..69269201c 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 @@ -113,7 +113,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -265,9 +265,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -275,8 +275,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -348,7 +348,7 @@ public void run() { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 8558548eb..d20aff361 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 @@ -116,7 +116,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -343,9 +343,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -353,8 +353,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -371,7 +371,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 e685e65de..7ca0bbd54 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 @@ -110,7 +110,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -270,9 +270,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -281,8 +281,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -455,7 +455,7 @@ private void resetLayoutRecording(@NonNull ViewGroup reportLayout) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 156bde73f..d3093e29a 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 @@ -133,7 +133,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -246,9 +246,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -261,8 +261,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -739,7 +739,7 @@ private VideoReportLayout getVideoLayoutByUid(int uid) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 a53186651..a4f83475a 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 @@ -115,7 +115,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -293,9 +293,9 @@ private void joinChannel(String channelId, boolean broadcast) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { @@ -305,8 +305,8 @@ private void joinChannel(String channelId, boolean broadcast) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -325,7 +325,7 @@ private void joinChannel(String channelId, boolean broadcast) { /** * Error code description can be found at: * en: {@see https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} - * cn: {@see https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} + * cn: {@see https://doc.shengwang.cn/api-ref/rtc/android/error-code} */ @Override public void onError(int err) { 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 de42b511c..aa797ec0b 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 @@ -109,7 +109,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -270,9 +270,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -281,8 +281,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -300,7 +300,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 366bdcb73..6456ee708 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 @@ -161,7 +161,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -338,9 +338,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -351,8 +351,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -381,7 +381,7 @@ private int getAudioScenarioValue(String label) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 30dba6300..ee07a7382 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 @@ -80,7 +80,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -237,7 +237,7 @@ public void run() { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 9cf067468..0d5dab9bd 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 @@ -147,7 +147,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -273,9 +273,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { ChannelMediaOptions option = new ChannelMediaOptions(); option.autoSubscribeAudio = true; @@ -284,8 +284,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -382,7 +382,7 @@ public AudioParams getEarMonitoringAudioParams() { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 ba7bd659a..e5d8b8b51 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 @@ -88,7 +88,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -249,9 +249,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ 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*/ @@ -264,8 +264,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -389,7 +389,7 @@ public int getObservedFramePosition() { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 6dc15611f..4062cafc2 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 @@ -134,7 +134,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -306,9 +306,9 @@ private void joinChannel(String channelId) { engine.startPreview(Constants.VideoSourceType.VIDEO_SOURCE_CUSTOM); /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -321,8 +321,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } 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 f473da0b6..69408d886 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 @@ -113,7 +113,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -245,9 +245,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -259,8 +259,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -272,12 +272,12 @@ private void joinChannel(String channelId) { private void startPublish() { if (transCodeSwitch.isChecked()) { /*LiveTranscoding: A class for managing user-specific CDN live audio/video transcoding settings. - * See */ + * See */ transcoding.width = dimensions.height; transcoding.height = dimensions.width; /*The transcodingUser class which defines the video properties of the user displaying the * video in the CDN live. Agora supports a maximum of 17 transcoding users in a CDN live streaming channel. - * See */ + * See */ LiveTranscoding.TranscodingUser localTranscodingUser = new LiveTranscoding.TranscodingUser(); localTranscodingUser.x = 0; localTranscodingUser.y = 0; @@ -357,7 +357,7 @@ private void stopPublish() { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 351d3084e..b7d4746ed 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 @@ -100,7 +100,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -234,9 +234,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -253,8 +253,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -272,7 +272,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 b9671fb1a..af62d71b6 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 @@ -154,7 +154,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -417,9 +417,9 @@ private void joinChannel() { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -436,8 +436,8 @@ private void joinChannel() { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -454,7 +454,7 @@ private void joinChannel() { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 34e903453..51026ab5b 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 @@ -100,7 +100,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -245,9 +245,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -259,8 +259,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -277,7 +277,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { @@ -477,7 +477,7 @@ public void run() { * The SDK triggers this callback when the local user fails to receive the stream message that the remote user sends by calling the sendStreamMessage method. * @param uid User ID of the remote user sending the data stream. * @param streamId Stream ID. - * @param error https://docs.agora.io/en/Video/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + * @param error https://docs.agora.io/en/realtime-media/rtc/reference/error-codes * @param missed The number of lost messages. * @param cached The number of incoming cached messages when the data stream is interrupted. */ 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 7ab138995..e1f2e75dd 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 @@ -169,7 +169,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -338,9 +338,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { ChannelMediaOptions option = new ChannelMediaOptions(); option.autoSubscribeAudio = true; @@ -349,8 +349,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -371,7 +371,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 1b7b35710..284f77937 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 @@ -119,7 +119,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -340,9 +340,9 @@ private void joinChannel(String channelId, boolean broadcast) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { @@ -352,8 +352,8 @@ private void joinChannel(String channelId, boolean broadcast) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -371,7 +371,7 @@ private void joinChannel(String channelId, boolean broadcast) { /** * Error code description can be found at: * en: {@see https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} - * cn: {@see https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} + * cn: {@see https://doc.shengwang.cn/api-ref/rtc/android/error-code} */ @Override public void onError(int err) { 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 1f6eb2c0b..85a5def5b 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 @@ -100,7 +100,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { try { /*Creates an RtcEngine instance. * @param context The context of Android Activity - * @param appId The App ID issued to you by Agora. See + * @param appId The App ID issued to you by Agora. See * How to get the App ID * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. * The SDK uses this class to report to the app on SDK runtime events.*/ @@ -221,9 +221,9 @@ private void joinChannel() { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { ChannelMediaOptions option = new ChannelMediaOptions(); @@ -235,8 +235,8 @@ private void joinChannel() { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -621,7 +621,7 @@ public void onJoinChannelSuccess(String channel, int uid, int elapsed) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 f00ec36ee..173f5284d 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 @@ -108,7 +108,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -355,9 +355,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -365,8 +365,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -384,7 +384,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 de37f2518..dbb24850e 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 @@ -96,7 +96,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -253,9 +253,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { @@ -265,8 +265,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -328,7 +328,7 @@ private void stopPlaying() { /** * Error code description can be found at: * en: {@see https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} - * cn: {@see https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} + * cn: {@see https://doc.shengwang.cn/api-ref/rtc/android/error-code} */ @Override public void onError(int err) { 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 8c2d4f05a..4072ca52e 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 @@ -192,7 +192,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -298,9 +298,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -313,8 +313,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -524,7 +524,7 @@ public void onStopTrackingTouch(SeekBar seekBar) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 570b2813a..4fd89bac9 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 @@ -101,7 +101,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -293,9 +293,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /*Allows a user to join a channel. * if you do not specify the uid, we will generate the uid for you. @@ -310,8 +310,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -326,7 +326,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 169d49471..ab34049c0 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 @@ -247,7 +247,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -403,9 +403,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, accessToken -> { /* Allows a user to join a channel. if you do not specify the uid, we will generate the uid for you*/ @@ -417,8 +417,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -435,8 +435,8 @@ private void joinChannel(String channelId) { 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 + * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 7fb035618..20cac446c 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 @@ -104,7 +104,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -238,9 +238,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { ChannelMediaOptions option = new ChannelMediaOptions(); @@ -255,8 +255,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -274,7 +274,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 2d03a55d3..aa6296f28 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 @@ -119,7 +119,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -280,9 +280,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -291,8 +291,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -310,7 +310,7 @@ private void joinChannel(String channelId) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { 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 66a9d48ba..9ed28cd7a 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 @@ -66,7 +66,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -128,9 +128,9 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { /* Allows a user to join a channel. @@ -139,8 +139,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); } }); @@ -212,7 +212,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat /** * Error code description can be found at: * en: ... - * cn: ... + * cn: ... */ @Override public void onError(int err) { 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 40991613c..332e52f29 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 @@ -64,7 +64,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -125,9 +125,9 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { /* Allows a user to join a channel. @@ -136,8 +136,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); } }); @@ -223,7 +223,7 @@ public void surfaceDestroyed(@NonNull SurfaceHolder holder) { /** * Error code description can be found at: * en: ... - * cn: ... + * cn: ... */ @Override public void onError(int err) { 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 44065f790..f8e18d665 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 @@ -65,7 +65,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -126,9 +126,9 @@ public void onCreate(@Nullable Bundle savedInstanceState) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { /* Allows a user to join a channel. @@ -137,8 +137,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); } }); @@ -223,7 +223,7 @@ public void surfaceDestroyed(@NonNull SurfaceHolder holder) { /** * Error code description can be found at: * en: ... - * cn: ... + * cn: ... */ @Override public void onError(int err) { 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 ba4ab3998..0d5edbc47 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 @@ -60,7 +60,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -176,9 +176,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -187,8 +187,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -208,7 +208,7 @@ private void joinChannel(String channelId) { /** * Error code description can be found at: * en: ... - * cn: ... + * cn: ... */ @Override public void onError(int error) { 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 10a98147c..aca159eb8 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 @@ -306,7 +306,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -566,9 +566,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ TokenUtils.gen(requireContext(), channelId, 0, ret -> { /* Allows a user to join a channel. @@ -577,8 +577,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); return; @@ -711,7 +711,7 @@ private String getAudioRouteKey(int routing) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int error) { 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 08d6d27af..2b899139a 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 @@ -98,7 +98,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { */ config.mContext = context.getApplicationContext(); /* - * The App ID issued to you by Agora. See How to get the App ID + * The App ID issued to you by Agora. See How to get the App ID */ config.mAppId = getAgoraAppId(); /* The channel profile. @@ -244,9 +244,9 @@ private void joinChannel(String channelId) { /* * 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 + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/authentication-workflow/android * 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*/ + * https://docs.agora.io/en/realtime-media/rtc/build/authenticate-users/deploy-token-server*/ int uid = new Random().nextInt(1000) + 100000; TokenUtils.gen(requireContext(), channelId, uid, ret -> { @@ -256,8 +256,8 @@ private void joinChannel(String channelId) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -275,7 +275,7 @@ private void joinChannel(String channelId) { /** * Error code description can be found at: * en: {@see https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} - * cn: {@see https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror} + * cn: {@see https://doc.shengwang.cn/api-ref/rtc/android/error-code} */ @Override public void onError(int err) { diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java index 6d33cfc25..75f912648 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideoByToken.java @@ -85,7 +85,7 @@ private boolean createRtcEngine(String appId) { // The context of Android Activity config.mContext = requireContext().getApplicationContext(); // The App ID issued to you by Agora. - // See How to get the App ID + // See How to get the App ID config.mAppId = appId; // The channel profile. // CHANNEL_PROFILE_COMMUNICATION(0): Communication. Agora recommends using the live streaming profile @@ -217,8 +217,8 @@ private void joinChannel(String channelId, String token) { 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 + // en: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes + // cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code showAlert(RtcEngine.getErrorDescription(Math.abs(res))); return; } @@ -236,7 +236,7 @@ private void joinChannel(String channelId, String token) { /** * 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 + * cn: https://doc.shengwang.cn/api-ref/rtc/android/error-code */ @Override public void onError(int err) { diff --git a/README.kr.md b/README.kr.md index 2d74c0acc..151e1c48e 100644 --- a/README.kr.md +++ b/README.kr.md @@ -25,7 +25,7 @@ ## 관련 자료 -- [FAQ](https://docs.agora.io/en/faq) 를 참조하여 기존 이슈 내용을 확인하세요. +- [FAQ](https://docs.agora.io/en/api-reference/faq) 를 참조하여 기존 이슈 내용을 확인하세요. - 더 많은 튜토리얼을 확인하려면 [Agora SDK Samples](https://github.com/AgoraIO) 를 살펴보세요. - 더 복잡한 Use case는 [Agora Use Case](https://github.com/AgoraIO-usecase) 를 참조하세요. - [Agora Community](https://github.com/AgoraIO-Community) 에서 개발자 커뮤니티 Repository를 확인할 수 있습니다. diff --git a/README.md b/README.md index 9226e27fd..6b9919c44 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ If you have any problems or suggestions regarding the sample projects, feel free ## Related resources -- Check our [FAQ](https://docs.agora.io/en/faq) to see if your issue has been recorded. +- Check our [FAQ](https://docs.agora.io/en/api-reference/faq) to see if your issue has been recorded. - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials. - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case. - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community). diff --git a/README.zh.md b/README.zh.md index 4cf227616..3810305a0 100644 --- a/README.zh.md +++ b/README.zh.md @@ -25,7 +25,7 @@ ## 相关资源 -- 你可以先参阅 [常见问题](https://docs.agora.io/cn/faq) +- 你可以先参阅 [常见问题](https://doc.shengwang.cn/faq/list) - 如果你想了解更多官方示例,可以参考 [官方 SDK 示例](https://github.com/AgoraIO) - 如果你想了解声网 SDK 在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase) - 如果你想了解声网的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample-Audio/APIExample-Audio/Common/GlobalSettings.swift b/iOS/APIExample-Audio/APIExample-Audio/Common/GlobalSettings.swift index 658f2cfc7..c8914ab2d 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Common/GlobalSettings.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Common/GlobalSettings.swift @@ -41,7 +41,7 @@ class SettingItem { class GlobalSettings { // The region for connection. This advanced feature applies to scenarios that have regional restrictions. - // For the regions that Agora supports, see https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Constants/AgoraAreaCode.html. After specifying the region, the SDK connects to the Agora servers within that region. + // For the regions that Agora supports, see https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraareacodetype. After specifying the region, the SDK connects to the Agora servers within that region. var area:AgoraAreaCodeType = .global static let shared = GlobalSettings() var settings:[String:SettingItem] = [ diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/AudioMixing.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/AudioMixing.swift index 711d71788..ff22db07a 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/AudioMixing.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/AudioMixing.swift @@ -153,7 +153,7 @@ class AudioMixingMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -303,8 +303,8 @@ extension AudioMixingMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -313,7 +313,7 @@ extension AudioMixingMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift index 9939feeb7..6a3fac0bb 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift @@ -103,7 +103,7 @@ class CustomAudioRenderMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -151,8 +151,8 @@ extension CustomAudioRenderMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en:https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en:https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -161,7 +161,7 @@ extension CustomAudioRenderMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift index 3c15032fc..199be93ba 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift @@ -92,7 +92,7 @@ class CustomAudioSourceMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -118,8 +118,8 @@ extension CustomAudioSourceMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -128,7 +128,7 @@ extension CustomAudioSourceMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift index 46c2b0a96..4a9524080 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift @@ -105,7 +105,7 @@ class CustomPcmAudioSourceMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -156,8 +156,8 @@ extension CustomPcmAudioSourceMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -166,7 +166,7 @@ extension CustomPcmAudioSourceMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/PrecallTest.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/PrecallTest.swift index 919c1d6e8..66f60b82d 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/PrecallTest.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/PrecallTest/PrecallTest.swift @@ -63,8 +63,8 @@ class PrecallTestEntry : BaseViewController let ret = agoraKit.startEchoTest(withConfig: testConfig) if ret != 0 { // for errors please take a look at: - // CN https://doc.shengwang.cn/api-ref/rtc/ios/error-code: - // EN https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // CN: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // EN https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode showAlert(title: "Error", message: "startEchoTest call failed: \(ret), please check your params") } showPopover(isValidate: false, seconds: 10) {[unowned self] in diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/RawAudioData.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/RawAudioData.swift index abc76f7db..4a46e0a72 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/RawAudioData.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RawAudioData/RawAudioData.swift @@ -11,7 +11,7 @@ /// 1.Register obesever: agoraKit.setAudioFrameDelegate(self) /// 2.Call back AgoraAudioFrameDelegate to get raw audio frame data /// -/// More detail: https://docs.agora.io/en/Interactive%20Broadcast/raw_data_audio_apple?platform=iOS +/// More detail: https://docs.agora.io/en/realtime-media/rtc/build/customize-audio-processing/stream-raw-audio/ios import AgoraRtcKit import AGEVideoLayout @@ -42,7 +42,7 @@ class RawAudioDataViewController: BaseViewController { NetworkManager.shared.generateToken(channelName: channelId, success: { token in let result = self.agoraKit.joinChannel(byToken: token, channelId: channelId, info: nil, uid: 0) if result != 0 { - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "Join channel failed with errorCode: \(result)") } @@ -117,7 +117,7 @@ extension RawAudioDataViewController: AgoraAudioFrameDelegate { // MARK: - AgoraRtcEngineDelegate extension RawAudioDataViewController: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code LogUtils.log(message: "Error occur: \(errorCode)", level: .error) self.showAlert(title: "Error", message: "Error: \(errorCode.description)") diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift index 08fbdadb6..cb1c508cd 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift @@ -104,7 +104,7 @@ class RhythmPlayerMain : BaseViewController if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -149,8 +149,8 @@ extension RhythmPlayerMain : AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -159,7 +159,7 @@ extension RhythmPlayerMain : AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/VoiceChanger.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/VoiceChanger.swift index a3b263e70..8f0ddca67 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/VoiceChanger.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/VoiceChanger.swift @@ -533,7 +533,7 @@ class VoiceChangerMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -559,8 +559,8 @@ extension VoiceChangerMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -569,7 +569,7 @@ extension VoiceChangerMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/JoinChannelAudioToken.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/JoinChannelAudioToken.swift index 955ed7c86..0eceafe7e 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/JoinChannelAudioToken.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/JoinChannelAudioToken.swift @@ -212,7 +212,7 @@ class JoinChannelAudioTokenMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -278,8 +278,8 @@ extension JoinChannelAudioTokenMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -288,7 +288,7 @@ extension JoinChannelAudioTokenMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift index bb0b9b2db..50bb53040 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift @@ -153,7 +153,7 @@ class JoinChannelAudioMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -220,8 +220,8 @@ extension JoinChannelAudioMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -230,7 +230,7 @@ extension JoinChannelAudioMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// en: https://api-ref.agora.io/en/voice-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample-Audio/README.md b/iOS/APIExample-Audio/README.md index bed41bd59..72b200780 100644 --- a/iOS/APIExample-Audio/README.md +++ b/iOS/APIExample-Audio/README.md @@ -9,7 +9,7 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ![](../pictures/ios_14_privacy.png) -[Solution](https://docs.agora.io/en/help/integration-issues/local_network_privacy) +[Solution](https://docs.agora.io/en/api-reference/faq/integration/local_network_privacy) ## Prerequisites @@ -69,7 +69,7 @@ You are all set. Now connect your iPhone or iPad device and run the project. ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample-Audio/README.zh.md b/iOS/APIExample-Audio/README.zh.md index 7ad708d4f..3e4872fee 100644 --- a/iOS/APIExample-Audio/README.zh.md +++ b/iOS/APIExample-Audio/README.zh.md @@ -10,7 +10,7 @@ ![](../pictures/ios_14_privacy.png) -[解决方案](https://docs.agora.io/en/help/integration-issues/local_network_privacy) +[解决方案](https://docs.agora.io/en/api-reference/faq/integration/local_network_privacy) ## 环境准备 @@ -51,7 +51,7 @@ pod install ## 联系我们 -- 常见问题请查看 [Agora FAQ](https://docs.agora.io/en/faq) +- 常见问题请查看 [Agora FAQ](https://doc.shengwang.cn/faq/list) - 更多示例请查看 [AgoraIO](https://github.com/AgoraIO) - 复杂场景示例请查看 [AgoraIO Use Cases](https://github.com/AgoraIO-usecase) - 社区维护项目请查看 [AgoraIO Community](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample-Audio/sdk.podspec b/iOS/APIExample-Audio/sdk.podspec index 976731aec..41e8f447c 100644 --- a/iOS/APIExample-Audio/sdk.podspec +++ b/iOS/APIExample-Audio/sdk.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=ios" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } spec.platform = :ios diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/StreamEncryption.m b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/StreamEncryption.m index cab6d1c81..885782203 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/StreamEncryption.m +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/StreamEncryption/StreamEncryption.m @@ -139,8 +139,8 @@ - (void)viewDidLoad { int ret = [self.agoraKit enableEncryption:YES encryptionConfig:config]; if (ret != 0) { // for errors please take a look at: - // CN https://docs.agora.io/cn/live-streaming-premium-legacy/API%20Reference/oc/Classes/AgoraRtcChannel.html?platform=iOS#//api/name/enableEncryption:encryptionConfig: - // EN https://docs.agora.io/en/video-calling/develop/media-stream-encryption#implement--media-stream-encryption + // CN https://doc.shengwang.cn/doc/rtc/ios/advanced-features/channel-encryption + // EN https://docs.agora.io/en/realtime-media/rtc/build/secure-and-protect-channels/media-stream-encryption [self showAlertWithTitle:@"Error" message:[NSString stringWithFormat:@"enableEncryption call failed: %d, please check your params", ret]]; } } diff --git a/iOS/APIExample-OC/README.md b/iOS/APIExample-OC/README.md index 160328e7a..579ab6fcb 100644 --- a/iOS/APIExample-OC/README.md +++ b/iOS/APIExample-OC/README.md @@ -9,7 +9,7 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ![](../pictures/ios_14_privacy.png) -[Solution](https://docs.agora.io/en/help/integration-issues/local_network_privacy) +[Solution](https://docs.agora.io/en/api-reference/faq/integration/local_network_privacy) ## Prerequisites @@ -69,7 +69,7 @@ You are all set. Now connect your iPhone or iPad device and run the project. ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample-OC/README.zh.md b/iOS/APIExample-OC/README.zh.md index 235e48f6f..35a6d2da0 100644 --- a/iOS/APIExample-OC/README.zh.md +++ b/iOS/APIExample-OC/README.zh.md @@ -10,7 +10,7 @@ ![](../pictures/ios_14_privacy.png) -[解决方案](https://docs.agora.io/en/help/integration-issues/local_network_privacy) +[解决方案](https://docs.agora.io/en/api-reference/faq/integration/local_network_privacy) ## 环境准备 @@ -51,7 +51,7 @@ pod install ## 联系我们 -- 常见问题请查看 [Agora FAQ](https://docs.agora.io/en/faq) +- 常见问题请查看 [Agora FAQ](https://doc.shengwang.cn/faq/list) - 更多示例请查看 [AgoraIO](https://github.com/AgoraIO) - 复杂场景示例请查看 [AgoraIO Use Cases](https://github.com/AgoraIO-usecase) - 社区维护项目请查看 [AgoraIO Community](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample-OC/sdk.podspec b/iOS/APIExample-OC/sdk.podspec index 976731aec..41e8f447c 100644 --- a/iOS/APIExample-OC/sdk.podspec +++ b/iOS/APIExample-OC/sdk.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=ios" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } spec.platform = :ios diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/Settings/GlobalSettings.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/Settings/GlobalSettings.swift index facb6c44a..2fd706148 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/Settings/GlobalSettings.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/Settings/GlobalSettings.swift @@ -42,7 +42,7 @@ class SettingItem { class GlobalSettings { // The region for connection. This advanced feature applies to scenarios that have regional restrictions. /** For the regions that Agora supports, - * see https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Constants/AgoraAreaCode.html. + * see https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraareacodetype. * After specifying the region, the SDK connects to the Agora servers within that region. */ var area: AgoraAreaCodeType = .global diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ARKit/ARKitRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ARKit/ARKitRTC.swift index dd69d1252..2f61a968b 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ARKit/ARKitRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ARKit/ARKitRTC.swift @@ -191,8 +191,8 @@ extension ARKitRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioMixing/AudioMixingRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioMixing/AudioMixingRTC.swift index 2d6c5a108..df7a3f69d 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioMixing/AudioMixingRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioMixing/AudioMixingRTC.swift @@ -263,8 +263,8 @@ extension AudioMixingRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioWaveform/AudioWaveformRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioWaveform/AudioWaveformRTC.swift index 1ea866c68..a0a7e3936 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioWaveform/AudioWaveformRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/AudioWaveform/AudioWaveformRTC.swift @@ -137,8 +137,8 @@ extension AudioWaveformRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ContentInspect/ContentInspectRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ContentInspect/ContentInspectRTC.swift index be014801b..a9340c1c7 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ContentInspect/ContentInspectRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ContentInspect/ContentInspectRTC.swift @@ -113,8 +113,8 @@ extension ContentInspectRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CreateDataStream/CreateDataStreamRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CreateDataStream/CreateDataStreamRTC.swift index 578d4f312..14ad6786f 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CreateDataStream/CreateDataStreamRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CreateDataStream/CreateDataStreamRTC.swift @@ -149,8 +149,8 @@ extension CreateDataStreamRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift index ef03afdfc..26fb5551c 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomAudioRender/CustomAudioRenderRTC.swift @@ -96,8 +96,8 @@ extension CustomAudioRenderRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en:https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en:https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomPCMAudioSource/CustomPCMAudioSourceRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomPCMAudioSource/CustomPCMAudioSourceRTC.swift index 3e1120526..3f93f7cf8 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomPCMAudioSource/CustomPCMAudioSourceRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/CustomPCMAudioSource/CustomPCMAudioSourceRTC.swift @@ -129,8 +129,8 @@ extension CustomPCMAudioSourceRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift index b69a51c68..3cc9ac0df 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LiveStreaming/LiveStreamingRTC.swift @@ -239,8 +239,8 @@ extension LiveStreamingRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LocalVideoComposition/LocalCompositeGraphRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LocalVideoComposition/LocalCompositeGraphRTC.swift index 77a288160..a1c4e7a2d 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LocalVideoComposition/LocalCompositeGraphRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/LocalVideoComposition/LocalCompositeGraphRTC.swift @@ -204,8 +204,8 @@ extension LocalCompositeGraphRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaChannelRelay/MediaChannelRelayRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaChannelRelay/MediaChannelRelayRTC.swift index 883a685e7..872a36386 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaChannelRelay/MediaChannelRelayRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaChannelRelay/MediaChannelRelayRTC.swift @@ -158,8 +158,8 @@ extension MediaChannelRelayRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaPlayer/MediaPlayerRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaPlayer/MediaPlayerRTC.swift index a919c9720..5719d6bbc 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaPlayer/MediaPlayerRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MediaPlayer/MediaPlayerRTC.swift @@ -240,8 +240,8 @@ extension MediaPlayerRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MutliCamera/MutliCameraRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MutliCamera/MutliCameraRTC.swift index 0b9667aef..8f3f7aad6 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MutliCamera/MutliCameraRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/MutliCamera/MutliCameraRTC.swift @@ -167,8 +167,8 @@ extension MutliCameraRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelRTC.swift index 18b8461bb..acdb2d29c 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelRTC.swift @@ -89,8 +89,8 @@ extension QuickSwitchChannelRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RTMPStream/RTMPStreamRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RTMPStream/RTMPStreamRTC.swift index 9e9a73dfb..ba21f038a 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RTMPStream/RTMPStreamRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RTMPStream/RTMPStreamRTC.swift @@ -145,8 +145,8 @@ extension RTMPStreamRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RhythmPlayer/RhythmPlayerRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RhythmPlayer/RhythmPlayerRTC.swift index 1a9df2014..10775a6d1 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RhythmPlayer/RhythmPlayerRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/RhythmPlayer/RhythmPlayerRTC.swift @@ -140,8 +140,8 @@ extension RhythmPlayerRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ScreenShare/ScreenShareRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ScreenShare/ScreenShareRTC.swift index cb5833ec4..1cc0a9201 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ScreenShare/ScreenShareRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/ScreenShare/ScreenShareRTC.swift @@ -217,8 +217,8 @@ extension ScreenShareRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/SpatialAudio/SpatialAudioRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/SpatialAudio/SpatialAudioRTC.swift index f0ee60546..522265bce 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/SpatialAudio/SpatialAudioRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/SpatialAudio/SpatialAudioRTC.swift @@ -203,8 +203,8 @@ extension SpatialAudioRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/StreamEncryption/StreamEncryptionRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/StreamEncryption/StreamEncryptionRTC.swift index c0dc7e9b7..08c45c10b 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/StreamEncryption/StreamEncryptionRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/StreamEncryption/StreamEncryptionRTC.swift @@ -55,9 +55,9 @@ class StreamEncryptionRTC: NSObject, ObservableObject { if ret != 0 { // for errors please take a look at: // swiftlint:disable line_length - // CN https://docs.agora.io/cn/live-streaming-premium-legacy/API%20Reference/oc/Classes/AgoraRtcChannel.html?platform=iOS#//api/name/enableEncryption:encryptionConfig: + // CN https://doc.shengwang.cn/doc/rtc/ios/advanced-features/channel-encryption // swiftlint:enable line_length - // EN https://docs.agora.io/en/video-calling/develop/media-stream-encryption#implement--media-stream-encryption + // EN https://docs.agora.io/en/realtime-media/rtc/build/secure-and-protect-channels/media-stream-encryption LogUtils.log(message: "enableEncryption call failed: \(ret), please check your params", level: .error) } } else { @@ -147,8 +147,8 @@ extension StreamEncryptionRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoMetadata/VideoMetaDataRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoMetadata/VideoMetaDataRTC.swift index e2a5edaf8..e240ffd77 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoMetadata/VideoMetaDataRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoMetadata/VideoMetaDataRTC.swift @@ -113,8 +113,8 @@ extension VideoMetaDataRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoProcess/VideoProcessRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoProcess/VideoProcessRTC.swift index ef4ebb801..dc194324b 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoProcess/VideoProcessRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VideoProcess/VideoProcessRTC.swift @@ -222,8 +222,8 @@ extension VideoProcessRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VoiceChanger/VoiceChangerRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VoiceChanger/VoiceChangerRTC.swift index 0ec215746..ea948b4c6 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VoiceChanger/VoiceChangerRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/VoiceChanger/VoiceChangerRTC.swift @@ -102,8 +102,8 @@ extension VoiceChangerRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelAudio/JoinChannelAudioRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelAudio/JoinChannelAudioRTC.swift index a479d4136..01402411a 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelAudio/JoinChannelAudioRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelAudio/JoinChannelAudioRTC.swift @@ -125,8 +125,8 @@ extension JoinChannelAudioRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorderRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorderRTC.swift index e2664c202..de26a789d 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorderRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorderRTC.swift @@ -173,8 +173,8 @@ extension JoinChannelVideoRecorderRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoTokenRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoTokenRTC.swift index 99d95a2a3..106ddfeb6 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoTokenRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoTokenRTC.swift @@ -106,8 +106,8 @@ extension JoinChannelVideoTokenRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo/JoinChannelVideoRTC.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo/JoinChannelVideoRTC.swift index fe036dae0..7321c7e17 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo/JoinChannelVideoRTC.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Basic/JoinChannelVideo/JoinChannelVideoRTC.swift @@ -103,8 +103,8 @@ extension JoinChannelVideoRTC: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample-SwiftUI/sdk.podspec b/iOS/APIExample-SwiftUI/sdk.podspec index 976731aec..41e8f447c 100644 --- a/iOS/APIExample-SwiftUI/sdk.podspec +++ b/iOS/APIExample-SwiftUI/sdk.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=ios" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } spec.platform = :ios diff --git a/iOS/APIExample/APIExample/Common/GlobalSettings.swift b/iOS/APIExample/APIExample/Common/GlobalSettings.swift index 9f2479801..957dc79d2 100644 --- a/iOS/APIExample/APIExample/Common/GlobalSettings.swift +++ b/iOS/APIExample/APIExample/Common/GlobalSettings.swift @@ -41,7 +41,7 @@ class SettingItem { class GlobalSettings { // The region for connection. This advanced feature applies to scenarios that have regional restrictions. /** For the regions that Agora supports, - * see https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Constants/AgoraAreaCode.html. + * see https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraareacodetype. * After specifying the region, the SDK connects to the Agora servers within that region. */ var area: AgoraAreaCodeType = .global diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ARKit/ARKit.swift b/iOS/APIExample/APIExample/Examples/Advanced/ARKit/ARKit.swift index d1ae730ae..2c516d0f1 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ARKit/ARKit.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/ARKit/ARKit.swift @@ -129,7 +129,7 @@ class ARKitMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -248,8 +248,8 @@ extension ARKitMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -258,7 +258,7 @@ extension ARKitMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift index 45c8364eb..37ea1d52a 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift @@ -256,8 +256,8 @@ extension AgoraBeautyMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift b/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift index 79cf47a1b..3668924f2 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift @@ -326,8 +326,8 @@ extension AudioMixingMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift b/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift index 411174ded..6193023ae 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/AudioRouterPlayer.swift @@ -317,8 +317,8 @@ extension AudioRouterPlayerMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/AudioWaveform.swift b/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/AudioWaveform.swift index 90b3cb42a..069373de7 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/AudioWaveform.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/AudioWaveform.swift @@ -194,8 +194,8 @@ extension AudioWaveformMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift b/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift index f312cce45..b5a43e5cf 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift @@ -11,7 +11,7 @@ /// 1.Enable content inspect: agoraKit.enableContentInspect(true, config:inspectConfig). /// 2.Call back mathod contentInspectResult of AgoraVideoFrameDelegate to get the inspect result. /// -/// More detail: https://docs.agora.io/cn/content-moderation/landing-page?platform=iOS +/// More detail: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/enablecontentinspect%28_%3Aconfig%3A%29 import AgoraRtcKit diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift b/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift index 81e78aa6f..6f996ded8 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift @@ -170,8 +170,8 @@ extension CreateDataStreamMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift index e9af18c1e..ab60a6261 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift @@ -143,8 +143,8 @@ extension CustomAudioRenderMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en:https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en:https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift index d9cf8791b..78f26ec8b 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift @@ -129,8 +129,8 @@ extension CustomAudioSourceMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift b/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift index 1ce0c8f82..cc790729f 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/CustomPcmAudioSource.swift @@ -162,8 +162,8 @@ extension CustomPcmAudioSourceMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift index c2fd325b8..591894685 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift @@ -136,8 +136,8 @@ extension CustomVideoRenderMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift index f1f602b9d..a0318d7da 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift @@ -187,8 +187,8 @@ extension CustomVideoSourcePushMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift index af12d74af..ed294594a 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift @@ -286,8 +286,8 @@ extension CustomVideoSourcePushMultiMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "didOccurWarning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift b/iOS/APIExample/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift index 3406a586b..950a09d17 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift @@ -570,8 +570,8 @@ extension LiveStreamingMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.swift b/iOS/APIExample/APIExample/Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.swift index d3d36e47f..86b4d19ab 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.swift @@ -243,8 +243,8 @@ extension LocalCompositeGraphMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift b/iOS/APIExample/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift index 8484038e8..46d54d84e 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift @@ -186,8 +186,8 @@ extension MediaChannelRelayMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift b/iOS/APIExample/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift index 1fd7cad0e..42d4b7978 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift @@ -321,8 +321,8 @@ extension MediaPlayerMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/MutliCamera/MutliCamera.swift b/iOS/APIExample/APIExample/Examples/Advanced/MutliCamera/MutliCamera.swift index 910c90c35..48a50ab2e 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/MutliCamera/MutliCamera.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/MutliCamera/MutliCamera.swift @@ -199,8 +199,8 @@ extension MutliCameraMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift b/iOS/APIExample/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift index 2e01007f5..267a07559 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift @@ -159,8 +159,8 @@ extension QuickSwitchChannel: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift b/iOS/APIExample/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift index 157dc400d..ab6d57a87 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift @@ -208,8 +208,8 @@ extension RTMPStreamingMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift b/iOS/APIExample/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift index a29f6f3d2..611816535 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift @@ -11,7 +11,7 @@ /// 1.Register obesever: agoraKit.setAudioFrameDelegate(self) /// 2.Call back AgoraAudioFrameDelegate to get raw audio frame data /// -/// More detail: https://docs.agora.io/en/Interactive%20Broadcast/raw_data_audio_apple?platform=iOS +/// More detail: https://docs.agora.io/en/realtime-media/rtc/build/customize-audio-processing/stream-raw-audio/ios import AgoraRtcKit import AGEVideoLayout diff --git a/iOS/APIExample/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift b/iOS/APIExample/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift index 4e0dfc10a..ba38bccef 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift @@ -114,8 +114,8 @@ class RawMediaDataMain: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -153,8 +153,8 @@ extension RawMediaDataMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) @@ -163,8 +163,8 @@ extension RawMediaDataMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift b/iOS/APIExample/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift index e3642fc2b..4a8f7717f 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift @@ -10,7 +10,7 @@ /// 1.Register obesever: agoraKit.setVideoFrameDelegate(self) /// 2.Call back AgoraVideoFrameDelegate to get raw video frame data /// -/// More detail: https://docs.agora.io/en/Interactive%20Broadcast/raw_data_video_apple?platform=iOS +/// More detail: https://docs.agora.io/en/realtime-media/rtc/build/capture-and-render-video/raw-video-processing/ios import AgoraRtcKit import AGEVideoLayout diff --git a/iOS/APIExample/APIExample/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift b/iOS/APIExample/APIExample/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift index 4e4bcbdb8..01ef9f3dc 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/RhythmPlayer/RhythmPlayer.swift @@ -149,8 +149,8 @@ extension RhythmPlayerMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift b/iOS/APIExample/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift index 276eb0187..9a67f975e 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift @@ -260,8 +260,8 @@ extension ScreenShareMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift b/iOS/APIExample/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift index 339199804..51f255549 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift @@ -168,8 +168,8 @@ extension SimpleFilterMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift b/iOS/APIExample/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift index 05e79b141..5968fb3fd 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift @@ -120,9 +120,9 @@ class StreamEncryptionMain: BaseViewController { if ret != 0 { // for errors please take a look at: // swiftlint:disable line_length - // CN https://docs.agora.io/cn/live-streaming-premium-legacy/API%20Reference/oc/Classes/AgoraRtcChannel.html?platform=iOS#//api/name/enableEncryption:encryptionConfig: + // CN https://doc.shengwang.cn/doc/rtc/ios/advanced-features/channel-encryption // swiftlint:enable line_length - // EN https://docs.agora.io/en/video-calling/develop/media-stream-encryption#implement--media-stream-encryption + // EN https://docs.agora.io/en/realtime-media/rtc/build/secure-and-protect-channels/media-stream-encryption self.showAlert(title: "Error", message: "enableEncryption call failed: \(ret), please check your params") } } else { @@ -216,8 +216,8 @@ extension StreamEncryptionMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/VideoChat.swift b/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/VideoChat.swift index c283f2f2b..6efd95025 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/VideoChat.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/VideoChat.swift @@ -206,8 +206,8 @@ extension VideoChatMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift b/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift index 69e7b08b3..bc3b3a01f 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift @@ -161,8 +161,8 @@ extension VideoMetadataMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift b/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift index 1b3154218..b3421f1a5 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift @@ -309,8 +309,8 @@ extension VideoProcessMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift b/iOS/APIExample/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift index b231881b6..a18c780c5 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift @@ -559,8 +559,8 @@ extension VoiceChangerMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift index b05b2779b..fdd4a84b0 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift @@ -297,8 +297,8 @@ extension JoinChannelAudioMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift index 9e397238b..abd8961e5 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift @@ -375,8 +375,8 @@ extension JoinChannelVideoRecorder: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift index 78753f743..266fca72b 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift @@ -172,7 +172,7 @@ class JoinChannelVideoToken: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraErrorCode.html + // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } @@ -197,8 +197,8 @@ extension JoinChannelVideoToken: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift index f960e0c88..00fa68356 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift @@ -198,8 +198,8 @@ extension JoinChannelVideoMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) diff --git a/iOS/APIExample/README.md b/iOS/APIExample/README.md index b4f7b1cf3..2b9a3a3f5 100644 --- a/iOS/APIExample/README.md +++ b/iOS/APIExample/README.md @@ -9,7 +9,7 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ![](../pictures/ios_14_privacy.png) -[Solution](https://docs.agora.io/en/help/integration-issues/local_network_privacy) +[Solution](https://docs.agora.io/en/api-reference/faq/integration/local_network_privacy) ## Prerequisites @@ -86,7 +86,7 @@ The extracted bundle is ignored by Git. ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample/README.zh.md b/iOS/APIExample/README.zh.md index fa92a2982..79f4bbc11 100644 --- a/iOS/APIExample/README.zh.md +++ b/iOS/APIExample/README.zh.md @@ -10,7 +10,7 @@ ![](../pictures/ios_14_privacy.png) -[解决方案](https://docs.agora.io/en/help/integration-issues/local_network_privacy) +[解决方案](https://docs.agora.io/en/api-reference/faq/integration/local_network_privacy) ## 环境准备 @@ -68,7 +68,7 @@ iOS/APIExample/APIExample/Resources/ ## 联系我们 -- 常见问题请查看 [Agora FAQ](https://docs.agora.io/en/faq) +- 常见问题请查看 [Agora FAQ](https://doc.shengwang.cn/faq/list) - 更多示例请查看 [AgoraIO](https://github.com/AgoraIO) - 复杂场景示例请查看 [AgoraIO Use Cases](https://github.com/AgoraIO-usecase) - 社区维护项目请查看 [AgoraIO Community](https://github.com/AgoraIO-Community) diff --git a/iOS/APIExample/fu.podspec b/iOS/APIExample/fu.podspec index 3fc502618..feb624064 100644 --- a/iOS/APIExample/fu.podspec +++ b/iOS/APIExample/fu.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=ios" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } spec.platform = :ios diff --git a/iOS/APIExample/sdk.podspec b/iOS/APIExample/sdk.podspec index 976731aec..41e8f447c 100644 --- a/iOS/APIExample/sdk.podspec +++ b/iOS/APIExample/sdk.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=ios" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } spec.platform = :ios diff --git a/iOS/APIExample/sense.podspec b/iOS/APIExample/sense.podspec index b4877fa64..2c0521e1b 100644 --- a/iOS/APIExample/sense.podspec +++ b/iOS/APIExample/sense.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=ios" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } spec.platform = :ios diff --git a/macOS/APIExample/Common/GlobalSettings.swift b/macOS/APIExample/Common/GlobalSettings.swift index cf3c87ffd..9c72c5e90 100644 --- a/macOS/APIExample/Common/GlobalSettings.swift +++ b/macOS/APIExample/Common/GlobalSettings.swift @@ -31,7 +31,7 @@ class SettingItem { class GlobalSettings { // The region for connection. This advanced feature applies to scenarios that have regional restrictions. - // For the regions that Agora supports, see https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Constants/AgoraAreaCode.html. After specifying the region, the SDK connects to the Agora servers within that region. + // For the regions that Agora supports, see https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraareacodetype. After specifying the region, the SDK connects to the Agora servers within that region. var area:AgoraAreaCodeType = .global static let shared = GlobalSettings() let resolutionSetting: SettingItem = SettingItem( diff --git a/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift b/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift index e5df33991..2fe86d8fd 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift @@ -153,8 +153,8 @@ class AgoraBeauty: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -378,8 +378,8 @@ extension AgoraBeauty: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -388,8 +388,8 @@ extension AgoraBeauty: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift b/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift index 69e49466d..2efcb54a9 100644 --- a/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift @@ -421,8 +421,8 @@ class AudioMixing: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -556,8 +556,8 @@ extension AudioMixing: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -566,8 +566,8 @@ extension AudioMixing: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift index c197bc3c6..caf9557bc 100644 --- a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift @@ -209,8 +209,8 @@ class ChannelMediaRelay: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } self.mediaRelayconfig.sourceInfo = AgoraChannelMediaRelayInfo(token: token) @@ -251,8 +251,8 @@ extension ChannelMediaRelay: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -261,8 +261,8 @@ extension ChannelMediaRelay: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift b/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift index 21470ef55..7ab57d455 100644 --- a/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift +++ b/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.swift @@ -12,7 +12,7 @@ /// 1.Enable content inspect: agoraKit.enableContentInspect(true, config:inspectConfig). /// 2.Call back mathod contentInspectResult of AgoraVideoFrameDelegate to get the inspect result. /// -/// More detail: https://docs.agora.io/cn/content-moderation/landing-page?platform=macOS +/// More detail: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/enablecontentinspect%28_%3Aconfig%3A%29 import AgoraRtcKit @@ -90,8 +90,8 @@ class ContentInspectViewController: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result ?? 0), please check your params") } }) @@ -136,7 +136,7 @@ extension ContentInspectViewController: AgoraRtcEngineDelegate { } func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - /// Error code description: https://docs.agora.io/en/Interactive%20Broadcast/error_rtc + /// Error code description: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes LogUtils.log(message: "Error occur: \(errorCode)", level: .error) self.showAlert(title: "Error", message: "Error: \(errorCode.description)") } @@ -166,4 +166,3 @@ extension ContentInspectViewController: AgoraRtcEngineDelegate { agoraKit?.setupRemoteVideo(videoCanvas) } } - diff --git a/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift b/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift index 6386bbc2e..fbddb72e0 100644 --- a/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift +++ b/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift @@ -205,8 +205,8 @@ class CreateDataStream: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -248,8 +248,8 @@ extension CreateDataStream: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -258,8 +258,8 @@ extension CreateDataStream: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift b/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift index 0e443cead..cd8e9b474 100644 --- a/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift +++ b/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift @@ -197,8 +197,8 @@ class CustomAudioRender: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -259,8 +259,8 @@ extension CustomAudioRender: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -269,8 +269,8 @@ extension CustomAudioRender: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift b/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift index 5a4764be8..3690ffacd 100644 --- a/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift +++ b/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift @@ -224,8 +224,8 @@ class CustomAudioSource: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -266,8 +266,8 @@ extension CustomAudioSource: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -276,8 +276,8 @@ extension CustomAudioSource: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift b/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift index 83360d8b6..ab26a86dc 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift +++ b/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift @@ -216,8 +216,8 @@ class CustomVideoRender: BaseViewController { self.isJoined = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -255,8 +255,8 @@ extension CustomVideoRender: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -265,8 +265,8 @@ extension CustomVideoRender: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift index 7a58cb4e4..0f218805c 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift @@ -250,8 +250,8 @@ class CustomVideoSourceMediaIO: BaseViewController { isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } } else { @@ -291,8 +291,8 @@ extension CustomVideoSourceMediaIO: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -301,8 +301,8 @@ extension CustomVideoSourceMediaIO: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift index a2c077942..cdbbaa1b0 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift @@ -254,8 +254,8 @@ class CustomVideoSourcePush: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result ?? 0), please check your params") } }) @@ -320,8 +320,8 @@ extension CustomVideoSourcePush: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -330,8 +330,8 @@ extension CustomVideoSourcePush: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift index eb2a083d0..44a02239f 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/CustomVideoSourcePushMulti.swift @@ -292,8 +292,8 @@ class CustomVideoSourcePushMulti: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel[\(channelName)] uid: \(connection.localUid) call failed: \(result), please check your params") } } @@ -418,8 +418,8 @@ extension CustomVideoSourcePushMulti: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -428,8 +428,8 @@ extension CustomVideoSourcePushMulti: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift index 107b39e8d..ad0c5102a 100644 --- a/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift +++ b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift @@ -78,8 +78,8 @@ class JoinMultipleChannel: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel1 call failed: \(result), please check your params") } else { @@ -146,8 +146,8 @@ class JoinMultipleChannel: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel2 call failed: \(result), please check your params") } else{ @@ -257,8 +257,8 @@ extension JoinMultipleChannel: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -267,8 +267,8 @@ extension JoinMultipleChannel: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) @@ -347,8 +347,8 @@ class JoinMultiChannelMainEventListener: NSObject, AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en:https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en:https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { if let connId = self.connectionId { @@ -359,8 +359,8 @@ class JoinMultiChannelMainEventListener: NSObject, AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { if let connId = self.connectionId { diff --git a/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift b/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift index d892920bc..e7d6484d2 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift @@ -425,8 +425,8 @@ class LiveStreamingMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -669,8 +669,8 @@ extension LiveStreamingMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -679,8 +679,8 @@ extension LiveStreamingMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift index 46b308f40..225546f5b 100644 --- a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift +++ b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift @@ -355,8 +355,8 @@ extension LocalVideoTranscoding: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -365,8 +365,8 @@ extension LocalVideoTranscoding: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift b/macOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift index eb216aae8..82463a9e3 100644 --- a/macOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift +++ b/macOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift @@ -117,8 +117,8 @@ class MediaPlayer: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) diff --git a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/MultiCameraSourece.swift b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/MultiCameraSourece.swift index 7fffd1c98..3f9ae61c2 100644 --- a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/MultiCameraSourece.swift +++ b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/MultiCameraSourece.swift @@ -438,8 +438,8 @@ class MultiCameraSoureceMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } self.isJoined = true @@ -476,8 +476,8 @@ class MultiCameraSoureceMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -512,8 +512,8 @@ extension MultiCameraSoureceMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -522,8 +522,8 @@ extension MultiCameraSoureceMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/Multipath/Multipath.swift b/macOS/APIExample/Examples/Advanced/Multipath/Multipath.swift index 08fe38804..b29209ded 100644 --- a/macOS/APIExample/Examples/Advanced/Multipath/Multipath.swift +++ b/macOS/APIExample/Examples/Advanced/Multipath/Multipath.swift @@ -193,8 +193,8 @@ class MultipathMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -245,8 +245,8 @@ extension MultipathMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -255,8 +255,8 @@ extension MultipathMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift b/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift index 6ebde272c..db15d372c 100644 --- a/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift @@ -242,8 +242,8 @@ class PrecallTest: BaseViewController { if ret != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code showAlert(title: "Error", message: "startEchoTest call failed: \(ret), please check your params") } } else { @@ -296,8 +296,8 @@ extension PrecallTest: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -306,8 +306,8 @@ extension PrecallTest: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift index 5bb92bc34..b39484668 100644 --- a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift +++ b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift @@ -11,7 +11,7 @@ /// 1.Register obesever: agoraKit.setVideoFrameDelegate(self) /// 2.Call back AgoraVideoFrameDelegate to get raw video frame data /// -/// More detail: https://docs.agora.io/en/Interactive%20Broadcast/raw_data_video_apple?platform=macOS +/// More detail: https://docs.agora.io/en/realtime-media/rtc/build/capture-and-render-video/raw-video-processing/macos import Foundation import AgoraRtcKit @@ -84,8 +84,8 @@ class QuickSwitchChannel: BaseViewController { NetworkManager.shared.generateToken(channelName: channelId, success: { token in let result = self.agoraKit.joinChannel(byToken: token, channelId: channelId, info: nil, uid: 0) if result != 0 { - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "Join channel failed with errorCode: \(result)") } }) @@ -117,7 +117,7 @@ class QuickSwitchChannel: BaseViewController { // MARK: - AgoraRtcEngineDelegate extension QuickSwitchChannel: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - /// Error code description: https://docs.agora.io/en/Interactive%20Broadcast/error_rtc + /// Error code description: https://docs.agora.io/en/realtime-media/rtc/reference/error-codes LogUtils.log(message: "error: \(errorCode)", level: .error) self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") } diff --git a/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift b/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift index 8787dc8eb..78f25fc63 100644 --- a/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift +++ b/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift @@ -204,8 +204,8 @@ class RTMPStreaming: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -245,8 +245,8 @@ extension RTMPStreaming: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -255,8 +255,8 @@ extension RTMPStreaming: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift b/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift index 9329463a3..65178de73 100644 --- a/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift +++ b/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift @@ -195,8 +195,8 @@ class RawAudioData: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -240,8 +240,8 @@ extension RawAudioData: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -250,8 +250,8 @@ extension RawAudioData: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift b/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift index 762b1c363..d72773b66 100644 --- a/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift +++ b/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift @@ -271,8 +271,8 @@ class RawMediaData: BaseViewController { isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } } else { @@ -311,8 +311,8 @@ extension RawMediaData: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -321,8 +321,8 @@ extension RawMediaData: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift b/macOS/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift index 6f488973d..736b479a8 100644 --- a/macOS/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift +++ b/macOS/APIExample/Examples/Advanced/RawVideoData/RawVideoData.swift @@ -11,7 +11,7 @@ /// 1.Register obesever: agoraKit.setVideoFrameDelegate(self) /// 2.Call back AgoraVideoFrameDelegate to get raw video frame data /// -/// More detail: https://docs.agora.io/en/Interactive%20Broadcast/raw_data_video_apple?platform=macOS +/// More detail: https://docs.agora.io/en/realtime-media/rtc/build/capture-and-render-video/raw-video-processing/macos import Foundation import AgoraRtcKit @@ -86,8 +86,8 @@ class RawVideoData: BaseViewController { NetworkManager.shared.generateToken(channelName: channelId, success: { token in let result = self.agoraKit.joinChannel(byToken: token, channelId: channelId, info: nil, uid: 0) if result != 0 { - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "Join channel failed with errorCode: \(result)") } }) @@ -136,8 +136,8 @@ extension RawVideoData: AgoraVideoFrameDelegate { // MARK: - AgoraRtcEngineDelegate extension RawVideoData: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code LogUtils.log(message: "error: \(errorCode)", level: .error) self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") } diff --git a/macOS/APIExample/Examples/Advanced/RtePlayer/RtePlayer.swift b/macOS/APIExample/Examples/Advanced/RtePlayer/RtePlayer.swift index 861eea34a..16e7b48ec 100644 --- a/macOS/APIExample/Examples/Advanced/RtePlayer/RtePlayer.swift +++ b/macOS/APIExample/Examples/Advanced/RtePlayer/RtePlayer.swift @@ -196,8 +196,8 @@ extension RtePlayer: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -206,8 +206,8 @@ extension RtePlayer: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift b/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift index aae4e5282..ee8c4cf81 100644 --- a/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift @@ -265,8 +265,8 @@ class ScreenShare: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "startScreenCapture call failed: \(result), please check your params") } else { isScreenSharing = true @@ -346,8 +346,8 @@ class ScreenShare: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en:https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en:https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "startScreenCapture call failed: \(result), please check your params") } else { isWindowSharing = true @@ -505,8 +505,8 @@ class ScreenShare: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -562,8 +562,8 @@ extension ScreenShare: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -572,8 +572,8 @@ extension ScreenShare: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift b/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift index d07b05d3f..ed7a78517 100644 --- a/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift +++ b/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift @@ -399,8 +399,8 @@ class SimpleFilterMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -434,8 +434,8 @@ extension SimpleFilterMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -444,8 +444,8 @@ extension SimpleFilterMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/Simulcast/Simulcast.swift b/macOS/APIExample/Examples/Advanced/Simulcast/Simulcast.swift index d8db1fd4d..b22bb9212 100644 --- a/macOS/APIExample/Examples/Advanced/Simulcast/Simulcast.swift +++ b/macOS/APIExample/Examples/Advanced/Simulcast/Simulcast.swift @@ -208,8 +208,8 @@ class SimulcastMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -327,8 +327,8 @@ extension SimulcastMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -337,8 +337,8 @@ extension SimulcastMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift b/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift index f9bcc94ec..98b527cf2 100644 --- a/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift +++ b/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift @@ -244,8 +244,8 @@ class StreamEncryption: BaseViewController { let ret = agoraKit.enableEncryption(true, encryptionConfig: config) if ret != 0 { // for errors please take a look at: - // CN https://docs.agora.io/cn/Video/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableEncryption:encryptionConfig: - // EN https://docs.agora.io/en/video-calling/develop/media-stream-encryption#implement--media-stream-encryption + // CN https://doc.shengwang.cn/doc/rtc/macos/advanced-features/channel-encryption + // EN https://docs.agora.io/en/realtime-media/rtc/build/secure-and-protect-channels/media-stream-encryption LogUtils.log(message: "enableEncryption call failed: \(ret), please check your params", level: .error) } // set up local video to render your local camera preview @@ -283,8 +283,8 @@ class StreamEncryption: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -345,8 +345,8 @@ extension StreamEncryption: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -355,8 +355,8 @@ extension StreamEncryption: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift b/macOS/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift index dc5aa827b..12d2e6bea 100644 --- a/macOS/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift +++ b/macOS/APIExample/Examples/Advanced/VideoProcess/VideoProcess.swift @@ -158,8 +158,8 @@ class VideoProcess: BaseViewController { if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -437,8 +437,8 @@ extension VideoProcess: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -447,8 +447,8 @@ extension VideoProcess: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift b/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift index d2d5b0d58..fe8fbc834 100644 --- a/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift @@ -603,8 +603,8 @@ class VoiceChanger: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -722,8 +722,8 @@ extension VoiceChanger: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -732,8 +732,8 @@ extension VoiceChanger: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift index a2fdae360..645641602 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift @@ -379,8 +379,8 @@ class JoinChannelAudioMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -460,8 +460,8 @@ extension JoinChannelAudioMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -470,8 +470,8 @@ extension JoinChannelAudioMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift index 2257273bc..80a0844b5 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift @@ -449,8 +449,8 @@ class JoinChannelVideoRecordMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -503,8 +503,8 @@ extension JoinChannelVideoRecordMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -513,8 +513,8 @@ extension JoinChannelVideoRecordMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift index c7769ae74..d834b1124 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift @@ -383,8 +383,8 @@ class JoinChannelVideoToken: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result ?? 0), please check your params") } @@ -426,8 +426,8 @@ extension JoinChannelVideoToken: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -436,8 +436,8 @@ extension JoinChannelVideoToken: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en:https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en:https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift b/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift index 3f382a468..f5e6d4620 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift @@ -366,8 +366,8 @@ class JoinChannelVideoMain: BaseViewController { self.isProcessing = false // Usually happens with invalid parameters // Error code description can be found at: - // en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - // cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + // en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + // cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") } }) @@ -408,8 +408,8 @@ extension JoinChannelVideoMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: - /// en: https://api-ref.agora.io/en/voice-sdk/ios/3.x/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorawarningcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) @@ -418,8 +418,8 @@ extension JoinChannelVideoMain: AgoraRtcEngineDelegate { /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand /// to let user know something wrong is happening /// Error code description can be found at: - /// en: https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agoraerrorcode - /// cn: https://doc.shengwang.cn/api-ref/rtc/ios/error-code + /// en: https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agoraerrorcode + /// cn: https://doc.shengwang.cn/api-ref/rtc/macos/error-code /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { LogUtils.log(message: "error: \(errorCode)", level: .error) diff --git a/macOS/README.md b/macOS/README.md index 1d6b3d833..44a26a884 100644 --- a/macOS/README.md +++ b/macOS/README.md @@ -80,7 +80,7 @@ The extracted bundle is ignored by Git. ## Contact Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/macOS/README.zh.md b/macOS/README.zh.md index 1b441db7a..eb433d3d5 100644 --- a/macOS/README.zh.md +++ b/macOS/README.zh.md @@ -58,7 +58,7 @@ macOS/APIExample/Resources/ ## 联系我们 -- 常见问题请查看 [Agora FAQ](https://docs.agora.io/en/faq) +- 常见问题请查看 [Agora FAQ](https://doc.shengwang.cn/faq/list) - 更多示例请查看 [AgoraIO](https://github.com/AgoraIO) - 复杂场景示例请查看 [AgoraIO Use Cases](https://github.com/AgoraIO-usecase) - 社区维护项目请查看 [AgoraIO Community](https://github.com/AgoraIO-Community) diff --git a/macOS/sdk.podspec b/macOS/sdk.podspec index 465004a14..a6487afb3 100644 --- a/macOS/sdk.podspec +++ b/macOS/sdk.podspec @@ -3,7 +3,7 @@ Pod::Spec.new do |spec| spec.version = "1.0" spec.summary = "Agora iOS video SDK" spec.description = "iOS library for agora A/V communication, broadcasting and data channel service." - spec.homepage = "https://docs.agora.io/en/Agora%20Platform/downloads" + spec.homepage = "https://docs.agora.io/en/api-reference/sdks?platform=macos" spec.license = { "type" => "Copyright", "text" => "Copyright 2018 agora.io. All rights reserved.\n"} spec.author = { "Agora Lab" => "developer@agora.io" } # spec.platform = :mac diff --git a/windows/APIExample/APIExample/APIExampleDlg.cpp b/windows/APIExample/APIExample/APIExampleDlg.cpp index 528cf177c..dfd230ed6 100755 --- a/windows/APIExample/APIExample/APIExampleDlg.cpp +++ b/windows/APIExample/APIExample/APIExampleDlg.cpp @@ -439,7 +439,7 @@ void CAPIExampleDlg::InitSceneList() void CAPIExampleDlg::OnBnClickedButtonFaq() { - ShellExecute(NULL, L"open", L"https://docs.agora.io/cn/Video/faq", NULL, NULL, SW_SHOWNORMAL); + ShellExecute(NULL, L"open", L"https://docs.agora.io/en/api-reference/faq", NULL, NULL, SW_SHOWNORMAL); } diff --git a/windows/README.md b/windows/README.md index d417f8405..08aa260b8 100644 --- a/windows/README.md +++ b/windows/README.md @@ -158,7 +158,7 @@ The `Release` directory is ignored by Git. ## Connect Us -- For potential issues, take a look at our [FAQ](https://docs.agora.io/cn/faq) first +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/api-reference/faq) first - Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials - Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case - Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) diff --git a/windows/README.zh.md b/windows/README.zh.md index 523840100..b6d87137f 100644 --- a/windows/README.zh.md +++ b/windows/README.zh.md @@ -181,7 +181,7 @@ windows/APIExample/Release/beauty_agora/ ## 联系我们 -- 如果你遇到了困难,可以先参阅[常见问题](https://docs.agora.io/cn/faq) +- 如果你遇到了困难,可以先参阅[常见问题](https://doc.shengwang.cn/faq/list) - 如果你想了解更多官方示例,可以参考[官方SDK示例](https://github.com/AgoraIO) - 如果你想了解声网SDK在复杂场景下的应用,可以参考[官方场景案例](https://github.com/AgoraIO-usecase) - 如果你想了解声网的一些社区开发者维护的项目,可以查看[社区](https://github.com/AgoraIO-Community) From 1734024ab7b338adceb1bb47049dca6f9355b629 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 10 Sep 2026 11:46:31 +0800 Subject: [PATCH 43/49] fix: harden AI templates and align example names Guard permission and token callbacks across leave, teardown, and restart. Correct Android SDK imports and initialization handling and the Objective-C Swift bridge interface. Add executable template lifecycle regressions and stage complete templates for application compilation in CI. Align example labels, localized resources, and architecture case indexes. --- .github/ci/README.md | 36 +++- .github/ci/policy/check_templates.py | 142 ++++++++++++++ .github/ci/policy/stage_compile_templates.py | 64 +++++++ .../template_fixtures/kotlin_dependencies.kt | 55 ++++++ .../template_fixtures/kotlin_scenarios.kt | 42 +++++ .../template_fixtures/objc_dependencies.h | 63 +++++++ .../tests/template_fixtures/objc_scenarios.m | 46 +++++ .../swift_dependencies.swift | 60 ++++++ .../template_fixtures/swift_scenarios.swift | 82 +++++++++ .github/workflows/compile.yml | 9 + .github/workflows/repository-policy.yml | 24 +++ .../.agents/skills/review-case/SKILL.md | 6 + .../references/fragment-template.java | 26 ++- Android/APIExample-Audio/ARCHITECTURE.md | 14 +- .../examples/advanced/PreCallTest.java | 2 +- .../example/examples/audio/AudioWaveform.java | 2 +- .../app/src/main/res/values/strings.xml | 10 +- .../.agents/skills/review-case/SKILL.md | 13 ++ .../.agents/skills/upsert-case/SKILL.md | 2 + .../references/composable-template.kt | 174 +++++++++++++----- Android/APIExample-Compose/AGENTS.md | 2 +- Android/APIExample-Compose/ARCHITECTURE.md | 34 ++-- .../api/example/compose/model/Components.kt | 10 +- .../api/example/compose/model/Examples.kt | 14 +- .../app/src/main/res/values/strings.xml | 28 +-- Android/APIExample/ARCHITECTURE.md | 68 +++---- .../examples/advanced/AgoraBeauty.java | 2 +- .../examples/advanced/ChannelEncryption.java | 2 +- .../advanced/CustomRemoteVideoRender.java | 2 +- .../advanced/JoinMultipleChannel.java | 2 +- .../examples/advanced/KtvCopyrightMusic.java | 2 +- .../advanced/LocalVideoTranscoding.java | 2 +- .../examples/advanced/MediaPlayer.java | 2 +- .../examples/advanced/MediaRecorder.java | 2 +- .../advanced/MultiVideoSourceTracks.java | 2 +- .../examples/advanced/PictureInPicture.java | 2 +- .../examples/advanced/PlayAudioFiles.java | 2 +- .../examples/advanced/PreCallTest.java | 2 +- .../advanced/ProcessAudioRawData.java | 2 +- .../examples/advanced/ProcessRawData.java | 2 +- .../advanced/PushExternalVideoYUV.java | 2 +- .../examples/advanced/ScreenSharing.java | 2 +- .../examples/advanced/SimpleExtension.java | 2 +- .../examples/advanced/ThirdPartyBeauty.java | 2 +- .../advanced/TransparentRendering.java | 2 +- .../examples/advanced/UrlLiveStream.java | 2 +- .../advanced/VideoProcessExtension.java | 2 +- .../examples/advanced/VideoQuickSwitch.java | 2 +- .../examples/audio/AudioRouterPlayer.java | 2 +- .../example/examples/audio/AudioWaveform.java | 2 +- .../app/src/main/res/values/strings.xml | 46 ++--- docs/ai/README.md | 6 + .../.agents/skills/review-case/SKILL.md | 26 ++- .../.agents/skills/upsert-case/SKILL.md | 94 ++++++++-- .../APIExample-Audio/ViewController.swift | 8 +- .../zh-Hans.lproj/Localizable.strings | 10 +- iOS/APIExample-Audio/ARCHITECTURE.md | 17 +- .../.agents/skills/review-case/SKILL.md | 40 ++-- .../.agents/skills/upsert-case/SKILL.md | 105 +++++++++-- .../CreateDataStream.storyboard | 6 +- .../APIExample-OC/ViewController.m | 24 +-- iOS/APIExample-OC/ARCHITECTURE.md | 34 ++-- .../zh-Hans.lproj/Localizable.strings | 21 +-- .../.agents/skills/review-case/SKILL.md | 13 ++ .../.agents/skills/upsert-case/SKILL.md | 84 +++++++-- .../APIExample-SwiftUI/ContentView.swift | 54 +++--- .../KtvCopyrightMusic/KtvCopyrightMusic.swift | 2 +- .../Resources/en.lproj/Localizable.strings | 46 +++-- .../zh-Hans.lproj/Localizable.strings | 24 ++- iOS/APIExample-SwiftUI/ARCHITECTURE.md | 23 ++- .../.agents/skills/review-case/SKILL.md | 36 ++-- .../.agents/skills/upsert-case/SKILL.md | 94 ++++++++-- .../Base.lproj/CreateDataStream.storyboard | 4 +- .../ThirdBeautify/ThirdBeautify.swift | 2 +- .../APIExample/ViewController.swift | 48 ++--- .../zh-Hans.lproj/Localizable.strings | 29 ++- iOS/APIExample/ARCHITECTURE.md | 39 ++-- .../Base.lproj/AgoraBeauty.storyboard | 2 +- .../AgoraBeauty/en.lproj/VideoProcess.strings | 4 +- .../zh-Hans.lproj/VideoProcess.strings | 4 +- .../Base.lproj/AudioMixing.storyboard | 2 +- .../AudioMixing/en.lproj/AudioMixing.strings | 4 +- .../zh-Hans.lproj/AudioMixing.strings | 4 +- .../Base.lproj/ChannelMediaRelay.storyboard | 2 +- .../zh-Hans.lproj/ChannelMediaRelay.strings | 4 +- .../ContentInspect/ContentInspect.storyboard | 2 +- .../Base.lproj/CreateDataStream.storyboard | 2 +- .../Base.lproj/CustomAudioSource.storyboard | 4 +- .../zh-Hans.lproj/CustomAudioSource.strings | 4 +- .../Base.lproj/CustomVideoRender.storyboard | 2 +- .../zh-Hans.lproj/CustomVideoRender.strings | 4 +- .../CustomVideoSourcePush.storyboard | 2 +- .../CustomVideoSourcePush.strings | 4 +- .../CustomVideoSourcePushMulti.storyboard | 2 +- .../CustomVideoSourcePushMulti.strings | 4 +- .../Base.lproj/LiveStreaming.storyboard | 2 +- .../en.lproj/LiveStreaming.storyboard | 2 +- .../en.lproj/LiveStreaming.strings | 4 +- .../zh-Hans.lproj/LiveStreaming.strings | 4 +- .../LocalVideoTranscoding.storyboard | 2 +- .../en.lproj/LocalVideoTranscoding.strings | 4 +- .../LocalVideoTranscoding.strings | 4 +- .../Base.lproj/MultiCameraSourece.storyboard | 2 +- .../zh-Hans.lproj/MultiCameraSourece.strings | 4 +- .../Base.lproj/PrecallTest.storyboard | 2 +- .../zh-Hans.lproj/PrecallTest.strings | 4 +- .../Base.lproj/QuickSwitchChannel.storyboard | 2 +- .../en.lproj/QuickSwitchChannel.storyboard | 2 +- .../zh-Hans.lproj/QuickSwitchChannel.strings | 4 +- .../Base.lproj/ScreenShare.storyboard | 2 +- .../zh-Hans.lproj/ScreenShare.strings | 4 +- .../Base.lproj/VoiceChanger.storyboard | 2 +- .../Advanced/VoiceChanger/VoiceChanger.swift | 2 +- .../zh-Hans.lproj/VoiceChanger.strings | 4 +- .../Base.lproj/JoinChannelAudio.storyboard | 2 +- .../zh-Hans.lproj/JoinChannelAudio.strings | 4 +- .../JoinChannelVideoRecorder.storyboard | 2 +- .../JoinChannelVideoRecorder.strings | 4 +- .../JoinChannelVideoToken.storyboard | 2 +- .../JoinChannelVideoToken.strings | 4 +- .../Base.lproj/JoinChannelVideo.storyboard | 2 +- .../zh-Hans.lproj/JoinChannelVideo.strings | 2 +- macOS/APIExample/ViewController.swift | 36 ++-- .../zh-Hans.lproj/Localizable.strings | 22 +-- .../APIExample/APIExample/APIExampleDlg.cpp | 40 ++-- windows/APIExample/APIExample/en.ini | 48 ++--- 126 files changed, 1664 insertions(+), 627 deletions(-) create mode 100644 .github/ci/policy/check_templates.py create mode 100644 .github/ci/policy/stage_compile_templates.py create mode 100644 .github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt create mode 100644 .github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt create mode 100644 .github/ci/policy/tests/template_fixtures/objc_dependencies.h create mode 100644 .github/ci/policy/tests/template_fixtures/objc_scenarios.m create mode 100644 .github/ci/policy/tests/template_fixtures/swift_dependencies.swift create mode 100644 .github/ci/policy/tests/template_fixtures/swift_scenarios.swift diff --git a/.github/ci/README.md b/.github/ci/README.md index 18554653b..9a59ea15b 100644 --- a/.github/ci/README.md +++ b/.github/ci/README.md @@ -7,7 +7,7 @@ policy helpers and the release packaging scripts loaded by Jenkins. | --- | --- | --- | | `Jenkinsfile_bitbucket.groovy` | External Jenkins shared library loads the release pipeline. | Verify job configuration and release evidence in the release ticket. | | `build/` | Jenkins platform Groovy entry points call packaging scripts, which use each project's `cloud_build.*` adapters. IPA entry points also depend on external Jenkins configuration. | Run the signing-preflight unit tests below; actual signing, private SDK injection and artifact verification belong to Jenkins. | -| `policy/` | `repository-policy.yml` runs AI asset validation and its regression tests; `.git-hooks/` supplies the local credential and commit-message checks. | Follow [AI validation](../../docs/ai/README.md) for venv setup and policy checks. | +| `policy/` | `repository-policy.yml` runs AI asset validation and native template lifecycle regressions; `.git-hooks/` supplies the local credential and commit-message checks. | Follow [AI validation](../../docs/ai/README.md) for venv setup and policy checks. | After creating the venv described above, run the packaging helper tests from the repository root: @@ -19,3 +19,37 @@ Keep the Jenkins entry point and `build/` paths stable: external jobs and existi refer to them directly. A lack of repository-local callers does not establish that an external entry point is unused. GitHub compilation uses public SDK dependencies and produces compile evidence; Jenkins owns release artifacts and signing evidence. + +## Template checks + +`policy/check_templates.py` extracts the canonical creation templates rather than maintaining +copies of their implementation. Its controlled SDK, UI and Token dependencies live in +`policy/tests/template_fixtures/`. These regressions exercise pending permission/Token exit, +repeated cleanup/setup, reopen, reordered responses and failure paths; they do not run RTC +sessions or prove SDK binary compatibility. The Apple check also compiles the OC template +against a Swift-generated header using the actual project's NetworkManager declaration. + +From the repository root: + +```bash +# macOS with Xcode command-line tools (Swift, Clang, Foundation) +python3 .github/ci/policy/check_templates.py apple +# JDK 17 and Kotlin CLI on PATH; KOTLINC can select a local compiler wrapper +python3 .github/ci/policy/check_templates.py kotlin +# Real Android/RTC interfaces, using the SDK selected for this checkout +python3 .github/ci/policy/check_templates.py android-sdk \ + --sdk-jar /path/to/agora-rtc-sdk.jar --android-jar /path/to/android.jar +``` + +Repository Policy runs Apple/Kotlin lifecycle checks on macOS/Ubuntu hosted runners; Ubuntu +supplies the Kotlin CLI. Missing compilers, missing template sections or missing inputs fail +the check rather than silently skipping it. No SDK download or credentials are needed for +these controlled regressions. + +`compile.yml` additionally calls `policy/stage_compile_templates.py` before compiling Android +Audio/Compose and all four iOS projects. The helper adds complete template sources to the +actual app target, including Xcode Sources membership, so its SDK and project interfaces are +checked with the normal build. It intentionally mutates the checkout: run it only in a +disposable CI/scratch checkout, never a developer working tree with pending changes. It does +not add menu entries or run the example. Template/fixture checks do not replace the normal +application build, permission UI integration, case registration or device validation. diff --git a/.github/ci/policy/check_templates.py b/.github/ci/policy/check_templates.py new file mode 100644 index 000000000..2510e3d57 --- /dev/null +++ b/.github/ci/policy/check_templates.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Compile/execute canonical template code. Missing compilers or inputs fail, never skip. + +Native lifecycle tests use controlled dependencies. android-sdk checks real SDK interfaces; +Compile CI additionally stages complete templates into their actual application targets. +""" +import argparse +import os +from pathlib import Path +import re +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[3] +FIXTURES = Path(__file__).parent / "tests/template_fixtures" + + +def run(*args, cwd): + subprocess.run([str(arg) for arg in args], cwd=cwd, check=True, timeout=180) + + +def token_declaration(project): + source = ROOT / "iOS" / project / project / "Common/NetworkManager/NetworkManager.swift" + # Extract only the declaration, never the request body, headers or configuration. + matches = re.findall(r"^\s*(func generateToken\([^\n]+)\s*\{\s*$", source.read_text(), re.M) + if len(matches) != 1: + raise ValueError(f"Expected one Token declaration in {source}") + return matches[0].strip() + + +def swift_blocks(project): + path = ROOT / "iOS" / project / ".agents/skills/upsert-case/SKILL.md" + return re.findall(r"```swift\n(.*?)\n```", path.read_text(), re.S) + + +def apple(work): + for project in ("APIExample", "APIExample-Audio", "APIExample-SwiftUI"): + source = swift_blocks(project)[0] + name = "RTC" if project.endswith("SwiftUI") else "Main" + source = source[source.index(f"class {name}:"):].replace("", "Template") + (work / "Template.swift").write_text("import Foundation\n" + source) + deps = (FIXTURES / "swift_dependencies.swift").read_text() + (work / "Dependencies.swift").write_text(deps.replace("__TOKEN_DECLARATION__", token_declaration(project))) + alias = "TemplateRTC" if project.endswith("SwiftUI") else "TemplateMain" + setup = 'sut.setupRTC(configs: ["channelName": "test"])' if project.endswith("SwiftUI") else "sut.setupRTC()" + scenarios = (FIXTURES / "swift_scenarios.swift").read_text() + (work / "main.swift").write_text(f"typealias TestSubject = {alias}\nfunc setup(_ sut: TestSubject) {{ {setup} }}\n" + scenarios) + run("xcrun", "swiftc", "-swift-version", "5", "-module-cache-path", work / "cache", + work / "Dependencies.swift", work / "Template.swift", work / "main.swift", "-o", work / "swift-test", cwd=work) + print(project, flush=True) + run(work / "swift-test", cwd=work) + + # Let Swift generate the actual Objective-C selector from the project's declaration. + bridge = '''import Foundation +@objc public class NetworkManager: NSObject { + @objc public static let shared = NetworkManager() + var requests: [(String, UInt, (String?) -> Void)] = [] + @objc public var requestCount: Int { requests.count } + @objc public __TOKEN_DECLARATION__ { requests.append((channelName, uid, success)) } + @objc public func respond(_ index: Int, token: String?) { requests[index].2(token) } +} +'''.replace("__TOKEN_DECLARATION__", token_declaration("APIExample-OC")) + (work / "Network.swift").write_text(bridge) + run("xcrun", "swiftc", "-swift-version", "5", "-module-cache-path", work / "cache", + "-emit-library", "-emit-objc-header", "-emit-objc-header-path", work / "APIExample_OC-swift.h", + "-module-name", "TemplateNetwork", work / "Network.swift", "-o", work / "libTemplateNetwork.dylib", cwd=work) + skill = (ROOT / "iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md").read_text() + implementation = re.findall(r"```objc\n(.*?)\n```", skill, re.S)[1] + imports = re.findall(r'^#import "([^"\n]+)"', implementation, re.M) + # Compile the import in the actual template; a nonexistent NetworkManager.h must fail. + bridge_imports = ''.join(f'#import "{name}"\n' for name in imports if name not in (".h", "KeyCenter.h")) + source = implementation[implementation.index("@interface Main ()"):].replace("", "Template") + (work / "main.m").write_text((FIXTURES / "objc_dependencies.h").read_text() + bridge_imports + source + "\n" + (FIXTURES / "objc_scenarios.m").read_text()) + run("xcrun", "clang", "-fobjc-arc", "-fblocks", "-Werror=objc-method-access", "-framework", "Foundation", + "-L", work, "-lTemplateNetwork", f"-Wl,-rpath,{work}", work / "main.m", "-o", work / "objc-test", cwd=work) + print("APIExample-OC", flush=True) + run(work / "objc-test", cwd=work) + + +def kotlin(work): + template = (ROOT / "Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt").read_text() + source = template[template.index("private class ExampleCaseSession("):template.index("\n@Preview")] + (work / "Main.kt").write_text((FIXTURES / "kotlin_dependencies.kt").read_text() + "\n" + source + "\n" + (FIXTURES / "kotlin_scenarios.kt").read_text()) + # A standard Kotlin CLI, including a caller-provided wrapper, may be selected via KOTLINC. + run(os.environ.get("KOTLINC", "kotlinc"), work / "Main.kt", "-include-runtime", "-d", work / "test.jar", cwd=work) + run("java", "-jar", work / "test.jar", cwd=work) + + +def android_sdk(work, sdk_jar, android_jar): + for path in (sdk_jar, android_jar): + if not path.is_file(): + raise ValueError(f"Missing SDK input: {path}") + template = ROOT / "Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java" + (work / "AudioExampleCaseTemplate.java").write_text(template.read_text()) + # Only project scaffolding is doubled; Android lifecycle and RTC signatures are real. + scaffolding = { + "androidx/annotation/Nullable.java": "package androidx.annotation; public @interface Nullable {}", + "io/agora/api/example/common/BaseFragment.java": "package io.agora.api.example.common; public class BaseFragment extends android.app.Fragment { protected String getAgoraAppId() { return \"\"; } }", + "io/agora/api/example/common/widget/AudioSeatManager.java": "package io.agora.api.example.common.widget; public class AudioSeatManager {}", + "io/agora/api/example/MainApplication.java": """package io.agora.api.example; +public class MainApplication extends android.app.Application { + public Settings getGlobalSettings() { return null; } + public static class Settings { + public int getAreaCode() { return 0; } + public io.agora.rtc2.proxy.LocalAccessPointConfiguration getPrivateCloudConfig() { return null; } + } +}""", + } + for relative, source in scaffolding.items(): + target = work / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(source) + compose = (ROOT / "Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt").read_text() + stats_imports = re.findall(r"^import (io\.agora\.[\w.]*RtcStats)$", compose, re.M) + if len(stats_imports) != 1: + raise ValueError("Expected one explicit SDK RtcStats import in Compose template") + (work / "StatsProbe.java").write_text(f"import {stats_imports[0]}; class StatsProbe {{ RtcStats stats; }}") + run("javac", "-J-Duser.language=en", "-J-Duser.country=US", "-classpath", os.pathsep.join(map(str, (sdk_jar, android_jar))), + "-d", work / "classes", *sorted(work.rglob("*.java")), cwd=work) + print("PASS: Audio lifecycle override and Compose RtcStats resolve against the supplied SDK", flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("check", choices=("apple", "kotlin", "android-sdk")) + parser.add_argument("--sdk-jar", type=Path) + parser.add_argument("--android-jar", type=Path) + args = parser.parse_args() + if args.check == "android-sdk" and (args.sdk_jar is None or args.android_jar is None): + parser.error("android-sdk requires --sdk-jar and --android-jar") + with tempfile.TemporaryDirectory(prefix="ai-template-check-") as directory: + work = Path(directory) + if args.check == "apple": + apple(work) + elif args.check == "kotlin": + kotlin(work) + else: + android_sdk(work, args.sdk_jar.resolve(), args.android_jar.resolve()) + + +if __name__ == "__main__": + main() diff --git a/.github/ci/policy/stage_compile_templates.py b/.github/ci/policy/stage_compile_templates.py new file mode 100644 index 000000000..ef702868e --- /dev/null +++ b/.github/ci/policy/stage_compile_templates.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Add complete template smoke sources to a disposable Compile CI checkout. + +This deliberately edits the checkout (and Xcode target membership). Do not run in a working +copy containing user edits. No SDK/configuration/credentials are generated by this helper. +""" +import argparse +from pathlib import Path +import re +import subprocess + +ROOT = Path(__file__).resolve().parents[3] +PROJECTS = ( + "Android/APIExample-Audio", "Android/APIExample-Compose", + "iOS/APIExample", "iOS/APIExample-Audio", "iOS/APIExample-SwiftUI", "iOS/APIExample-OC", +) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("project", choices=PROJECTS) + args = parser.parse_args() + project = ROOT / args.project + skill = project / ".agents/skills/upsert-case" + if args.project.startswith("Android/"): + audio = args.project.endswith("Audio") + template = skill / "references" / ("fragment-template.java" if audio else "composable-template.kt") + name = "AudioExampleCaseTemplate.java" if audio else "AITemplateSmoke.kt" + destination = project / "app/src/main/java/ai/templatecheck" / name + destination.parent.mkdir(parents=True, exist_ok=True) + with destination.open("x") as target: + target.write(template.read_text()) + else: + source = (skill / "SKILL.md").read_text() + if args.project.endswith("OC"): + blocks = re.findall(r"```objc\n(.*?)\n```", source, re.S) + files = {"AITemplateSmoke.h": blocks[0], "AITemplateSmoke.m": blocks[1]} + else: + blocks = re.findall(r"```swift\n(.*?)\n```", source, re.S) + count = 2 if args.project.endswith("SwiftUI") else 1 + files = {"AITemplateSmoke.swift": "\n".join(blocks[:count])} + for name, content in files.items(): + with (project / name).open("x") as target: + target.write(content.replace("", "AITemplateSmoke")) + # CocoaPods supplies xcodeproj. Compile the smoke source in the actual application target + # so SDK, UIKit/SwiftUI and the generated Swift-to-OC header come from that project. + subprocess.run([ + "ruby", "-rxcodeproj", "-e", """ +project = Xcodeproj::Project.open(ARGV.shift) +name = ARGV.shift +target = project.targets.find { |item| item.name == name } +abort("Missing application target #{name}") unless target +ARGV.each do |path| + reference = project.main_group.new_file(path) + target.source_build_phase.add_file_reference(reference) unless path.end_with?('.h') +end +project.save +""", str(project / (project.name + ".xcodeproj")), project.name, *files, + ], check=True, timeout=60) + print(f"Staged complete template for {args.project}; run its normal compile command next.") + + +if __name__ == "__main__": + main() diff --git a/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt b/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt new file mode 100644 index 000000000..d3bda0f94 --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt @@ -0,0 +1,55 @@ +// Controlled Android/SDK doubles. No network, native library or Compose runtime is used. +class Context { val applicationContext get() = this } +object Looper { fun getMainLooper() = this; fun myLooper() = this } +class Handler(looper: Looper) { + fun post(block: () -> Unit) { queue.add(block) } + companion object { + val queue = ArrayDeque<() -> Unit>() + fun drain() { while (queue.isNotEmpty()) queue.removeFirst()() } + } +} +object BuildConfig { const val AGORA_APP_ID = "" } +object SettingPreferences { fun getArea() = 0 } +object AgoraConfig { var certificate = ""; fun getAppCertificate() = certificate } +object TokenUtils { + data class Request(val channel: String, val uid: Int, val callback: (String?) -> Unit) + val requests = mutableListOf() + fun genToken(channel: String, uid: Int, callback: (String?) -> Unit) { + requests.add(Request(channel, uid, callback)) + } +} +open class IRtcEngineEventHandler { + class RtcStats + open fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {} + open fun onLeaveChannel(stats: RtcStats?) {} +} +typealias RtcStats = IRtcEngineEventHandler.RtcStats +class RtcEngineConfig { + var mAreaCode = 0 + var mContext: Context? = null + var mAppId = "" + var mEventHandler: IRtcEngineEventHandler? = null +} +class ChannelMediaOptions { var publishMicrophoneTrack = false } +class RtcEngine(val config: RtcEngineConfig) { + var destroyed = false + var leaves = 0 + val joins = mutableListOf>() + fun joinChannel(token: String?, channel: String, uid: Int, options: ChannelMediaOptions): Int { + check(!destroyed) { "join after destroy" } + joins.add(channel to uid) + return joinResult + } + fun leaveChannel() { check(!destroyed); leaves++ } + companion object { + val instances = mutableListOf() + var destroys = 0 + var failCreation = false + var joinResult = 0 + fun create(config: RtcEngineConfig): RtcEngine { + if (failCreation) throw Exception("controlled initialization failure") + return RtcEngine(config).also { instances.add(it) } + } + fun destroy() { destroys++; instances.last().destroyed = true } + } +} diff --git a/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt b/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt new file mode 100644 index 000000000..f7a776617 --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt @@ -0,0 +1,42 @@ +fun main() { + val errors = mutableListOf() + var joined = false + val sut = ExampleCaseSession({ joined = it }, { errors.add(it) }) + fun request(channel: String) { check(sut.beginJoin(channel)); sut.onPermissionResult(true) } + fun respond(index: Int, token: String? = "test-response") { + TokenUtils.requests[index].callback(token) + Handler.drain() + } + fun joins() = RtcEngine.instances.sumOf { it.joins.size } + RtcEngine.failCreation = true + sut.start(Context()); check(!sut.beginJoin("no-engine") && errors.size == 1) + RtcEngine.failCreation = false + sut.start(Context()); sut.start(Context()); check(RtcEngine.instances.size == 1) + val first = RtcEngine.instances.last() + request("old"); sut.dispose(); sut.dispose(); respond(0) + check(joins() == 0 && RtcEngine.destroys == 1 && first.leaves == 1) + sut.start(Context()); request("fresh"); respond(0); check(joins() == 0) + respond(1); check(joins() == 1 && RtcEngine.instances.last().joins.last() == ("fresh" to 0)) + sut.leave(); request("leaving"); sut.leave(); respond(2); check(joins() == 1) + request("superseded"); request("latest"); respond(4); respond(3) + check(joins() == 2 && RtcEngine.instances.last().joins.last().first == "latest") + sut.leave() + check(sut.beginJoin("permission-old")); check(!sut.beginJoin("second-permission")) + val count = TokenUtils.requests.size + sut.dispose(); sut.start(Context()) + check(!sut.beginJoin("new-before-old-permission-returns")) + sut.onPermissionResult(true); check(TokenUtils.requests.size == count) + check(sut.beginJoin("denied")); sut.onPermissionResult(false); check(TokenUtils.requests.size == count) + AgoraConfig.certificate = "enabled-in-test" + request("missing-token"); respond(count, null); check(joins() == 2) + AgoraConfig.certificate = "" + request("no-certificate"); respond(count + 1, null); check(joins() == 3) + sut.leave(); RtcEngine.joinResult = -1 + request("join-error"); respond(count + 2); check(errors.any { it.contains("joinChannel failed") }) + // Old SDK callback must not resurrect UI state after the same holder starts another engine. + first.config.mEventHandler!!.onJoinChannelSuccess("fresh", 0, 0) + Handler.drain(); check(!joined) + sut.dispose() + check(errors.none { it.contains("test-response") }) + println("PASS: initialization failure, pending exit, repeat cleanup/setup, reopen, leave, reordered responses, permission and token failures, join error") +} diff --git a/.github/ci/policy/tests/template_fixtures/objc_dependencies.h b/.github/ci/policy/tests/template_fixtures/objc_dependencies.h new file mode 100644 index 000000000..06a1663e7 --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/objc_dependencies.h @@ -0,0 +1,63 @@ +// Controlled SDK/UI doubles. The NetworkManager interface is generated by Swift separately. +#import +@protocol AgoraRtcEngineDelegate @end +typedef NSInteger AgoraErrorCode; +typedef NS_ENUM(NSInteger, AgoraClientRole) { AgoraClientRoleBroadcaster = 1 }; +@interface UIViewController : NSObject +- (void)viewDidLoad; +- (void)willMoveToParentViewController:(UIViewController *)parent; +@end +@implementation UIViewController +- (void)viewDidLoad {} +- (void)willMoveToParentViewController:(UIViewController *)parent {} +@end +@interface BaseViewController : UIViewController @end +@implementation BaseViewController @end +@interface TemplateMain : BaseViewController @end +@interface AgoraRtcEngineConfig : NSObject +@property NSString *appId; +@end +@implementation AgoraRtcEngineConfig @end +@interface AgoraRtcChannelMediaOptions : NSObject +@property AgoraClientRole clientRoleType; +@property BOOL publishMicrophoneTrack; +@end +@implementation AgoraRtcChannelMediaOptions @end +static NSMutableArray *engines; +static int destroys, joins, joinResult; +static NSString *lastChannel; +static NSUInteger lastUID; +@interface AgoraRtcEngineKit : NSObject +@property BOOL destroyed; +@property int leaves; ++ (instancetype)sharedEngineWithConfig:(AgoraRtcEngineConfig *)config delegate:(id)delegate; ++ (void)destroy; +- (int)leaveChannel:(id)completion; +- (int)joinChannelByToken:(NSString *)token channelId:(NSString *)channel uid:(NSUInteger)uid + mediaOptions:(AgoraRtcChannelMediaOptions *)options joinSuccess:(id)completion; +@end +@implementation AgoraRtcEngineKit ++ (instancetype)sharedEngineWithConfig:(AgoraRtcEngineConfig *)config delegate:(id)delegate { + if (!engines) engines = [NSMutableArray new]; + AgoraRtcEngineKit *engine = [self new]; + [engines addObject:engine]; + return engine; +} ++ (void)destroy { destroys++; ((AgoraRtcEngineKit *)engines.lastObject).destroyed = YES; } +- (int)leaveChannel:(id)completion { assert(!self.destroyed); self.leaves++; return 0; } +- (int)joinChannelByToken:(NSString *)token channelId:(NSString *)channel uid:(NSUInteger)uid + mediaOptions:(AgoraRtcChannelMediaOptions *)options joinSuccess:(id)completion { + assert([NSThread isMainThread] && !self.destroyed); + joins++; lastChannel = channel; lastUID = uid; + return joinResult; +} +@end +static NSString *certificate; +@interface KeyCenter : NSObject ++ (NSString *)AppId; ++ (NSString *)Certificate; +@end +@implementation KeyCenter ++ (NSString *)AppId { return @""; } ++ (NSString *)Certificate { return certificate; } +@end diff --git a/.github/ci/policy/tests/template_fixtures/objc_scenarios.m b/.github/ci/policy/tests/template_fixtures/objc_scenarios.m new file mode 100644 index 000000000..f40c10286 --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/objc_scenarios.m @@ -0,0 +1,46 @@ +static void drain(void) { + __block BOOL done = NO; + dispatch_async(dispatch_get_main_queue(), ^{ done = YES; }); + while (!done) [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.001]]; +} +static void request(TemplateMain *sut, NSString *channel) { + [sut requestJoin:channel requestPermission:^(void (^completion)(BOOL)) { completion(YES); }]; + drain(); +} +static void respond(NSInteger index, NSString *token) { + dispatch_sync(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [[NetworkManager shared] respond:index token:token]; + }); + drain(); +} +int main(void) { @autoreleasepool { + TemplateMain *sut = [TemplateMain new]; + [sut viewDidLoad]; [sut setupRTC]; + assert(engines.count == 1); + AgoraRtcEngineKit *first = engines.lastObject; + request(sut, @"old"); + [sut onDestroy]; [sut onDestroy]; respond(0, @"test-response"); + assert(joins == 0 && destroys == 1 && first.leaves == 1); + [sut setupRTC]; request(sut, @"fresh"); respond(0, @"test-response"); assert(joins == 0); + respond(1, @"test-response"); assert(joins == 1 && [lastChannel isEqual:@"fresh"] && lastUID == 0); + [sut leaveChannel]; request(sut, @"leaving"); [sut leaveChannel]; respond(2, @"test-response"); + assert(joins == 1); + request(sut, @"superseded"); request(sut, @"latest"); + respond(4, @"test-response"); respond(3, @"test-response"); + assert(joins == 2 && [lastChannel isEqual:@"latest"]); + __block void (^permission)(BOOL); + [sut requestJoin:@"permission-old" requestPermission:^(void (^completion)(BOOL)) { permission = completion; }]; + NSInteger count = [NetworkManager shared].requestCount; + [sut onDestroy]; [sut setupRTC]; permission(YES); drain(); + assert([NetworkManager shared].requestCount == count); + [sut requestJoin:@"denied" requestPermission:^(void (^completion)(BOOL)) { completion(NO); }]; + drain(); assert([NetworkManager shared].requestCount == count); + certificate = @"enabled-in-test"; + request(sut, @"missing-token"); respond(count, nil); assert(joins == 2); + certificate = nil; + request(sut, @"no-certificate"); respond(count + 1, nil); assert(joins == 3); + joinResult = -1; + request(sut, @"join-error"); respond(count + 2, @"test-response"); + [sut onDestroy]; + puts("PASS: OC bridge selector, pending exit, repeat cleanup/setup, reopen, leave, reordered responses, permission and token failures"); +} } diff --git a/.github/ci/policy/tests/template_fixtures/swift_dependencies.swift b/.github/ci/policy/tests/template_fixtures/swift_dependencies.swift new file mode 100644 index 000000000..956c9b8c5 --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/swift_dependencies.swift @@ -0,0 +1,60 @@ +// Controlled SDK/UI doubles: these check template control flow, not RTC SDK compatibility. +import Foundation +protocol ObservableObject {} +class UIViewController: NSObject { + func viewDidLoad() {} + func willMove(toParent parent: UIViewController?) {} +} +class BaseViewController: UIViewController {} +protocol AgoraRtcEngineDelegate: AnyObject {} +enum AgoraErrorCode: Int { case failed = 1 } +enum Role { case broadcaster } +enum Profile { case `default` } +class AgoraRtcEngineConfig { var appId = "" } +class AgoraRtcChannelMediaOptions { + var clientRoleType = Role.broadcaster + var publishMicrophoneTrack = false +} +class AgoraRtcEngineKit { + static var instances: [AgoraRtcEngineKit] = [] + static var destroys = 0 + static var joinResult: Int32 = 0 + var destroyed = false + var leaves = 0 + var joins: [(String, UInt, String?)] = [] + static func sharedEngine(with config: AgoraRtcEngineConfig, delegate: AgoraRtcEngineDelegate) -> AgoraRtcEngineKit { + let engine = AgoraRtcEngineKit() + instances.append(engine) + return engine + } + static func destroy() { + destroys += 1 + instances.last?.destroyed = true + } + func leaveChannel(_ callback: (() -> Void)? = nil) { + precondition(!destroyed, "leave after destroy") + leaves += 1 + } + func joinChannel(byToken token: String?, channelId: String, uid: UInt, + mediaOptions: AgoraRtcChannelMediaOptions) -> Int32 { + precondition(Thread.isMainThread, "join off main") + precondition(!destroyed, "join after destroy") + joins.append((channelId, uid, token)) + return Self.joinResult + } + func setAudioProfile(_ profile: Profile) {} +} +enum KeyCenter { static let AppId = ""; static var Certificate: String? } +enum LogUtils { + enum Level { case info, error } + static var messages: [String] = [] + static func log(message: String, level: Level) { messages.append(message) } +} +class NetworkManager { + static let shared = NetworkManager() + var requests: [(String, UInt, (String?) -> Void)] = [] + // The runner inserts the project's real generateToken declaration here. + __TOKEN_DECLARATION__ { + requests.append((channelName, uid, success)) + } +} diff --git a/.github/ci/policy/tests/template_fixtures/swift_scenarios.swift b/.github/ci/policy/tests/template_fixtures/swift_scenarios.swift new file mode 100644 index 000000000..44d3a1061 --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/swift_scenarios.swift @@ -0,0 +1,82 @@ +import Foundation +func drain() { + var done = false + DispatchQueue.main.async { done = true } + while !done { RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.001)) } +} +func request(_ sut: TestSubject, _ channel: String) { + sut.requestJoin(channelName: channel) { $0(true) } + drain() +} +func respond(_ index: Int, _ token: String? = "test-response") { + // The callback may arrive off main. The template must serialize it with teardown. + DispatchQueue.global().sync { NetworkManager.shared.requests[index].2(token) } + drain() +} +func totalJoins() -> Int { AgoraRtcEngineKit.instances.reduce(0) { $0 + $1.joins.count } } +let sut = TestSubject() +setup(sut) +let first = AgoraRtcEngineKit.instances.last! +setup(sut) +precondition(AgoraRtcEngineKit.instances.count == 1, "setup must be idempotent") + +// Exit while a token is pending; leave is required even without didJoinChannel. +request(sut, "old") +sut.onDestroy() +sut.onDestroy() +respond(0) +precondition(totalJoins() == 0 && first.leaves == 1 && AgoraRtcEngineKit.destroys == 1) + +// Reopen the same retained owner. An older response cannot use the new engine. +setup(sut) +request(sut, "fresh") +respond(0) +precondition(totalJoins() == 0) +respond(1) +precondition(totalJoins() == 1) +let second = AgoraRtcEngineKit.instances.last! +precondition(second.joins[0].0 == "fresh" && second.joins[0].1 == 0) + +// Leave without destroy also invalidates pending requests. +sut.leaveChannel() +request(sut, "leaving") +sut.leaveChannel() +respond(2) +precondition(totalJoins() == 1) + +// Latest request wins even when older replies arrive last. +request(sut, "superseded") +request(sut, "latest") +respond(4) +respond(3) +precondition(totalJoins() == 2 && second.joins.last!.0 == "latest") + +// Delayed permission grant after teardown must not even request a token. +var permission: ((Bool) -> Void)? +sut.requestJoin(channelName: "permission-old") { permission = $0 } +let requestsBeforeExit = NetworkManager.shared.requests.count +sut.onDestroy() +setup(sut) +permission?(true) +drain() +precondition(NetworkManager.shared.requests.count == requestsBeforeExit) + +// Denial and certificate-enabled token failure never join; no-certificate nil is valid. +sut.requestJoin(channelName: "denied") { $0(false) } +drain() +precondition(NetworkManager.shared.requests.count == requestsBeforeExit) +KeyCenter.Certificate = "enabled-in-test" +request(sut, "missing-token") +respond(requestsBeforeExit, nil) +precondition(totalJoins() == 2) +KeyCenter.Certificate = nil +request(sut, "no-certificate") +respond(requestsBeforeExit + 1, nil) +precondition(totalJoins() == 3) +AgoraRtcEngineKit.joinResult = -1 +request(sut, "join-error") +respond(requestsBeforeExit + 2) +precondition(LogUtils.messages.contains { $0.contains("joinChannel failed") }) +precondition(!LogUtils.messages.contains { $0.contains("test-response") }) +sut.onDestroy() +print("PASS: pending exit, repeated cleanup/setup, reopen, leave, reordered responses, permission and token failures, join error") diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index f2449ac44..772799177 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -45,6 +45,11 @@ jobs: - name: Configure compile-only App ID working-directory: ${{ matrix.project }} run: printf 'AGORA_APP_ID=%s\nAGORA_APP_CERT=\n' "$PLACEHOLDER_APP_ID" > local.properties + - name: Stage template compilation checks + if: matrix.name == 'Audio' || matrix.name == 'Compose' + env: + TEMPLATE_PROJECT: ${{ matrix.project }} + run: python3 .github/ci/policy/stage_compile_templates.py "$TEMPLATE_PROJECT" - name: Compile working-directory: ${{ matrix.project }} run: ./gradlew --no-daemon --stacktrace :app:assembleDebug @@ -103,6 +108,10 @@ jobs: - name: Install CocoaPods dependencies working-directory: ${{ matrix.project }} run: pod install --repo-update + - name: Stage template compilation checks + env: + TEMPLATE_PROJECT: ${{ matrix.project }} + run: python3 .github/ci/policy/stage_compile_templates.py "$TEMPLATE_PROJECT" - name: Compile working-directory: ${{ matrix.project }} env: diff --git a/.github/workflows/repository-policy.yml b/.github/workflows/repository-policy.yml index 6f0e92878..949a4f44b 100644 --- a/.github/workflows/repository-policy.yml +++ b/.github/workflows/repository-policy.yml @@ -205,3 +205,27 @@ jobs: - name: Validate AI assets run: python3 .github/ci/policy/validate_ai_assets.py + + template-lifecycle: + name: Template lifecycle (${{ matrix.check }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + include: + - check: apple + runner: macos-14 + - check: kotlin + runner: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + if: matrix.check == 'kotlin' + with: + distribution: temurin + java-version: "17" + - name: Compile and exercise template lifecycles + env: + TEMPLATE_CHECK: ${{ matrix.check }} + run: python3 .github/ci/policy/check_templates.py "$TEMPLATE_CHECK" diff --git a/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md index 3b3e95c23..50ccaf625 100644 --- a/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md +++ b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md @@ -46,3 +46,9 @@ Use this skill after a case has been created or modified in `APIExample-Audio/`. - Never approve any video API usage in the audio-only project - Never skip the build command + +## Initialization Failure + +`RtcEngine.create(config)` throws a checked exception. Catch it inside the Fragment lifecycle +override, release any returned engine if later configuration fails, clear the field and show +a failure state. Do not add `throws Exception` to the override or allow Join with a null engine. diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java index 65191135f..6efe7ee2b 100644 --- a/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java @@ -1,5 +1,6 @@ import android.content.Context; import android.os.Bundle; +import android.util.Log; import androidx.annotation.Nullable; @@ -35,13 +36,24 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { config.mEventHandler = iRtcEngineEventHandler; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); - engine = RtcEngine.create(config); - engine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\"" - + RtcEngine.getSdkVersion() + "\"}}"); - LocalAccessPointConfiguration privateCloud = - ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); - if (privateCloud != null) { - engine.setLocalAccessPoint(privateCloud); + try { + engine = RtcEngine.create(config); + engine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\"" + + RtcEngine.getSdkVersion() + "\"}}"); + LocalAccessPointConfiguration privateCloud = + ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); + if (privateCloud != null) { + engine.setLocalAccessPoint(privateCloud); + } + } catch (Exception error) { + // Creation/configuration can fail; release any engine already returned to us. + if (engine != null) { + engine.leaveChannel(); + RtcEngine.destroy(); + engine = null; + } + Log.e("AudioExample", "RTC initialization failed", error); + // Show a localized failure state in the concrete case; disable Join until retry succeeds. } } diff --git a/Android/APIExample-Audio/ARCHITECTURE.md b/Android/APIExample-Audio/ARCHITECTURE.md index 0d6697358..725ee5460 100644 --- a/Android/APIExample-Audio/ARCHITECTURE.md +++ b/Android/APIExample-Audio/ARCHITECTURE.md @@ -41,20 +41,20 @@ APIExample-Audio/ │ ├── 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" + │ │ ├── JoinChannelAudioByToken.java # [0] "Join a channel (Token)" + │ │ └── JoinChannelAudio.java # [1] "Join a channel (Audio)" │ ├── 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 + │ │ ├── VoiceEffects.java # [10] "Voice Effects" — setVoiceBeautifierPreset + │ │ ├── customaudio/CustomAudioSource.java # [11] "Custom Audio Source (PCM)" — 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 + │ │ ├── PreCallTest.java # [15] "Pre-call Test" — network/device test before joining │ │ ├── RhythmPlayer.java # Hidden — APIs deprecated since RTC SDK 4.6.0 │ │ └── SpatialSound.java # [17] "Spatial Audio" — 3D spatial audio │ └── audio/ # Audio-specific cases (grouped as ADVANCED) - │ └── AudioWaveform.java # [18] "Audio Waveform" — audio visualization + │ └── AudioWaveform.java # [16] "Audio Waveform" — audio visualization │ └── utils/ ├── ClassUtils.java # DEX scanner — auto-discovers @Example classes @@ -74,7 +74,7 @@ APIExample-Audio/ | 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 Source (PCM) | `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 | 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 1b39961ca..7024be23e 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java @@ -34,7 +34,7 @@ import io.agora.rtc2.proxy.LocalAccessPointConfiguration; @Example( - index = 15, + index = 16, group = ADVANCED, name = R.string.item_precalltest, actionId = R.id.action_mainFragment_to_PreCallTest, 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 1466d7a7d..65067bd98 100644 --- a/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java +++ b/Android/APIExample-Audio/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java @@ -28,7 +28,7 @@ import io.agora.rtc2.proxy.LocalAccessPointConfiguration; @Example( - index = 18, + index = 15, group = Examples.ADVANCED, name = R.string.item_audiowaveform, actionId = R.id.action_mainFragment_to_AudioWaveform, diff --git a/Android/APIExample-Audio/app/src/main/res/values/strings.xml b/Android/APIExample-Audio/app/src/main/res/values/strings.xml index 9269092e1..95e9f945e 100644 --- a/Android/APIExample-Audio/app/src/main/res/values/strings.xml +++ b/Android/APIExample-Audio/app/src/main/res/values/strings.xml @@ -22,15 +22,15 @@ Microphone Publish Local Audio - Live Interactive Audio Streaming - Live Interactive Audio Streaming(Token Verify) + Join a channel (Audio) + Join a channel (Token) Quick Switch Channel - Pre-call Tests + Pre-call Test Play Audio Files - Set the Voice Beautifier and Effects + Voice Effects Audio Waveform - Custom Audio Sources + Custom Audio Source (PCM) Custom Audio Render Raw Audio Data Spatial Audio diff --git a/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md index 5e1d495b2..11ee023f8 100644 --- a/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md +++ b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md @@ -9,6 +9,19 @@ description: > Compose. --- +## Pending Permission and Token Requests + +Review the complete lifecycle against [the creation template](../upsert-case/SKILL.md). +All request/engine ownership transitions must run on main. Freeze channel/UID and request +generation before requesting permission or Token; before joining, check that the generation +and engine identity are still current. Leave invalidates pending work even before joined; +destroy also clears the engine. Repeated cleanup must not destroy another case's engine. +Weak references alone do not invalidate a request when its owner remains alive. + +Exercise delayed permission and Token responses after leave/destroy, repeated cleanup, +rapid reopen and out-of-order responses. Only the current request may join. Also check +permission denial, absent required Token and a nonzero join result. Never log credentials. + ## What this skill is for Use this skill after a Compose case has been created or modified in `APIExample-Compose/`. It checks lifecycle, state, registration, and build verification before the case is treated as review-ready. diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md index f9f4fd1c4..317628845 100644 --- a/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md @@ -36,6 +36,8 @@ Use this skill to add or update a case in `APIExample-Compose/`. It owns the ful - Run `./gradlew assembleDebug` from `APIExample-Compose/` - Confirm the edited case updates `Examples.kt`, `strings.xml`, `values-zh/strings.xml` when needed, and `ARCHITECTURE.md` - Use `rememberSaveable` for user inputs, and keep joined status/assigned UID in `remember` with the current engine; a recreated engine starts without an RTC session. Keep `RtcEngine` and other non-serializable objects out of saved state. +- Exercise permission/Token pending → leave/dispose → delayed response, rapid reopen and reordered responses. Join must use the frozen channel/UID and current engine only. +- Keep one permission launch outstanding until its result is consumed, even if Leave invalidates that request. - Confirm the public Composable stays stateful and the preview stays on the private view function ## Out of scope diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt index 250d5a3f6..49dc9e351 100644 --- a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt @@ -1,12 +1,14 @@ package io.agora.api.example.compose.samples import android.Manifest +import android.content.Context +import android.os.Handler +import android.os.Looper import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -15,83 +17,164 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import io.agora.api.example.compose.BuildConfig import io.agora.api.example.compose.data.SettingPreferences +import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.utils.TokenUtils import io.agora.rtc2.ChannelMediaOptions import io.agora.rtc2.IRtcEngineEventHandler +import io.agora.rtc2.IRtcEngineEventHandler.RtcStats import io.agora.rtc2.RtcEngine import io.agora.rtc2.RtcEngineConfig -import io.agora.rtc2.RtcStats @Composable fun ExampleCase() { val context = LocalContext.current var channelName by rememberSaveable { mutableStateOf("") } var isJoined by remember { mutableStateOf(false) } - var localUid by remember { mutableIntStateOf(0) } - - val rtcEngine = remember { - RtcEngine.create(RtcEngineConfig().apply { - mAreaCode = SettingPreferences.getArea() - mContext = context - mAppId = BuildConfig.AGORA_APP_ID - mEventHandler = object : IRtcEngineEventHandler() { - override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { - super.onJoinChannelSuccess(channel, uid, elapsed) - isJoined = true - localUid = uid - } - - override fun onLeaveChannel(stats: RtcStats?) { - super.onLeaveChannel(stats) - isJoined = false - localUid = 0 - } - - // Add case-specific callbacks here for remote users, stats, or custom media flows. - } - }) + var errorMessage by remember { mutableStateOf(null) } + val session = remember { + ExampleCaseSession(onJoined = { isJoined = it }, onError = { errorMessage = it }) } - - DisposableEffect(rtcEngine) { - onDispose { - rtcEngine.leaveChannel() - RtcEngine.destroy() - } + DisposableEffect(session) { + session.start(context) + onDispose { session.dispose() } } - val permissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { grantedMap -> - if (grantedMap.values.all { it }) { - TokenUtils.gen(channelName, 0) { token -> - rtcEngine.joinChannel(token, channelName, 0, ChannelMediaOptions()) - } - } - } val requiredPermissions = arrayOf( Manifest.permission.RECORD_AUDIO, // Add Manifest.permission.CAMERA for video-capable cases. ) + val permissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { grantedMap -> + session.onPermissionResult(requiredPermissions.all { grantedMap[it] == true }) + } ExampleCaseView( channelName = channelName, isJoined = isJoined, + errorMessage = errorMessage, onJoinClick = { newChannelName -> channelName = newChannelName - permissionLauncher.launch(requiredPermissions) + if (session.beginJoin(newChannelName)) { + errorMessage = null + permissionLauncher.launch(requiredPermissions) + } }, - onLeaveClick = { - rtcEngine.leaveChannel() - } + onLeaveClick = { session.leave() } ) } +// Owns one engine. Call public methods on main; all asynchronous continuations return to main. +// Keep this class in the case file, with its own name when creating a concrete case. +private class ExampleCaseSession( + private val onJoined: (Boolean) -> Unit, + private val onError: (String) -> Unit +) { + private data class Request(val id: Long, val channel: String, val uid: Int = 0) + private val mainHandler = Handler(Looper.getMainLooper()) + private var engine: RtcEngine? = null + private var generation = 0L + private var engineGeneration = 0L + private var permissionRequest: Request? = null + private var activeChannel: String? = null + + fun start(context: Context) { + check(Looper.myLooper() == Looper.getMainLooper()) + if (engine != null) return + engineGeneration += 1 + val lifetimeID = engineGeneration + try { + engine = RtcEngine.create(RtcEngineConfig().apply { + mAreaCode = SettingPreferences.getArea() + mContext = context.applicationContext + mAppId = BuildConfig.AGORA_APP_ID + mEventHandler = object : IRtcEngineEventHandler() { + override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { + mainHandler.post { + if (engineGeneration == lifetimeID && engine != null && activeChannel == channel) onJoined(true) + } + } + override fun onLeaveChannel(stats: RtcStats?) { + mainHandler.post { + if (engineGeneration == lifetimeID && engine != null && activeChannel == null) onJoined(false) + } + } + // Dispatch case-specific callbacks to main and check current ownership too. + } + }) + } catch (error: Exception) { + onError("RTC initialization failed") + } + } + + fun beginJoin(channel: String): Boolean { + check(Looper.myLooper() == Looper.getMainLooper()) + // Only one permission launch may be outstanding: its result has no request identifier. + if (engine == null || channel.isBlank() || permissionRequest != null || activeChannel != null) return false + generation += 1 + permissionRequest = Request(generation, channel) + return true + } + + fun onPermissionResult(granted: Boolean) { + check(Looper.myLooper() == Looper.getMainLooper()) + val request = permissionRequest ?: return + permissionRequest = null + val expectedEngine = engine ?: return + if (request.id != generation) return + if (!granted) { + onError("Permission denied") + return + } + val tokenRequired = AgoraConfig.getAppCertificate().isNotEmpty() + // genToken supports projects without an App Certificate as well. + TokenUtils.genToken(request.channel, request.uid) { token -> + mainHandler.post { + if (request.id != generation || engine !== expectedEngine) return@post + if (tokenRequired && token.isNullOrEmpty()) { + onError("Token request failed") + return@post + } + val options = ChannelMediaOptions().apply { + publishMicrophoneTrack = true + // Configure video publication/canvases only after camera permission. + } + activeChannel = request.channel + val result = expectedEngine.joinChannel(token, request.channel, request.uid, options) + if (result != 0) { + activeChannel = null + onError("joinChannel failed: $result") + } + } + } + } + + fun leave() { + check(Looper.myLooper() == Looper.getMainLooper()) + generation += 1 + activeChannel = null + // Retain the pending permission slot until its obsolete result is consumed. + engine?.leaveChannel() + onJoined(false) + } + + fun dispose() { + leave() // Invalidate even if permission/Token/join is still pending. + if (engine != null) { + // Stop case-owned capture, players, timers and observers here. + RtcEngine.destroy() + engine = null + } + } +} + @Preview @Composable private fun ExampleCasePreview() { ExampleCaseView( channelName = "Channel Name", isJoined = false, + errorMessage = null, onJoinClick = {}, onLeaveClick = {} ) @@ -101,8 +184,9 @@ private fun ExampleCasePreview() { private fun ExampleCaseView( channelName: String, isJoined: Boolean, + errorMessage: String?, onJoinClick: (String) -> Unit, onLeaveClick: () -> Unit ) { - // Replace with case-specific UI while keeping preview on this private view function. + // Replace with case-specific UI; display localized error text and keep Preview on this view. } diff --git a/Android/APIExample-Compose/AGENTS.md b/Android/APIExample-Compose/AGENTS.md index 0102f3284..5a4a3dea7 100644 --- a/Android/APIExample-Compose/AGENTS.md +++ b/Android/APIExample-Compose/AGENTS.md @@ -20,7 +20,7 @@ See [README.md — Obtain an App Id](README.md#obtain-an-app-id). - 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. -- Keep one remembered `RtcEngine` per case and pair it with `DisposableEffect(rtcEngine) { onDispose { } }`. The effect must clean up the exact engine it owns. `onDispose` runs when the effect leaves composition or a key changes; `Unit` is valid when ownership is constant. Use a lifecycle-owner key only when engine creation and teardown both follow that owner's lifetime. +- Keep one remembered `RtcEngine` per case, directly or in a remembered case session holder. Pair ownership with `DisposableEffect(rtcEngine)` or `DisposableEffect(session) { onDispose { } }`; a session holder creates its engine in that effect. The effect must clean up the exact engine it owns. `onDispose` runs when the effect leaves composition or a key changes; `Unit` is valid when ownership is constant. Use a lifecycle-owner key only when engine creation and teardown both follow that owner's lifetime. - Save user inputs with `rememberSaveable`; keep live session state such as joined status and assigned UID with the current engine so it resets when that engine is recreated. - Always call `rtcEngine.leaveChannel()` before `RtcEngine.destroy()` in `onDispose`. - Permissions use `rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions())`. diff --git a/Android/APIExample-Compose/ARCHITECTURE.md b/Android/APIExample-Compose/ARCHITECTURE.md index 41db317fc..829dcbaa6 100644 --- a/Android/APIExample-Compose/ARCHITECTURE.md +++ b/Android/APIExample-Compose/ARCHITECTURE.md @@ -29,37 +29,37 @@ APIExample-Compose/ │ ├── model/ │ ├── Example.kt # data class: name: Int, content: @Composable - │ ├── Examples.kt # Hardcoded lists: BasicExampleList, AdvanceExampleList + │ ├── Examples.kt # Hardcoded lists: BasicExampleList, AdvancedExampleList │ └── 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" + │ ├── JoinChannelVideoToken.kt # Basic: "Join a channel (Token)" + │ ├── JoinChannelVideo.kt # Basic: "Join a channel (Video)" — canonical reference + │ ├── JoinChannelAudio.kt # Basic: "Join a channel (Audio)" │ ├── 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 + │ ├── OriginAudioData.kt # Advanced: "Raw Audio Data" — raw audio processing + │ ├── CustomAudioSource.kt # Advanced: "Custom Audio Source (PCM)" — push external audio │ ├── CustomAudioRender.kt # Advanced: "Custom Audio Render" — pull audio rendering - │ ├── OriginVideoData.kt # Advanced: "Origin Video Data" — raw video processing + │ ├── OriginVideoData.kt # Advanced: "Raw 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 + │ ├── JoinMultiChannel.kt # Advanced: "Join Multiple Channels" — multi-channel join + │ ├── ChannelEncryption.kt # Advanced: "Stream Encryption" — built-in encryption │ ├── PlayAudioFiles.kt # Advanced: "Play Audio Files" — audio mixing - │ ├── PreCallTest.kt # Advanced: "Pre Call Test" — network/device test + │ ├── 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 + │ ├── ScreenSharing.kt # Advanced: "Screen Share" — screen capture & share + │ ├── VideoProcessExtension.kt # Advanced: "Video Process" — video filter │ ├── RhythmPlayer.kt # Hidden — APIs deprecated since RTC SDK 4.6.0 │ ├── 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 + │ ├── HostAcrossChannel.kt # Advanced: "Media Channel Relay" — cross-channel relay + │ ├── SpatialSound.kt # Advanced: "Spatial Audio" — 3D spatial audio │ ├── ui/ │ ├── home/ @@ -103,7 +103,7 @@ APIExample-Compose/ | 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 Source (PCM) | `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 | @@ -129,9 +129,9 @@ Registration is **manual** — no reflection, no annotation scanning. **To add a case, update at least four project-local artifacts:** -**1. `model/Examples.kt`** — append to `BasicExampleList` or `AdvanceExampleList`: +**1. `model/Examples.kt`** — append to `BasicExampleList` or `AdvancedExampleList`: ```kotlin -val AdvanceExampleList = listOf( +val AdvancedExampleList = listOf( // … existing entries … Example(R.string.example_my_new_case) { MyNewCase() } ) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Components.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Components.kt index 164d8361f..5e947060c 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Components.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Components.kt @@ -12,13 +12,13 @@ val basicComponent = Component( examples = BasicExampleList ) -val advanceComponent = Component( +val advancedComponent = Component( 1, - "Advance", - examples = AdvanceExampleList + "Advanced", + examples = AdvancedExampleList ) val Components = listOf( basicComponent, - advanceComponent -) \ No newline at end of file + advancedComponent +) diff --git a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt index d93611bd0..db0a70d96 100644 --- a/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt +++ b/Android/APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt @@ -42,27 +42,27 @@ val BasicExampleList = listOf( Example(R.string.example_join_channel_audio) { JoinChannelAudio() } ) -val AdvanceExampleList = listOf( +val AdvancedExampleList = listOf( Example(R.string.example_live_streaming) { LiveStreaming() }, Example(R.string.example_rtmp_streaming) { RTMPStreaming() }, Example(R.string.example_media_metadata) { MediaMetadata() }, Example(R.string.example_voice_effects) { VoiceEffects() }, - Example(R.string.example_originaudiodata) { OriginAudioData() }, Example(R.string.example_customaudiosource) { CustomAudioSource() }, Example(R.string.example_customaudiorender) { CustomAudioRender() }, - Example(R.string.example_originvideodata) { OriginVideoData() }, + Example(R.string.example_originaudiodata) { OriginAudioData() }, + Example(R.string.example_playaudiofiles) { PlayAudioFiles() }, Example(R.string.example_customvideosource) { CustomVideoSource() }, Example(R.string.example_customvideorender) { CustomVideoRender() }, + Example(R.string.example_originvideodata) { OriginVideoData() }, Example(R.string.example_pictureinpicture) { PictureInPicture() }, + Example(R.string.example_videoprocessextension) { VideoProcessExtension() }, + Example(R.string.example_localvideotranscoding) { LocalVideoTranscoding() }, Example(R.string.example_joinmultichannel) { JoinMultiChannel() }, Example(R.string.example_channelencryption) { ChannelEncryption() }, - Example(R.string.example_playaudiofiles) { PlayAudioFiles() }, Example(R.string.example_precalltest) { PreCallTest() }, - Example(R.string.example_mediarecorder) { MediaRecorder() }, Example(R.string.example_mediaplayer) { MediaPlayer() }, + Example(R.string.example_mediarecorder) { MediaRecorder() }, Example(R.string.example_screensharing) { ScreenSharing() }, - Example(R.string.example_videoprocessextension) { VideoProcessExtension() }, - Example(R.string.example_localvideotranscoding) { LocalVideoTranscoding() }, Example(R.string.example_senddatastream) { SendDataStream() }, Example(R.string.example_hostacrosschannel) { HostAcrossChannel() }, Example(R.string.example_spatialsound) { SpatialSound() }, diff --git a/Android/APIExample-Compose/app/src/main/res/values/strings.xml b/Android/APIExample-Compose/app/src/main/res/values/strings.xml index 4c0119cb7..e6642e8d7 100644 --- a/Android/APIExample-Compose/app/src/main/res/values/strings.xml +++ b/Android/APIExample-Compose/app/src/main/res/values/strings.xml @@ -1,32 +1,32 @@ APIExample-Compose - Join Video Channel - Join Audio Channel + Join a channel (Video) + Join a channel (Audio) Live Streaming - Join Video Channel (With Token) + Join a channel (Token) RTMP Streaming Media Metadata Voice Effects - Custom Audio Source + Custom Audio Source (PCM) Custom Audio Render Custom Video Source Custom Video Render - Origin Audio Data - Origin Video Data - Join Multi Channel - Channel Encryption + Raw Audio Data + Raw Video Data + Join Multiple Channels + Stream Encryption Play Audio Files - Pre Call Test + Pre-call Test Media Recorder Media Player - Screen Sharing - Video Process Extension + Screen Share + Video Process Local Video Transcoding Send Data Stream Rhythm Player - Host Across Channel + Media Channel Relay Picture In Picture - Spaital Sound + Spatial Audio Encryption Mode Encryption Key @@ -113,4 +113,4 @@ Meeting 1V1 Live Show - \ No newline at end of file + diff --git a/Android/APIExample/ARCHITECTURE.md b/Android/APIExample/ARCHITECTURE.md index 56fb4c020..f4eb9b4e3 100644 --- a/Android/APIExample/ARCHITECTURE.md +++ b/Android/APIExample/ARCHITECTURE.md @@ -45,50 +45,50 @@ APIExample/ │ ├── 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" + │ │ ├── JoinChannelVideoByToken.java # [0] "Join a channel (Token)" + │ │ ├── JoinChannelVideo.java # [1] "Join a channel (Video)" + │ │ └── JoinChannelAudio.java # [2] "Join a channel (Audio)" │ ├── advanced/ # group = "ADVANCED" (index 10+) - │ │ ├── LiveStreaming.java # [10] "RTC Live Streaming" — setClientRole, broadcaster/audience - │ │ ├── RTMPStreaming.java # [11] "Push Streams to CDN" — RTMP push streaming + │ │ ├── LiveStreaming.java # [10] "Live Streaming" — setClientRole, broadcaster/audience + │ │ ├── RTMPStreaming.java # [11] "RTMP Streaming" — 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 + │ │ ├── VoiceEffects.java # [13] "Voice Effects" — setVoiceBeautifierPreset + │ │ ├── customaudio/CustomAudioSource.java # [14] "Custom Audio Source (PCM)" — 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 - │ │ ├── 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 + │ │ ├── PushExternalVideoYUV.java # [20] "Custom Video Source" — push YUV external video + │ │ ├── CustomRemoteVideoRender.java # [22] "Custom Video Render" — custom remote video rendering + │ │ ├── ProcessAudioRawData.java # [16] "Raw Audio Data" — audio raw data processing + │ │ ├── MultiVideoSourceTracks.java # [21] "Multi Video Source Tracks" — multiple video sources + │ │ ├── ProcessRawData.java # [23] "Raw Video Data" — video raw data processing + │ │ ├── SimpleExtension.java # [25] "Simple Filter Extension" — custom video extension + │ │ ├── PictureInPicture.java # [24] "Picture In Picture" — PiP mode + │ │ ├── VideoQuickSwitch.java # [28] "Quick Switch Channel" — fast channel switching + │ │ ├── JoinMultipleChannel.java # [29] "Join Multiple Channels" — multi-channel join + │ │ ├── ChannelEncryption.java # [30] "Stream Encryption" — built-in encryption + │ │ ├── PlayAudioFiles.java # [17] "Play Audio Files" — audio mixing + │ │ ├── PreCallTest.java # [31] "Pre-call Test" — network/device test before joining + │ │ ├── MediaPlayer.java # [32] "Media Player" — play media files + │ │ ├── MediaRecorder.java # [33] "Media Recorder" — record media streams + │ │ ├── ScreenSharing.java # [34] "Screen Share" — screen capture & share + │ │ ├── VideoProcessExtension.java # [26] "Video Process" — video filter extension + │ │ ├── LocalVideoTranscoding.java # [27] "Local Video Transcoding" — local video compositing │ │ ├── RhythmPlayer.java # Hidden — APIs deprecated since RTC SDK 4.6.0 │ │ ├── SendDataStream.java # [35] "Send Data Stream" — data channel messaging - │ │ ├── HostAcrossChannel.java # [36] "Relay Streams across Channels" — cross-channel relay + │ │ ├── HostAcrossChannel.java # [36] "Media Channel Relay" — 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 + │ │ ├── ThirdPartyBeauty.java # [40] "Third-party Beauty" — third-party beauty SDK + │ │ ├── KtvCopyrightMusic.java # [39] "KTV Copyright Music" — licensed music + │ │ ├── TransparentRendering.java # [42] "Transparent Rendering" — alpha channel rendering + │ │ ├── UrlLiveStream.java # [43] "URL Streaming (RTE Player)" — URL-based live stream + │ │ ├── AgoraBeauty.java # [41] "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 BASIC) - │ ├── AudioWaveform.java # [5] "Audio Waveform" — audio visualization - │ ├── AudioRouterPlayer.java # [6] "AudioRouter(Third Party Player)" — third-party audio routing + │ ├── AudioWaveform.java # [18] "Audio Waveform" — audio visualization + │ ├── AudioRouterPlayer.java # [19] "Audio Router (Third-party Player)" — third-party audio routing │ └── AudioRouterPlayer*.java # Exo / Ijk / Native variants │ ├── service/ @@ -118,7 +118,7 @@ APIExample/ | 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 Source (PCM) | `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 | @@ -146,7 +146,7 @@ APIExample/ | 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` | `createVideoEffectObject()`, `setVideoEffectStringParam()`, `enableVirtualBackground()` | Demonstrates built-in Agora beauty effects with face shaping and virtual background | +| Beauty 2.0 | `advanced/AgoraBeauty.java` | `createVideoEffectObject()`, `setVideoEffectStringParam()`, `enableVirtualBackground()` | 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 | 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 4e536023b..00cadf51b 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AgoraBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AgoraBeauty.java @@ -54,7 +54,7 @@ * The type Agora beauty. */ @Example( - index = 43, + index = 41, group = ADVANCED, name = R.string.item_agora_beauty, actionId = R.id.action_mainFragment_agora_beauty, 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 b313f4cb6..67d794639 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java @@ -47,7 +47,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 26, + index = 30, group = ADVANCED, name = R.string.item_channelencryption, actionId = R.id.action_mainFragment_to_channel_encryption, 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 3e14af009..09b8c5432 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java @@ -56,7 +56,7 @@ * This example demonstrates how to customize the renderer to render the local scene of the remote video stream. */ @Example( - index = 17, + index = 22, group = ADVANCED, name = R.string.item_customremoterender, actionId = R.id.action_mainFragment_to_CustomRemoteRender, 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 7aa31fca4..c39837d26 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java @@ -49,7 +49,7 @@ * The type Join multiple channel. */ @Example( - index = 25, + index = 29, group = ADVANCED, name = R.string.item_joinmultichannel, actionId = R.id.action_mainFragment_to_MultiChannel, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java index c19e3a055..109b47574 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/KtvCopyrightMusic.java @@ -12,7 +12,7 @@ * The type Ktv copyright music. */ @Example( - index = 40, + index = 39, group = ADVANCED, name = R.string.item_ktv_copyright_music, actionId = R.id.action_mainFragment_to_ktv_copyright_music, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java index b18d2819b..0f9d6f902 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LocalVideoTranscoding.java @@ -49,7 +49,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 33, + index = 27, group = ADVANCED, name = R.string.item_localvideotranscoding, actionId = R.id.action_mainFragment_to_LocalVideoTranscoding, 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 d20aff361..839956468 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayer.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayer.java @@ -65,7 +65,7 @@ * The type Media player. */ @Example( - index = 29, + index = 32, group = ADVANCED, name = R.string.item_mediaplayer, actionId = R.id.action_mainFragment_to_MediaPlayer, 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 7ca0bbd54..f36ca3212 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaRecorder.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaRecorder.java @@ -54,7 +54,7 @@ * The type Media recorder. */ @Example( - index = 30, + index = 33, group = ADVANCED, name = R.string.item_media_recorder, actionId = R.id.action_mainFragment_to_MediaRecorder, 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 d3093e29a..f3b93cb8b 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiVideoSourceTracks.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiVideoSourceTracks.java @@ -66,7 +66,7 @@ * The type Multi video source tracks. */ @Example( - index = 19, + index = 21, group = ADVANCED, name = R.string.item_multiVideoSourceTracks, actionId = R.id.action_mainFragment_to_MultiVideoSourceTracks, 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 aa797ec0b..f53a6fe30 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PictureInPicture.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PictureInPicture.java @@ -50,7 +50,7 @@ /** * This demo demonstrates how to make a one-to-one video call */ -@Example(index = 22, group = ADVANCED, name = R.string.item_picture_in_picture, actionId = R.id.action_mainFragment_to_picture_in_picture, tipsId = R.string.picture_in_picture) +@Example(index = 24, group = ADVANCED, name = R.string.item_picture_in_picture, actionId = R.id.action_mainFragment_to_picture_in_picture, tipsId = R.string.picture_in_picture) public class PictureInPicture extends BaseFragment implements View.OnClickListener { private static final String TAG = PictureInPicture.class.getSimpleName(); 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 6456ee708..c9f521aaf 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java @@ -43,7 +43,7 @@ * The type Play audio files. */ @Example( - index = 27, + index = 17, group = ADVANCED, name = R.string.item_playaudiofiles, actionId = R.id.action_mainFragment_to_PlayAudioFiles, 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 ee07a7382..352551859 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java @@ -36,7 +36,7 @@ * The type Pre call test. */ @Example( - index = 28, + index = 31, group = ADVANCED, name = R.string.item_precalltest, actionId = R.id.action_mainFragment_to_PreCallTest, 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 0d5dab9bd..f59db1acd 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java @@ -44,7 +44,7 @@ * @author cjw */ @Example( - index = 18, + index = 16, group = ADVANCED, name = R.string.item_raw_audio, actionId = R.id.action_mainFragment_raw_audio, 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 e5d8b8b51..2cbca4184 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java @@ -55,7 +55,7 @@ * The type Process raw data. */ @Example( - index = 20, + index = 23, group = ADVANCED, name = R.string.item_processraw, actionId = R.id.action_mainFragment_to_ProcessRawData, 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 4062cafc2..2c68eb17e 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideoYUV.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideoYUV.java @@ -59,7 +59,7 @@ * The type Push external video yuv. */ @Example( - index = 16, + index = 20, group = ADVANCED, name = R.string.item_pushexternal, actionId = R.id.action_mainFragment_to_PushExternalVideo, 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 af62d71b6..82c93d6d0 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ScreenSharing.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ScreenSharing.java @@ -60,7 +60,7 @@ * screen share stream during an audio-video call. */ @Example( - index = 31, + index = 34, group = ADVANCED, name = R.string.item_screensharing, actionId = R.id.action_mainFragment_to_ScreenSharing, 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 e1f2e75dd..da0def571 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SimpleExtension.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SimpleExtension.java @@ -46,7 +46,7 @@ * @author cjw */ @Example( - index = 21, + index = 25, group = ADVANCED, name = R.string.item_ext, actionId = R.id.action_mainFragment_extension, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java index 1105a709c..33915f87d 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java @@ -29,7 +29,7 @@ * The type Third party beauty. */ @Example( - index = 39, + index = 40, group = ADVANCED, name = R.string.item_third_party_beauty, actionId = R.id.action_mainFragment_to_third_party_beauty, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java index dbb24850e..a3b6bfd03 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java @@ -45,7 +45,7 @@ * This demo demonstrates how to make a one-to-one video call */ @Example( - index = 41, + index = 42, group = ADVANCED, name = R.string.item_transparentrendering, actionId = R.id.action_mainFragment_to_transparentrendering, 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 25bb10b35..fa2ae42e7 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/UrlLiveStream.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/UrlLiveStream.java @@ -32,7 +32,7 @@ * This demo demonstrates how to make a live stream with url */ @Example( - index = 42, + index = 43, group = ADVANCED, name = R.string.ultra_live_streaming_with_url, actionId = R.id.action_mainFragment_to_url_live_stream, 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 4072ca52e..85110067e 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoProcessExtension.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoProcessExtension.java @@ -57,7 +57,7 @@ * This demo demonstrates how to make a VideoProcessExtension */ @Example( - index = 32, + index = 26, group = ADVANCED, name = R.string.item_videoProcessExtension, actionId = R.id.action_mainFragment_video_enhancement, 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 4fd89bac9..5757cb52e 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java @@ -56,7 +56,7 @@ * @author cjw */ @Example( - index = 24, + index = 28, group = ADVANCED, name = R.string.item_quickswitck, actionId = R.id.action_mainFragment_to_QuickSwitch, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java index 45b7859c8..7a800b095 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayer.java @@ -21,7 +21,7 @@ * The type Audio router player. */ @Example( - index = 6, + index = 19, group = ADVANCED, name = R.string.item_audiorouter_player, actionId = R.id.action_mainFragment_to_AudioRouterPlayer, diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java index 0d5edbc47..e289e8d1d 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioWaveform.java @@ -32,7 +32,7 @@ * The type Audio waveform. */ @Example( - index = 5, + index = 18, group = ADVANCED, name = R.string.item_audiowaveform, actionId = R.id.action_mainFragment_to_AudioWaveform, diff --git a/Android/APIExample/app/src/main/res/values/strings.xml b/Android/APIExample/app/src/main/res/values/strings.xml index 975e9346c..6d2007813 100644 --- a/Android/APIExample/app/src/main/res/values/strings.xml +++ b/Android/APIExample/app/src/main/res/values/strings.xml @@ -85,53 +85,53 @@ Enhanced Low Latency Preload - Live Interactive Audio Streaming - Live Interactive Video Streaming - Live Interactive Video Streaming(Token Verify) + Join a channel (Audio) + Join a channel (Video) + Join a channel (Token) Quick Switch Channel Adjust the Volume - Join Multiple Channel - Pre-call Tests - Relay Streams across Channels + Join Multiple Channels + Pre-call Test + Media Channel Relay Set the Audio Profile Play Audio Files - Set the Voice Beautifier and Effects + Voice Effects - MediaPlayer - AudioRouter(Third Party Player) + Media Player + Audio Router (Third-party Player) Audio Waveform Regional Connection Push Streams to CDN - Streaming from RTC to CDN + RTMP Streaming Inject Online Media Stream Custom Video Source Multi Video Source Tracks Switch Video Sources Multiple Video Sources - Screen Sharing - Custom Audio Sources + Screen Share + Custom Audio Source (PCM) Custom Audio Render - Custom Video Renderer + Custom Video Render Raw Video Data Media Metadata Media Stream Encryption(Custom Encoder) - Media Stream Encryption + Stream Encryption Set the Video Profile Super Resolution - RTC Live Streaming + Live Streaming Multi Process - Screen Sharing Raw Audio Data - Simple Extension + Simple Filter Extension Send Data Stream ARCore Spatial Audio Content Inspect Picture In Picture - Third-party beauty - Agora beauty 2.0 + Third-party Beauty + Beauty 2.0 KTV Copyright Music - Local/Remote MediaRecorder - LocalVideoTranscoding + Media Recorder + Local Video Transcoding Simulcast Multipath @@ -243,7 +243,7 @@ Recording Countdown Playing Countdown Recording on Microphone - Video Enhancement + Video Process Rhythm Player Beats per Measure Beats per Minute @@ -431,8 +431,8 @@ Urltra Wide Telephoto Enter picture in picture mode - TransparentRendering - Ultra Live Streaming with Url + Transparent Rendering + URL Streaming (RTE Player) local fps, range:1-60 remote fps, range:1-60 scenario diff --git a/docs/ai/README.md b/docs/ai/README.md index dcdef365e..126588e39 100644 --- a/docs/ai/README.md +++ b/docs/ai/README.md @@ -39,6 +39,12 @@ On Windows, create the environment with `py -3 -m venv .venv` and use Reuse it for later checks; do not install the validator into the system Python environment. The same checks run in `.github/workflows/repository-policy.yml` for pushes and pull requests. +That workflow also compiles and executes selected Android/iOS template lifecycle regressions. +After editing creation templates, run the relevant native checks documented in +[CI template checks](../../.github/ci/README.md#template-checks). The post-release compile +workflow also builds the complete Android Audio/Compose and four iOS templates in their real +application targets. Structural validation alone does not establish template API correctness +or safe asynchronous cleanup. The requirement contract owns shared behavior. Platform implementations may differ only when the SDK, UI framework, permission model, or operating system requires it, and the diff --git a/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md index 6a764e7af..599fe635f 100644 --- a/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md @@ -13,6 +13,19 @@ metadata: # review-case — APIExample-Audio +## Pending Permission and Token Requests + +Review the complete lifecycle against [the creation template](../upsert-case/SKILL.md). +All request/engine ownership transitions must run on main. Freeze channel/UID and request +generation before requesting permission or Token; before joining, check that the generation +and engine identity are still current. Leave invalidates pending work even before joined; +destroy also clears the engine. Repeated cleanup must not destroy another case's engine. +Weak references alone do not invalidate a request when its owner remains alive. + +Exercise delayed permission and Token responses after leave/destroy, repeated cleanup, +rapid reopen and out-of-order responses. Only the current request may join. Also check +permission denial, absent required Token and a nonzero join result. Never log credentials. + ## Review Dimensions (in priority order) ### 1. Audio-Only Constraint (highest priority for this project) @@ -34,16 +47,9 @@ Any video API call in this project is a critical error — the SDK will crash or - `leaveChannel()` + `AgoraRtcEngineKit.destroy()` called in `willMove(toParent:)` when `parent == nil` - No engine instance stored beyond the Main VC's lifetime -**Correct:** -```swift -override func willMove(toParent parent: UIViewController?) { - super.willMove(toParent: parent) - if parent == nil { - agoraKit?.leaveChannel() - AgoraRtcEngineKit.destroy() - } -} -``` +**Correct:** Call the creation template’s idempotent `onDestroy()` from +`willMove(toParent:)` when `parent == nil`. It invalidates pending permission/Token work +before leaving, destroying and clearing the owned engine. --- diff --git a/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md index 08c6ea5f8..267aa9a79 100644 --- a/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -68,41 +68,98 @@ class Entry: UIViewController { } class Main: BaseViewController { - var agoraKit: AgoraRtcEngineKit? + private(set) var agoraKit: AgoraRtcEngineKit? + private var tokenRequestID = 0 override func viewDidLoad() { super.viewDidLoad() - guard let channelName = configs["channelName"] as? String else { return } + setupRTC() + } + + func setupRTC() { + precondition(Thread.isMainThread) + guard agoraKit == nil else { return } let config = AgoraRtcEngineConfig() config.appId = KeyCenter.AppId agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) agoraKit?.setAudioProfile(.default) - // request microphone permission, then join - NetworkManager.shared.generateToken(channelName: channelName) { [weak self] token in - let option = AgoraRtcChannelMediaOptions() - option.publishMicrophoneTrack = true - self?.agoraKit?.joinChannel(byToken: token, channelId: channelName, - uid: 0, mediaOptions: option) + // Configure this case's media, then call requestJoin(channelName:requestPermission:). + // Supply the project's permission flow; its completion must report granted/denied. + } + + // Main-queue entry point. A new request supersedes any pending permission/Token response. + func requestJoin(channelName: String, + requestPermission: (@escaping (Bool) -> Void) -> Void) { + precondition(Thread.isMainThread) + guard let engine = agoraKit, !channelName.isEmpty else { return } + tokenRequestID += 1 + let requestID = tokenRequestID + let uid: UInt = 0 + requestPermission { [weak self, weak engine] granted in + DispatchQueue.main.async { [weak self, weak engine] in + guard let self = self, let engine = engine, + self.tokenRequestID == requestID, self.agoraKit === engine else { return } + guard granted else { + LogUtils.log(message: "Permission denied", level: .error) + return + } + NetworkManager.shared.generateToken(channelName: channelName, uid: uid) { [weak self, weak engine] token in + DispatchQueue.main.async { [weak self, weak engine] in + guard let self = self, let engine = engine, + self.tokenRequestID == requestID, self.agoraKit === engine else { return } + if !(KeyCenter.Certificate ?? "").isEmpty && (token ?? "").isEmpty { + LogUtils.log(message: "Token request failed", level: .error) + return + } + let option = AgoraRtcChannelMediaOptions() + option.clientRoleType = .broadcaster + option.publishMicrophoneTrack = true + let result = engine.joinChannel(byToken: token, channelId: channelName, + uid: uid, mediaOptions: option) + if result != 0 { + LogUtils.log(message: "joinChannel failed: \(result)", level: .error) + } + } + } + } } } + func leaveChannel() { + precondition(Thread.isMainThread) + tokenRequestID += 1 + agoraKit?.leaveChannel(nil) + } + + func onDestroy() { + precondition(Thread.isMainThread) + leaveChannel() // Invalidate callbacks even while Token/permission/join is pending. + guard agoraKit != nil else { return } + // Stop case-owned capture, players, timers and observers here. + AgoraRtcEngineKit.destroy() + agoraKit = nil + } + override func willMove(toParent parent: UIViewController?) { super.willMove(toParent: parent) - if parent == nil { - agoraKit?.leaveChannel() - AgoraRtcEngineKit.destroy() - } + if parent == nil { onDestroy() } } } extension Main: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info) + } } func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error) + } } } ``` @@ -146,6 +203,12 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: --- +The lifecycle code is a skeleton: wire `requestJoin(channelName:requestPermission:)` from +setup or the Join action, passing the channel from `configs` and the case's actual permission +request. Never replace the permission closure with an unconditional grant in a real case. +Keep setup, join, leave and destroy on the main queue. A user Leave action must call +`leaveChannel()` so pending requests are invalidated, even before the SDK reports joined. + ## Verification Checklist - [ ] Folder created under correct category (Basic / Advanced) @@ -160,6 +223,9 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - [ ] UI updates inside delegate callbacks dispatched to `DispatchQueue.main` - [ ] Microphone permission requested before `joinChannel()` - [ ] Case Index row added/updated in `ARCHITECTURE.md` +- [ ] Permission/Token pending → leave/destroy → delayed callback does not join +- [ ] Repeat cleanup, reopen and out-of-order Token responses preserve only the active request +- [ ] Missing required Token and nonzero join return produce a failure state without logging credentials - [ ] Project builds without errors --- diff --git a/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift b/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift index d8617becb..84375eb77 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift @@ -28,14 +28,14 @@ class ViewController: AGViewController { MenuItem(name: "Join a channel (Token)".localized, storyboard: "JoinChannelAudioToken", controller: ""), MenuItem(name: "Join a channel (Audio)".localized, storyboard: "JoinChannelAudio", controller: "") ]), - MenuSection(name: "Anvanced", rows: [ - MenuItem(name: "Voice Changer".localized, storyboard: "VoiceChanger", controller: ""), + MenuSection(name: "Advanced", rows: [ + MenuItem(name: "Voice Effects".localized, storyboard: "VoiceChanger", controller: ""), MenuItem(name: "Custom Audio Source".localized, storyboard: "CustomAudioSource", controller: ""), - MenuItem(name: "Custom Audio Source(PCM)".localized, storyboard: "CustomPcmAudioSource", controller: "CustomPcmAudioSource"), + MenuItem(name: "Custom Audio Source (PCM)".localized, storyboard: "CustomPcmAudioSource", controller: "CustomPcmAudioSource"), MenuItem(name: "Custom Audio Render".localized, storyboard: "CustomAudioRender", controller: "CustomAudioRender"), MenuItem(name: "Raw Audio Data".localized, storyboard: "RawAudioData", controller: ""), MenuItem(name: "Audio Mixing".localized, storyboard: "AudioMixing", controller: ""), - MenuItem(name: "Precall Test".localized, storyboard: "PrecallTest", controller: ""), + MenuItem(name: "Pre-call Test".localized, storyboard: "PrecallTest", controller: ""), MenuItem(name: "Spatial Audio".localized, storyboard: "SpatialAudio", controller: "SpatialAudio"), ]), ] diff --git a/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings index 45f1991e2..3d407bc50 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings @@ -10,12 +10,11 @@ "Join a channel (Audio)" = "实时语音通话/直播"; "Live Streaming" = "RTC实时直播/主播/观众"; "RTMP Streaming" = "RTMP旁路推流"; -"Video Metadata" = "SEI消息"; -"Voice Changer" = "美声/音效"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; "Custom Audio Source" = "音频自采集"; -"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; "Custom Audio Render" = "音频自渲染"; -"Custom Video Source(Push)" = "视频自采集(Push)"; "Custom Video Render" = "视频自渲染(Metal)"; "Quick Switch Channel" = "快速切换频道"; "Join Multiple Channels" = "加入多频道"; @@ -23,7 +22,7 @@ "Audio Mixing" = "音频文件混音"; "Raw Video Data" = "视频裸数据"; "Simple Filter Extension" = "插件系统"; -"Precall Test" = "通话前网络/设备测试"; +"Pre-call Test" = "通话前网络/设备测试"; "Media Player" = "流媒体播放器"; "Screen Share" = "屏幕共享"; "Media Channel Relay" = "跨频道流转发"; @@ -108,7 +107,6 @@ "Broadcaster" = "主播"; "Audience" = "观众"; "Pick Role" = "选择角色"; -"Create Data Stream" = "创建数据流"; "Raw Audio Data" = "音频裸数据"; "Screen Share + Camera" = "屏幕共享 + 摄像头"; "please input channel name!" = "请输入频道名!"; diff --git a/iOS/APIExample-Audio/ARCHITECTURE.md b/iOS/APIExample-Audio/ARCHITECTURE.md index 65143682a..4a46642ae 100644 --- a/iOS/APIExample-Audio/ARCHITECTURE.md +++ b/iOS/APIExample-Audio/ARCHITECTURE.md @@ -50,14 +50,14 @@ APIExample-Audio/ │ │ ├── JoinChannelAudio/ # "Join a channel (Audio)" │ │ └── JoinChannelAudio(Token)/ # "Join a channel (Token)" │ └── Advanced/ - │ ├── VoiceChanger/ # "Voice Changer" — voice beautifier/effects + │ ├── VoiceChanger/ # "Voice Effects" — voice beautifier/effects │ ├── CustomAudioSource/ # "Custom Audio Source" │ ├── CustomPcmAudioSource/ # "Custom Audio Source (PCM)" │ ├── CustomAudioRender/ # "Custom Audio Render" │ ├── RawAudioData/ # "Raw Audio Data" │ ├── AudioMixing/ # "Audio Mixing" │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 - │ ├── PrecallTest/ # "Precall Test" + │ ├── PrecallTest/ # "Pre-call Test" │ └── SpatialAudio/ # "Spatial Audio" │ ├── Resources/ # Audio sample files @@ -130,8 +130,11 @@ the upsert and review skills. ## Token Flow -```swift -NetworkManager.shared.generateToken(channelName: channelId, uid: uid) { token in - self.agoraKit?.joinChannel(byToken: token, channelId: channelId, uid: uid, mediaOptions: options) -} -``` +Use the guarded permission → Token → join implementation in +[upsert-case](.agents/skills/upsert-case/SKILL.md). Snapshot channel, UID, request generation +and engine identity before asynchronous work. Recheck them on main before each continuation; +leave/destroy invalidates pending requests and destroy clears engine ownership. Weak capture +alone does not protect a still-alive controller whose RTC session has ended. + +A nil/empty Token is allowed only when no App Certificate is configured. Reject a missing +required Token and check the SDK join return code without logging credentials. diff --git a/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md index 8972e79e4..e1b25b3f3 100644 --- a/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md @@ -12,6 +12,19 @@ metadata: # review-case — APIExample-OC +## Pending Permission and Token Requests + +Review the complete lifecycle against [the creation template](../upsert-case/SKILL.md). +All request/engine ownership transitions must run on main. Freeze channel/UID and request +generation before requesting permission or Token; before joining, check that the generation +and engine identity are still current. Leave invalidates pending work even before joined; +destroy also clears the engine. Repeated cleanup must not destroy another case's engine. +Weak references alone do not invalidate a request when its owner remains alive. + +Exercise delayed permission and Token responses after leave/destroy, repeated cleanup, +rapid reopen and out-of-order responses. Only the current request may join. Also check +permission denial, absent required Token and a nonzero join result. Never log credentials. + ## Review Dimensions (in priority order) ### 1. Engine Lifecycle @@ -19,18 +32,10 @@ metadata: **Check:** - `[AgoraRtcEngineKit sharedEngineWithConfig:delegate:]` called in `viewDidLoad` (not in Entry VC) - `[self.agoraKit leaveChannel:]` + `[AgoraRtcEngineKit destroy]` called when leaving -- Cleanup triggered by `isMovingFromParentViewController` in `viewDidDisappear:`, or in `dealloc` +- Navigation cleanup invalidates requests in `willMoveToParentViewController:` when `parent == nil`; modal presentation needs its own explicit dismissal cleanup -**Correct:** -```objc -- (void)viewDidDisappear:(BOOL)animated { - [super viewDidDisappear:animated]; - if (self.isMovingFromParentViewController) { - [self.agoraKit leaveChannel:nil]; - [AgoraRtcEngineKit destroy]; - } -} -``` +**Correct:** Use `willMoveToParentViewController:` with `parent == nil` and the +idempotent `onDestroy` in the creation template. Temporary disappearance is not teardown. **Wrong:** ```objc @@ -75,13 +80,10 @@ All `AgoraRtcEngineDelegate` callbacks may arrive on a background thread. - Delegate property on `AgoraRtcEngineKit` is `weak` (it is by SDK design, but verify no strong cycle) - No `__unsafe_unretained` used for delegate or view references -**Correct:** -```objc -__weak typeof(self) weakSelf = self; -[[NetworkManager shared] generateTokenWithChannelName:channelName success:^(NSString *token) { - [weakSelf.agoraKit joinChannelByToken:token ...]; -}]; -``` +Import `APIExample_OC-swift.h` for `NetworkManager` and call +`generateTokenWithChannelName:uid:success:` with the snapshotted UID. Swift's `uid = 0` +does not generate an Objective-C selector without `uid:`. Use the complete guarded block +in the creation template; a weak reference without session validation is insufficient. --- @@ -163,5 +165,5 @@ Severity levels: - Verify `NS_ASSUME_NONNULL_BEGIN/END` wraps the header to reduce nullability warnings - Verify `IBOutlet` properties are `weak` (Xcode default, but worth confirming) -- `isMovingFromParentViewController` is the correct guard in `viewDidDisappear:` for navigation-based cleanup — do NOT use `isBeingDismissed` (that's for modal presentation) +- For navigation removal, use `willMoveToParentViewController:` with `parent == nil`; distinguish it from temporary disappearance and modal dismissal - ARC is enabled — no manual `retain`/`release` calls should appear diff --git a/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md index 64c2929c5..62d483593 100644 --- a/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md @@ -69,7 +69,7 @@ Create `.m`: #import ".h" #import #import "KeyCenter.h" -#import "NetworkManager.h" +#import "APIExample_OC-swift.h" @interface Entry () @property (weak, nonatomic) IBOutlet UITextField *channelTextField; @@ -87,40 +87,101 @@ Create `.m`: @end @interface Main () -@property (nonatomic, strong) AgoraRtcEngineKit *agoraKit; +@property (nonatomic, strong, nullable) AgoraRtcEngineKit *agoraKit; +@property (nonatomic) NSUInteger tokenRequestID; @end @implementation Main - (void)viewDidLoad { [super viewDidLoad]; - NSString *channelName = self.configs[@"channelName"]; + [self setupRTC]; +} + +- (void)setupRTC { + NSAssert([NSThread isMainThread], @"RTC lifecycle must run on main"); + if (self.agoraKit != nil) return; AgoraRtcEngineConfig *config = [AgoraRtcEngineConfig new]; config.appId = [KeyCenter AppId]; self.agoraKit = [AgoraRtcEngineKit sharedEngineWithConfig:config delegate:self]; - // configure engine, request permissions, then join - [[NetworkManager shared] generateTokenWithChannelName:channelName success:^(NSString *token) { - AgoraRtcChannelMediaOptions *option = [AgoraRtcChannelMediaOptions new]; - [self.agoraKit joinChannelByToken:token channelId:channelName - uid:0 mediaOptions:option joinSuccess:nil]; - }]; + // Configure media, then call requestJoin:requestPermission: with the actual permission flow. } -- (void)viewDidDisappear:(BOOL)animated { - [super viewDidDisappear:animated]; - if (self.isMovingFromParentViewController) { - [self.agoraKit leaveChannel:nil]; - [AgoraRtcEngineKit destroy]; - } +- (void)requestJoin:(NSString *)channelName + requestPermission:(void (^)(void (^)(BOOL)))requestPermission { + NSAssert([NSThread isMainThread], @"RTC lifecycle must run on main"); + AgoraRtcEngineKit *engine = self.agoraKit; + if (engine == nil || channelName.length == 0) return; + NSString *channel = [channelName copy]; + NSUInteger uid = 0; + NSUInteger requestID = ++self.tokenRequestID; + __weak typeof(self) weakSelf = self; + __weak AgoraRtcEngineKit *weakEngine = engine; + requestPermission(^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + typeof(self) self = weakSelf; + AgoraRtcEngineKit *engine = weakEngine; + if (!self || !engine || self.tokenRequestID != requestID || self.agoraKit != engine) return; + if (!granted) { + NSLog(@"Permission denied"); + return; + } + [[NetworkManager shared] generateTokenWithChannelName:channel uid:uid success:^(NSString * _Nullable token) { + dispatch_async(dispatch_get_main_queue(), ^{ + typeof(self) self = weakSelf; + AgoraRtcEngineKit *engine = weakEngine; + if (!self || !engine || self.tokenRequestID != requestID || self.agoraKit != engine) return; + if ([KeyCenter Certificate].length > 0 && token.length == 0) { + NSLog(@"Token request failed"); + return; + } + AgoraRtcChannelMediaOptions *option = [AgoraRtcChannelMediaOptions new]; + option.clientRoleType = AgoraClientRoleBroadcaster; + option.publishMicrophoneTrack = YES; + // Configure video publication/canvases only after camera permission. + int result = [engine joinChannelByToken:token channelId:channel + uid:uid mediaOptions:option joinSuccess:nil]; + if (result != 0) NSLog(@"joinChannel failed: %d", result); + }); + }]; + }); + }); +} + +- (void)leaveChannel { + NSAssert([NSThread isMainThread], @"RTC lifecycle must run on main"); + self.tokenRequestID += 1; + [self.agoraKit leaveChannel:nil]; +} + +- (void)onDestroy { + [self leaveChannel]; // Invalidate even while permission/Token/join is pending. + if (self.agoraKit == nil) return; + // Stop case-owned capture, players, timers and observers here. + [AgoraRtcEngineKit destroy]; + self.agoraKit = nil; +} + +- (void)willMoveToParentViewController:(UIViewController *)parent { + [super willMoveToParentViewController:parent]; + if (parent == nil) [self onDestroy]; } @end @implementation Main (AgoraRtcEngineDelegate) - (void)rtcEngine:(AgoraRtcEngineKit *)engine didJoinChannel:(NSString *)channel withUid:(NSUInteger)uid elapsed:(NSInteger)elapsed { - NSLog(@"Joined: %@ uid: %lu", channel, (unsigned long)uid); + __weak typeof(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.agoraKit != engine) return; + NSLog(@"Joined: %@ uid: %lu", channel, (unsigned long)uid); + }); } - (void)rtcEngine:(AgoraRtcEngineKit *)engine didOccurError:(AgoraErrorCode)errorCode { - NSLog(@"Error: %ld", (long)errorCode); + __weak typeof(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.agoraKit != engine) return; + NSLog(@"Error: %ld", (long)errorCode); + }); } @end ``` @@ -163,6 +224,13 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: --- +This is a skeleton: call `requestJoin:requestPermission:` from setup or the Join action, +passing the channel from `configs` and the case's actual permission request. The permission +completion reports granted/denied; never use an unconditional grant in a real case. Keep +setup, join, leave and destroy on main. A user Leave action must call `leaveChannel`. +`NetworkManager` is Swift: import the generated `APIExample_OC-swift.h`; Swift default +arguments do not remove `uid:` from the Objective-C selector. + ## Verification Checklist - [ ] Folder created under correct category (Basic / Advanced) @@ -177,6 +245,9 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - [ ] `__weak typeof(self) weakSelf = self` used in blocks that capture `self` - [ ] Camera/microphone permissions requested before `joinChannelByToken:` - [ ] Case Index row added/updated in `ARCHITECTURE.md` +- [ ] Permission/Token pending → leave/destroy → delayed callback does not join +- [ ] Repeat cleanup, reopen and out-of-order responses preserve only the current request +- [ ] Required Token failures and nonzero join returns are handled without logging credentials - [ ] Project builds without errors --- diff --git a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/CreateDataStream/CreateDataStream.storyboard b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/CreateDataStream/CreateDataStream.storyboard index 9e4d0ea2e..89f8828bc 100644 --- a/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/CreateDataStream/CreateDataStream.storyboard +++ b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/CreateDataStream/CreateDataStream.storyboard @@ -9,7 +9,7 @@ - + @@ -60,10 +60,10 @@ - + - + diff --git a/iOS/APIExample-OC/APIExample-OC/ViewController.m b/iOS/APIExample-OC/APIExample-OC/ViewController.m index d94a81720..143ca3e3c 100644 --- a/iOS/APIExample-OC/APIExample-OC/ViewController.m +++ b/iOS/APIExample-OC/APIExample-OC/ViewController.m @@ -52,33 +52,33 @@ + (NSArray *)menus { [[MenuItem alloc]initWithName:@"Local or remote recording".localized storyboard:@"JoinChannelVideoRecorder" controller:@""] ]]; [array addObject:basicSection]; - MenuSection *anvancedSection = [[MenuSection alloc] initWithName:@"Anvanced" rows:@[ + MenuSection *anvancedSection = [[MenuSection alloc] initWithName:@"Advanced" rows:@[ [[MenuItem alloc]initWithName:@"Live Streaming".localized storyboard:@"LiveStreaming" controller:@"LiveStreaming"], [[MenuItem alloc]initWithName:@"RTMP Streaming".localized storyboard:@"RTMPStreaming" controller:@""], - [[MenuItem alloc]initWithName:@"Video Metadata".localized storyboard:@"VideoMetadata" controller:@""], - [[MenuItem alloc]initWithName:@"Voice Changer".localized storyboard:@"VoiceChanger" controller:@""], - [[MenuItem alloc]initWithName:@"Custom Audio Source".localized storyboard:@"CustomPcmAudioSource" controller:@""], + [[MenuItem alloc]initWithName:@"Media Metadata".localized storyboard:@"VideoMetadata" controller:@""], + [[MenuItem alloc]initWithName:@"Voice Effects".localized storyboard:@"VoiceChanger" controller:@""], + [[MenuItem alloc]initWithName:@"Custom Audio Source (PCM)".localized storyboard:@"CustomPcmAudioSource" controller:@""], [[MenuItem alloc]initWithName:@"Custom Audio Render".localized storyboard:@"CustomAudioRender" controller:@""], - [[MenuItem alloc]initWithName:@"Custom Video Source(Push)".localized storyboard:@"CustomVideoSourcePush" controller:@""], - [[MenuItem alloc]initWithName:@"Custom Video Render".localized storyboard:@"CustomVideoRender" controller:@""], [[MenuItem alloc]initWithName:@"Raw Audio Data".localized storyboard:@"RawAudioData" controller:@""], + [[MenuItem alloc]initWithName:@"Audio Mixing".localized storyboard:@"AudioMixing" controller:@""], + [[MenuItem alloc]initWithName:@"Custom Video Source".localized storyboard:@"CustomVideoSourcePush" controller:@""], + [[MenuItem alloc]initWithName:@"Custom Video Render".localized storyboard:@"CustomVideoRender" controller:@""], [[MenuItem alloc]initWithName:@"Raw Video Data".localized storyboard:@"RawVideoData" controller:@""], - [[MenuItem alloc]initWithName:@"Picture In Picture(require iOS15)".localized storyboard:@"PictureInPicture" controller:@""], + [[MenuItem alloc]initWithName:@"Picture In Picture".localized storyboard:@"PictureInPicture" controller:@""], [[MenuItem alloc]initWithName:@"Simple Filter Extension".localized storyboard:@"SimpleFilter" controller:@""], + [[MenuItem alloc]initWithName:@"Video Process".localized storyboard:@"VideoProcess" controller:@""], + [[MenuItem alloc]initWithName:@"Local Composite Graph".localized storyboard:@"LocalCompositeGraph" controller:@""], [[MenuItem alloc]initWithName:@"Join Multiple Channels".localized storyboard:@"JoinMultiChannel" controller:@""], [[MenuItem alloc]initWithName:@"Stream Encryption".localized storyboard:@"StreamEncryption" controller:@""], - [[MenuItem alloc]initWithName:@"Audio Mixing".localized storyboard:@"AudioMixing" controller:@""], [[MenuItem alloc]initWithName:@"Media Player".localized storyboard:@"MediaPlayer" controller:@""], [[MenuItem alloc]initWithName:@"Screen Share".localized storyboard:@"ScreenShare" controller:@""], - [[MenuItem alloc]initWithName:@"Video Process".localized storyboard:@"VideoProcess" controller:@""], - [[MenuItem alloc]initWithName:@"Create Data Stream".localized storyboard:@"CreateDataStream" controller:@""], + [[MenuItem alloc]initWithName:@"Send Data Stream".localized storyboard:@"CreateDataStream" controller:@""], [[MenuItem alloc]initWithName:@"Media Channel Relay".localized storyboard:@"MediaChannelRelay" controller:@""], [[MenuItem alloc]initWithName:@"Spatial Audio".localized storyboard:@"SpatialAudio" controller:@""], [[MenuItem alloc]initWithName:@"Content Inspect".localized storyboard:@"ContentInspect" controller:@""], - [[MenuItem alloc]initWithName:@"Mutli Camera(iOS13.0+)".localized storyboard:@"MutliCamera" controller:@""], + [[MenuItem alloc]initWithName:@"Multi Camera".localized storyboard:@"MutliCamera" controller:@""], [[MenuItem alloc]initWithName:@"Simulcast".localized storyboard:@"Simulcast" controller:@""], [[MenuItem alloc]initWithName:@"Multipath".localized storyboard:@"Multipath" controller:@""], - [[MenuItem alloc]initWithName:@"LocalCompositeGraph".localized storyboard:@"LocalCompositeGraph" controller:@""], ]]; [array addObject:anvancedSection]; diff --git a/iOS/APIExample-OC/ARCHITECTURE.md b/iOS/APIExample-OC/ARCHITECTURE.md index 9c0697014..90265818e 100644 --- a/iOS/APIExample-OC/ARCHITECTURE.md +++ b/iOS/APIExample-OC/ARCHITECTURE.md @@ -72,15 +72,15 @@ APIExample-OC/ │ └── Advanced/ │ ├── LiveStreaming/ # "Live Streaming" │ ├── RTMPStreaming/ # "RTMP Streaming" - │ ├── VideoMetadata/ # "Video Metadata" - │ ├── VoiceChanger/ # "Voice Changer" - │ ├── CustomPcmAudioSource/ # "Custom Audio Source" + │ ├── VideoMetadata/ # "Media Metadata" + │ ├── VoiceChanger/ # "Voice Effects" + │ ├── CustomPcmAudioSource/ # "Custom Audio Source (PCM)" │ ├── CustomAudioRender/ # "Custom Audio Render" - │ ├── CustomVideoSourcePush/ # "Custom Video Source (Push)" + │ ├── CustomVideoSourcePush/ # "Custom Video Source" │ ├── CustomVideoRender/ # "Custom Video Render" │ ├── RawAudioData/ # "Raw Audio Data" │ ├── RawVideoData/ # "Raw Video Data" - │ ├── PictureInPicture/ # "Picture In Picture (iOS15+)" + │ ├── PictureInPicture/ # "Picture In Picture" │ ├── SimpleFilter/ # "Simple Filter Extension" │ ├── JoinMultiChannel/ # "Join Multiple Channels" │ ├── StreamEncryption/ # "Stream Encryption" @@ -89,11 +89,11 @@ APIExample-OC/ │ ├── ScreenShare/ # "Screen Share" │ ├── VideoProcess/ # "Video Process" │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 - │ ├── CreateDataStream/ # "Create Data Stream" + │ ├── CreateDataStream/ # "Send Data Stream" │ ├── MediaChannelRelay/ # "Media Channel Relay" │ ├── SpatialAudio/ # "Spatial Audio" │ ├── ContentInspect/ # "Content Inspect" - │ ├── MutliCamera/ # "Multi Camera (iOS13+)" + │ ├── MutliCamera/ # "Multi Camera" │ ├── Simulcast/ # "Simulcast" │ ├── Multipath/ # "Multipath" │ └── LocalCompositeGraph/ # "Local Composite Graph" @@ -149,19 +149,25 @@ viewDidLoad → [AgoraRtcEngineKit sharedEngineWithAppId:delegate:] ↓ [AgoraRtcEngineDelegate callbacks — may be on background thread] ↓ -viewDidDisappear: when isMovingFromParentViewController is true +willMoveToParentViewController: when parent == nil → [engine leaveChannel:] → [AgoraRtcEngineKit destroy] ``` -Navigation cleanup is guarded by `isMovingFromParentViewController`, as shown in the +Navigation cleanup uses `willMoveToParentViewController:` with `parent == nil`, as shown in the upsert and review skills. Merely covering the controller does not end the case; `dealloc` is not the primary scene-exit hook. ## Token Flow -```objc -[[NetworkManager shared] generateTokenWithChannelName:channelName success:^(NSString *token) { - [self.agoraKit joinChannelByToken:token channelId:channelName uid:0 mediaOptions:options]; -}]; -``` +Use the guarded permission → Token → join implementation in +[upsert-case](.agents/skills/upsert-case/SKILL.md). Snapshot channel, UID, request generation +and engine identity before asynchronous work. Recheck them on main before each continuation; +leave/destroy invalidates pending requests and destroy clears engine ownership. Weak capture +alone does not protect a still-alive controller whose RTC session has ended. + +A nil/empty Token is allowed only when no App Certificate is configured. Reject a missing +required Token and check the SDK join return code without logging credentials. + +Import `APIExample_OC-swift.h` for the Swift `NetworkManager`. Its Objective-C selector is +`generateTokenWithChannelName:uid:success:`; the Swift default UID is not an OC overload. diff --git a/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings index 1bae82890..3a20fbbaa 100644 --- a/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings @@ -12,23 +12,22 @@ "Local or remote recording" = "本地/远端录制"; "Live Streaming" = "RTC实时直播/主播/观众"; "RTMP Streaming" = "RTMP旁路推流"; -"Video Metadata" = "SEI消息"; -"Voice Changer" = "美声/音效"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; "Custom Audio Source" = "音频自采集"; -"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; "Custom Audio Render" = "音频自渲染"; "Custom Video Source" = "视频自采集"; -"Custom Video Source(Push)" = "视频自采集(Push)"; -"Custom Video Source(Multi)" = "多路视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; "Custom Video Render" = "视频自渲染(Metal)"; "Quick Switch Channel" = "快速切换频道"; "Join Multiple Channels" = "加入多频道"; "Stream Encryption" = "音视频流加密"; -"LocalCompositeGraph" = "本地合图屏幕共享"; +"Local Composite Graph" = "本地合图屏幕共享"; "Audio Mixing" = "音频文件混音"; "Raw Video Data" = "视频裸数据"; "Simple Filter Extension" = "插件系统"; -"Precall Test" = "通话前网络/设备测试"; +"Pre-call Test" = "通话前网络/设备测试"; "Media Player" = "流媒体播放器"; "Screen Share" = "屏幕共享"; "Media Channel Relay" = "跨频道流转发"; @@ -115,7 +114,7 @@ "Broadcaster" = "主播"; "Audience" = "观众"; "Pick Role" = "选择角色"; -"Create Data Stream" = "创建数据流"; +"Send Data Stream" = "发送数据流"; "Raw Audio Data" = "音频裸数据"; "Screen Share + Camera" = "屏幕共享 + 摄像头"; "please input channel name!" = "请输入频道名!"; @@ -130,12 +129,12 @@ "SwitchCamera" = "切换摄像头"; "remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; "Screenshot successful" = "截图成功"; -"Picture In Picture(require iOS15)" = "画中画(iOS15支持)"; +"Picture In Picture" = "画中画"; "please input Token!" = "请输入Token"; "please input AppId!" = "请输入APPID"; "stopMicrophoneRecording" = "停止麦克风采集"; -"Ktv copyright music" = "KTV版权音乐"; -"Mutli Camera(iOS13.0+)" = "多路摄像头(iOS13.0+)"; +"KTV Copyright Music" = "KTV版权音乐"; +"Multi Camera" = "多路摄像头"; "Multipath" = "网络传输高级功能"; "Simulcast" = "同源多流"; "Open Rear Camera" = "打开后摄像头"; diff --git a/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md index 5f4d3b929..d8ed3220b 100644 --- a/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md @@ -12,6 +12,19 @@ metadata: # review-case — APIExample-SwiftUI +## Pending Permission and Token Requests + +Review the complete lifecycle against [the creation template](../upsert-case/SKILL.md). +All request/engine ownership transitions must run on main. Freeze channel/UID and request +generation before requesting permission or Token; before joining, check that the generation +and engine identity are still current. Leave invalidates pending work even before joined; +destroy also clears the engine. Repeated cleanup must not destroy another case's engine. +Weak references alone do not invalidate a request when its owner remains alive. + +Exercise delayed permission and Token responses after leave/destroy, repeated cleanup, +rapid reopen and out-of-order responses. Only the current request may join. Also check +permission denial, absent required Token and a nonzero join result. Never log credentials. + ## Review Dimensions (in priority order) ### 1. Engine Lifecycle diff --git a/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md index 5a9e25028..e0e66caa0 100644 --- a/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md @@ -52,39 +52,88 @@ import AgoraRtcKit import SwiftUI class RTC: NSObject, ObservableObject { - var agoraKit: AgoraRtcEngineKit! - private var isJoined = false + private(set) var agoraKit: AgoraRtcEngineKit? + private var tokenRequestID = 0 func setupRTC(configs: [String: Any]) { + precondition(Thread.isMainThread) + guard agoraKit == nil else { return } let config = AgoraRtcEngineConfig() config.appId = KeyCenter.AppId agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + // Configure this case's media, then call requestJoin(channelName:requestPermission:). + // Supply the project's permission flow; its completion must report granted/denied. + } - guard let channelName = configs["channelName"] as? String else { return } - let option = AgoraRtcChannelMediaOptions() - option.clientRoleType = .broadcaster - - NetworkManager.shared.generateToken(channelName: channelName) { [weak self] token in - self?.agoraKit.joinChannel(byToken: token, channelId: channelName, - uid: 0, mediaOptions: option) + // Main-queue entry point. A new request supersedes any pending permission/Token response. + func requestJoin(channelName: String, + requestPermission: (@escaping (Bool) -> Void) -> Void) { + precondition(Thread.isMainThread) + guard let engine = agoraKit, !channelName.isEmpty else { return } + tokenRequestID += 1 + let requestID = tokenRequestID + let uid: UInt = 0 + requestPermission { [weak self, weak engine] granted in + DispatchQueue.main.async { [weak self, weak engine] in + guard let self = self, let engine = engine, + self.tokenRequestID == requestID, self.agoraKit === engine else { return } + guard granted else { + LogUtils.log(message: "Permission denied", level: .error) + return + } + NetworkManager.shared.generateToken(channelName: channelName, uid: uid) { [weak self, weak engine] token in + DispatchQueue.main.async { [weak self, weak engine] in + guard let self = self, let engine = engine, + self.tokenRequestID == requestID, self.agoraKit === engine else { return } + if !(KeyCenter.Certificate ?? "").isEmpty && (token ?? "").isEmpty { + LogUtils.log(message: "Token request failed", level: .error) + return + } + let option = AgoraRtcChannelMediaOptions() + option.clientRoleType = .broadcaster + option.publishMicrophoneTrack = true + // Configure camera publication/canvases for video cases after camera permission. + let result = engine.joinChannel(byToken: token, channelId: channelName, + uid: uid, mediaOptions: option) + if result != 0 { + LogUtils.log(message: "joinChannel failed: \(result)", level: .error) + } + } + } + } } } + func leaveChannel() { + precondition(Thread.isMainThread) + tokenRequestID += 1 + agoraKit?.leaveChannel(nil) + } + func onDestroy() { - if isJoined { agoraKit.leaveChannel(nil) } + precondition(Thread.isMainThread) + leaveChannel() // Invalidate callbacks even while Token/permission/join is pending. + guard agoraKit != nil else { return } + // Stop case-owned capture, players, timers and observers here. AgoraRtcEngineKit.destroy() + agoraKit = nil } } extension RTC: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - isJoined = true - LogUtils.log(message: "Joined: \(channel)", level: .info) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info) + } } func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "Error: \(errorCode)", level: .error) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error) + } } } ``` @@ -153,6 +202,12 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: --- +The lifecycle code is a skeleton: wire `requestJoin(channelName:requestPermission:)` from +setup or the Join action, passing the channel from `configs` and the case's actual permission +request. Never replace the permission closure with an unconditional grant in a real case. +Keep setup, join, leave and destroy on the main queue. A user Leave action must call +`leaveChannel()` so pending requests are invalidated, even before the SDK reports joined. + ## Verification Checklist - [ ] Folder created under correct category (Basic / Advanced) @@ -166,6 +221,9 @@ Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - [ ] New Swift files are in the `APIExample-SwiftUI` target's Sources build phase - [ ] New assets or localized files are in the target's Resources build phase, when applicable - [ ] Case Index row added/updated in `ARCHITECTURE.md` +- [ ] Permission/Token pending → leave/destroy → delayed callback does not join +- [ ] Repeat cleanup, reopen and out-of-order Token responses preserve only the active request +- [ ] Missing required Token and nonzero join return produce a failure state without logging credentials - [ ] Project builds without errors --- diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift index 82eea31a4..dacca3ad8 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift @@ -17,9 +17,11 @@ struct MenuItem: Identifiable { let id = UUID() var name: String var view: AnyView - init(name: String, view: AnyView) { + var requiresIOS15: Bool + init(name: String, view: AnyView, requiresIOS15: Bool = false) { self.name = name self.view = view + self.requiresIOS15 = requiresIOS15 } } @@ -35,44 +37,49 @@ struct ContentView: View { MenuItem(name: "Local or remote recording".localized, view: AnyView(JoinChannelVideoRecorderEntry())) ]), - MenuSection(name: "Anvanced", rows: [ + MenuSection(name: "Advanced", rows: [ MenuItem(name: "Live Streaming".localized, view: AnyView(LiveStreamingEntry())), - MenuItem(name: "Video Metadata".localized, + MenuItem(name: "RTMP Streaming".localized, + view: AnyView(RTMPStreamEntry())), + MenuItem(name: "Media Metadata".localized, view: AnyView(VideoMetadataEntry())), - MenuItem(name: "Custom Audio Source(PCM)".localized, + MenuItem(name: "Voice Effects".localized, + view: AnyView(VoiceChangerEntry())), + MenuItem(name: "Custom Audio Source (PCM)".localized, view: AnyView(CustomPCMAudioSourceEntry())), + MenuItem(name: "Custom Audio Render".localized, + view: AnyView(CustomAudioRenderEntry())), MenuItem(name: "Raw Audio Data".localized, view: AnyView(RawAudioDataEntry())), + MenuItem(name: "Audio Mixing".localized, + view: AnyView(AudioMixingEntry())), + MenuItem(name: "Audio Waveform".localized, + view: AnyView(AudioWaveformEntry())), MenuItem(name: "Raw Video Data".localized, view: AnyView(RawVideoDataEntry())), + MenuItem(name: "Picture In Picture".localized, + view: AnyView(PictureInPictureEntry()), + requiresIOS15: true), + MenuItem(name: "Video Process".localized, + view: AnyView(VideoProcessEntry())), MenuItem(name: "Local Video Transcoding".localized, view: AnyView(LocalVideoTranscodingEntry())), - MenuItem(name: "Voice Changer".localized, - view: AnyView(VoiceChangerEntry())), - MenuItem(name: "RTMP Streaming".localized, - view: AnyView(RTMPStreamEntry())), - MenuItem(name: "Custom Audio Render".localized, - view: AnyView(CustomAudioRenderEntry())), - MenuItem(name: "Picture In Picture".localized, - view: AnyView(PictureInPictureEntry())), + MenuItem(name: "Local Composite Graph".localized, + view: AnyView(LocalCompositeGraphEntry())), MenuItem(name: "Quick Switch Channel".localized, view: AnyView(QuickSwitchChannelEntry())), MenuItem(name: "Join Multiple Channels".localized, view: AnyView(JoinMultiChannelEntry())), MenuItem(name: "Stream Encryption".localized, view: AnyView(StreamEncryptionEntry())), - MenuItem(name: "Audio Mixing".localized, - view: AnyView(AudioMixingEntry())), - MenuItem(name: "Precall Test".localized, + MenuItem(name: "Pre-call Test".localized, view: AnyView(PrecallTest())), MenuItem(name: "Media Player".localized, view: AnyView(MediaPlayerEntry())), MenuItem(name: "Screen Share".localized, view: AnyView(ScreenShareEntry())), - MenuItem(name: "Video Process".localized, - view: AnyView(VideoProcessEntry())), - MenuItem(name: "Create Data Stream".localized, + MenuItem(name: "Send Data Stream".localized, view: AnyView(CreateDataStreamEntry())), MenuItem(name: "Media Channel Relay".localized, view: AnyView(MediaChannelRelayEntry())), @@ -80,19 +87,16 @@ struct ContentView: View { view: AnyView(SpatialAudioEntry())), MenuItem(name: "Content Inspect".localized, view: AnyView(ContentInspectEntry())), - MenuItem(name: "Mutli Camera(iOS13.0+)".localized, + MenuItem(name: "Multi Camera".localized, view: AnyView(MutliCameraEntry())), - MenuItem(name: "Ktv copyright music".localized, + MenuItem(name: "KTV Copyright Music".localized, view: AnyView(KtvCopyrightMusic())), MenuItem(name: "ARKit".localized, view: AnyView(ARKitEntry())), - MenuItem(name: "Audio Waveform".localized, - view: AnyView(AudioWaveformEntry())), MenuItem(name: "Simulcast".localized, view: AnyView(SimulcastEntry())), MenuItem(name: "Multipath".localized, - view: AnyView(MultipathEntry())), - MenuItem(name: "LocalCompositeGraph".localized, view: AnyView(LocalCompositeGraphEntry())) + view: AnyView(MultipathEntry())) ]) ] @@ -119,7 +123,7 @@ struct ContentView: View { struct MenuItemView: View { var item: MenuItem var body: some View { - if item.name == "Picture In Picture".localized { + if item.requiresIOS15 { if #available(iOS 15.0, *) { NavigationLink(destination: { item.view.navigationTitle(item.name) diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.swift index 14a2e82c4..ff0eeceaf 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Examples/Advanced/KtvCopyrightMusic/KtvCopyrightMusic.swift @@ -10,7 +10,7 @@ import SwiftUI struct KtvCopyrightMusic: View { var body: some View { ZStack { - Button("Ktv copyright music".localized) { + Button("KTV Copyright Music".localized) { let urlString = "https://docs.agora.io/en/interactive-live-streaming/overview/product-overview?platform=ios" if let url = URL(string: urlString) { diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings index 4762e0563..6ea3fe861 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings @@ -10,33 +10,32 @@ "anti shake" = "Anti-shake"; "Enter channel name" = "Enter channel name"; "Default background Color" = "Default background color"; -"Join a channel (Token)" = "Real-time Video Call/Live Streaming (Token)"; -"Join a channel (Video)" = "Real-time Video Call/Live Streaming"; -"Join a channel (Audio)" = "Real-time Voice Call/Live Streaming"; -"Local or remote recording" = "Local/Remote Recording"; -"Live Streaming" = "RTC Live Streaming/Host/Audience"; +"Join a channel (Token)" = "Join a channel (Token)"; +"Join a channel (Video)" = "Join a channel (Video)"; +"Join a channel (Audio)" = "Join a channel (Audio)"; +"Local or remote recording" = "Local or remote recording"; +"Live Streaming" = "Live Streaming"; "RTMP Streaming" = "RTMP Streaming"; "Transcoding" = "Transcoding"; -"Video Metadata" = "SEI Message"; -"Voice Changer" = "Voice Effects/Beautifier"; +"Media Metadata" = "Media Metadata"; +"Voice Effects" = "Voice Effects"; "Custom Audio Source" = "Custom Audio Source"; -"Custom Audio Source(PCM)" = "Custom Audio Source (PCM)"; +"Custom Audio Source (PCM)" = "Custom Audio Source (PCM)"; "Custom Audio Render" = "Custom Audio Render"; "Custom Video Source" = "Custom Video Source"; -"Custom Video Source(Push)" = "Custom Video Source (Push)"; -"Custom Video Source(Multi)" = "Multi Custom Video Source"; +"Multiple Video Sources" = "Multi Custom Video Source"; "Custom Video Render" = "Custom Video Render (Metal)"; -"Picture In Picture" = "Picture In Picture (iOS 15+)"; +"Picture In Picture" = "Picture In Picture"; "Quick Switch Channel" = "Quick Switch Channel"; "Join Multiple Channels" = "Join Multiple Channels"; -"Stream Encryption" = "Media Stream Encryption"; -"Audio Mixing" = "Audio File Mixing"; +"Stream Encryption" = "Stream Encryption"; +"Audio Mixing" = "Audio Mixing"; "Raw Video Data" = "Raw Video Data"; "Simple Filter Extension" = "Plugin System"; -"Precall Test" = "Pre-call Network/Device Test"; +"Pre-call Test" = "Pre-call Test"; "Media Player" = "Media Player"; "Screen Share" = "Screen Share"; -"Media Channel Relay" = "Cross Channel Stream Relay"; +"Media Channel Relay" = "Media Channel Relay"; "Set Resolution" = "Set Video Resolution"; "Set Fps" = "Set Video Frame Rate"; "B Fps" = "B Frame"; @@ -121,15 +120,15 @@ "Broadcaster" = "Broadcaster"; "Audience" = "Audience"; "Pick Role" = "Select Role"; -"Create Data Stream" = "Create Data Stream"; +"Send Data Stream" = "Send Data Stream"; "Raw Audio Data" = "Raw Audio Data"; "Screen Share + Camera" = "Screen Share + Camera"; "please input channel name!" = "Please enter channel name!"; -"Video Process" = "Video Enhancement"; +"Video Process" = "Video Process"; "Rhythm Player" = "Virtual Metronome"; "Spatial Audio" = "Spatial Audio"; "Please move the red icon to experience the 3D audio effect" = "Please move the red icon to experience 3D audio effect"; -"Content Inspect" = "Content Moderation"; +"Content Inspect" = "Content Inspect"; "Video Neutral" = "Video Normal"; "Video Sexy" = "Video Suggestive"; "Video Porn" = "Video Pornographic"; @@ -137,12 +136,11 @@ "screenshot" = "Screenshot"; "remote user has not joined, and cannot take a screenshot" = "Cannot take screenshot, remote user hasn't joined"; "Screenshot successful" = "Screenshot successful"; -"Picture In Picture(require iOS15)" = "Picture In Picture (iOS15+ required)"; "please input Token!" = "Please enter Token!"; "please input AppId!" = "Please enter AppID!"; "stopMicrophoneRecording" = "Stop Microphone Recording"; -"Ktv copyright music" = "KTV Copyright Music"; -"Mutli Camera(iOS13.0+)" = "Multi Camera (iOS13.0+)"; +"KTV Copyright Music" = "KTV Copyright Music"; +"Multi Camera" = "Multi Camera"; "Open Rear Camera" = "Open Rear Camera"; "Close Rear Camera" = "Close Rear Camera"; "license authorization failed, please check whether the license file is correct" = "License authorization failed, please check if license file is correct"; @@ -159,9 +157,9 @@ "Yellow" = "Yellow"; "preload Channel" = "Preload"; "cancel preload" = "Cancel Preload"; -"Audio Router(Third Party Player)" = "Audio Router (Third Party Player)"; +"Audio Router (Third-party Player)" = "Audio Router (Third Party Player)"; "Player Type(ijkplayer/avplayer)" = "Player Selection (ijkplayer/native)"; -"Audio Waveform" = "Audio Spectrum"; +"Audio Waveform" = "Audio Waveform"; "Set AINS Mode" = "Set AI Noise Suppression mode"; "AGGRESSIVE" = "Aggressive Mode"; "BALANCED" = "(Default) Balanced Mode"; @@ -257,7 +255,7 @@ "Voice Formant" = "Voice Formant"; "Set Layers Config(Maximum 3)" = "Set Layers Config(Maximum 3)"; "Maxmum 3 layers can be selected" = "Maxmum 3 layers can be selected"; -"LocalCompositeGraph" = "LocalCompositeGraph"; +"Local Composite Graph" = "Local Composite Graph"; "3D Voice" = "3D Voice"; "Neutral" = "Neutral"; "Sweet" = "Sweet"; diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings index 536e809b3..452b28357 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings @@ -17,23 +17,22 @@ "Live Streaming" = "RTC实时直播/主播/观众"; "RTMP Streaming" = "RTMP旁路推流"; "Transcoding" = "转码"; -"Video Metadata" = "SEI消息"; -"Voice Changer" = "美声/音效"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; "Custom Audio Source" = "音频自采集"; -"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; "Custom Audio Render" = "音频自渲染"; "Custom Video Source" = "视频自采集"; -"Custom Video Source(Push)" = "视频自采集(Push)"; -"Custom Video Source(Multi)" = "多路视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; "Custom Video Render" = "视频自渲染(Metal)"; -"Picture In Picture" = "画中画(iOS 15支持)"; +"Picture In Picture" = "画中画"; "Quick Switch Channel" = "快速切换频道"; "Join Multiple Channels" = "加入多频道"; "Stream Encryption" = "音视频流加密"; "Audio Mixing" = "音频文件混音"; "Raw Video Data" = "视频裸数据"; "Simple Filter Extension" = "插件系统"; -"Precall Test" = "通话前网络/设备测试"; +"Pre-call Test" = "通话前网络/设备测试"; "Media Player" = "流媒体播放器"; "Screen Share" = "屏幕共享"; "Media Channel Relay" = "跨频道流转发"; @@ -121,7 +120,7 @@ "Broadcaster" = "主播"; "Audience" = "观众"; "Pick Role" = "选择角色"; -"Create Data Stream" = "创建数据流"; +"Send Data Stream" = "发送数据流"; "Raw Audio Data" = "音频裸数据"; "Screen Share + Camera" = "屏幕共享 + 摄像头"; "please input channel name!" = "请输入频道名!"; @@ -137,12 +136,11 @@ "screenshot" = "截图"; "remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; "Screenshot successful" = "截图成功"; -"Picture In Picture(require iOS15)" = "画中画(iOS15支持)"; "please input Token!" = "请输入Token"; "please input AppId!" = "请输入APPID"; "stopMicrophoneRecording" = "停止麦克风采集"; -"Ktv copyright music" = "KTV版权音乐"; -"Mutli Camera(iOS13.0+)" = "多路摄像头(iOS13.0+)"; +"KTV Copyright Music" = "KTV版权音乐"; +"Multi Camera" = "多路摄像头"; "Open Rear Camera" = "打开后摄像头"; "Close Rear Camera" = "关闭后摄像头"; "license authorization failed, please check whether the license file is correct" = "license授权失败, 请检查license文件是否正确"; @@ -159,7 +157,7 @@ "Yellow" = "黄色"; "preload Channel" = "预加载"; "cancel preload" = "取消预加载"; -"Audio Router(Third Party Player)" = "音频路由(第三方播放器)"; +"Audio Router (Third-party Player)" = "音频路由(第三方播放器)"; "Player Type(ijkplayer/avplayer)" = "播放器选择(ijkplayer/原生)"; "Audio Waveform" = "音频频谱"; "Set AINS Mode" = "设置是否开启 AI 降噪功能并设置降噪模式。"; @@ -257,7 +255,7 @@ "Voice Formant" = "语音共振峰"; "Set Layers Config(Maximum 3)" = "配置多流(最多3个)"; "Maxmum 3 layers can be selected" = "最多配置三个"; -"LocalCompositeGraph" = "本地合图屏幕共享"; +"Local Composite Graph" = "本地合图屏幕共享"; "3D Voice" = "3D 人声"; "Neutral" = "中性"; "Sweet" = "甜美"; diff --git a/iOS/APIExample-SwiftUI/ARCHITECTURE.md b/iOS/APIExample-SwiftUI/ARCHITECTURE.md index f2a691560..2c426a947 100644 --- a/iOS/APIExample-SwiftUI/ARCHITECTURE.md +++ b/iOS/APIExample-SwiftUI/ARCHITECTURE.md @@ -78,8 +78,8 @@ APIExample-SwiftUI/ │ └── Advanced/ │ ├── LiveStreaming/ # "Live Streaming" │ ├── RTMPStream/ # "RTMP Streaming" - │ ├── VideoMetadata/ # "Video Metadata" - │ ├── VoiceChanger/ # "Voice Changer" + │ ├── VideoMetadata/ # "Media Metadata" + │ ├── VoiceChanger/ # "Voice Effects" │ ├── CustomPCMAudioSource/ # "Custom Audio Source (PCM)" │ ├── CustomAudioRender/ # "Custom Audio Render" │ ├── RawAudioData/ # "Raw Audio Data" @@ -89,18 +89,18 @@ APIExample-SwiftUI/ │ ├── JoinMultiChannel/ # "Join Multiple Channels" │ ├── StreamEncryption/ # "Stream Encryption" │ ├── AudioMixing/ # "Audio Mixing" - │ ├── PrecallTest/ # "Precall Test" + │ ├── PrecallTest/ # "Pre-call Test" │ ├── MediaPlayer/ # "Media Player" │ ├── ScreenShare/ # "Screen Share" │ ├── LocalVideoTranscoding/ # "Local Video Transcoding" │ ├── LocalVideoComposition/ # "Local Composite Graph" │ ├── VideoProcess/ # "Video Process" │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 - │ ├── CreateDataStream/ # "Create Data Stream" + │ ├── CreateDataStream/ # "Send Data Stream" │ ├── MediaChannelRelay/ # "Media Channel Relay" │ ├── SpatialAudio/ # "Spatial Audio" │ ├── ContentInspect/ # "Content Inspect" - │ ├── MutliCamera/ # "Multi Camera (iOS13+)" + │ ├── MutliCamera/ # "Multi Camera" │ ├── KtvCopyrightMusic/ # "KTV Copyright Music" │ ├── ARKit/ # "ARKit" │ ├── AudioWaveform/ # "Audio Waveform" @@ -178,8 +178,11 @@ UIKit video views (`VideoUIView`) are bridged into SwiftUI via `UIViewRepresenta ## Token Flow -```swift -NetworkManager.shared.generateToken(channelName: channelName) { token in - self.agoraKit.joinChannel(byToken: token, channelId: channelName, uid: 0, mediaOptions: option) -} -``` +Use the guarded permission → Token → join implementation in +[upsert-case](.agents/skills/upsert-case/SKILL.md). Snapshot channel, UID, request generation +and engine identity before asynchronous work. Recheck them on main before each continuation; +leave/destroy invalidates pending requests and destroy clears engine ownership. Weak capture +alone does not protect a still-alive controller whose RTC session has ended. + +A nil/empty Token is allowed only when no App Certificate is configured. Reject a missing +required Token and check the SDK join return code without logging credentials. diff --git a/iOS/APIExample/.agents/skills/review-case/SKILL.md b/iOS/APIExample/.agents/skills/review-case/SKILL.md index 816203da2..cab4f5f99 100644 --- a/iOS/APIExample/.agents/skills/review-case/SKILL.md +++ b/iOS/APIExample/.agents/skills/review-case/SKILL.md @@ -12,6 +12,19 @@ metadata: # review-case — APIExample +## Pending Permission and Token Requests + +Review the complete lifecycle against [the creation template](../upsert-case/SKILL.md). +All request/engine ownership transitions must run on main. Freeze channel/UID and request +generation before requesting permission or Token; before joining, check that the generation +and engine identity are still current. Leave invalidates pending work even before joined; +destroy also clears the engine. Repeated cleanup must not destroy another case's engine. +Weak references alone do not invalidate a request when its owner remains alive. + +Exercise delayed permission and Token responses after leave/destroy, repeated cleanup, +rapid reopen and out-of-order responses. Only the current request may join. Also check +permission denial, absent required Token and a nonzero join result. Never log credentials. + ## Review Dimensions (in priority order) ### 1. Engine Lifecycle @@ -23,16 +36,9 @@ The most critical dimension. Leaks here cause crashes in subsequent examples. - `leaveChannel()` + `AgoraRtcEngineKit.destroy()` called in `willMove(toParent:)` when `parent == nil` - No engine instance stored beyond the Main VC's lifetime -**Correct:** -```swift -override func willMove(toParent parent: UIViewController?) { - super.willMove(toParent: parent) - if parent == nil { - agoraKit?.leaveChannel() - AgoraRtcEngineKit.destroy() - } -} -``` +**Correct:** Call the creation template’s idempotent `onDestroy()` from +`willMove(toParent:)` when `parent == nil`. It invalidates pending permission/Token work +before leaving, destroying and clearing the owned engine. **Wrong:** ```swift @@ -78,13 +84,9 @@ func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: I - Microphone permission requested before `joinChannel()` for all cases - `joinChannel()` called only inside the permission grant callback, not before -**Correct:** -```swift -AgoraAudioSession.sharedInstance().requestRecordPermission { [weak self] granted in - guard granted else { return } - self?.agoraKit?.joinChannel(...) -} -``` +**Correct:** Pass the case’s actual camera/microphone permission request to +`requestJoin(channelName:requestPermission:)` in the creation template. Its guarded main-queue +continuation checks the request and engine lifetime before requesting a Token or joining. --- diff --git a/iOS/APIExample/.agents/skills/upsert-case/SKILL.md b/iOS/APIExample/.agents/skills/upsert-case/SKILL.md index 7fd7363af..6e8e93ff3 100644 --- a/iOS/APIExample/.agents/skills/upsert-case/SKILL.md +++ b/iOS/APIExample/.agents/skills/upsert-case/SKILL.md @@ -70,39 +70,98 @@ class Entry: UIViewController { } class Main: BaseViewController { - var agoraKit: AgoraRtcEngineKit? + private(set) var agoraKit: AgoraRtcEngineKit? + private var tokenRequestID = 0 override func viewDidLoad() { super.viewDidLoad() - guard let channelName = configs["channelName"] as? String else { return } + setupRTC() + } + + func setupRTC() { + precondition(Thread.isMainThread) + guard agoraKit == nil else { return } let config = AgoraRtcEngineConfig() config.appId = KeyCenter.AppId agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) - // configure engine, request permissions, then join - NetworkManager.shared.generateToken(channelName: channelName) { [weak self] token in - let option = AgoraRtcChannelMediaOptions() - self?.agoraKit?.joinChannel(byToken: token, channelId: channelName, - uid: 0, mediaOptions: option) + // Configure this case's media, then call requestJoin(channelName:requestPermission:). + // Supply the project's permission flow; its completion must report granted/denied. + } + + // Main-queue entry point. A new request supersedes any pending permission/Token response. + func requestJoin(channelName: String, + requestPermission: (@escaping (Bool) -> Void) -> Void) { + precondition(Thread.isMainThread) + guard let engine = agoraKit, !channelName.isEmpty else { return } + tokenRequestID += 1 + let requestID = tokenRequestID + let uid: UInt = 0 + requestPermission { [weak self, weak engine] granted in + DispatchQueue.main.async { [weak self, weak engine] in + guard let self = self, let engine = engine, + self.tokenRequestID == requestID, self.agoraKit === engine else { return } + guard granted else { + LogUtils.log(message: "Permission denied", level: .error) + return + } + NetworkManager.shared.generateToken(channelName: channelName, uid: uid) { [weak self, weak engine] token in + DispatchQueue.main.async { [weak self, weak engine] in + guard let self = self, let engine = engine, + self.tokenRequestID == requestID, self.agoraKit === engine else { return } + if !(KeyCenter.Certificate ?? "").isEmpty && (token ?? "").isEmpty { + LogUtils.log(message: "Token request failed", level: .error) + return + } + let option = AgoraRtcChannelMediaOptions() + option.clientRoleType = .broadcaster + option.publishMicrophoneTrack = true + // Configure camera publication/canvases for video cases after camera permission. + let result = engine.joinChannel(byToken: token, channelId: channelName, + uid: uid, mediaOptions: option) + if result != 0 { + LogUtils.log(message: "joinChannel failed: \(result)", level: .error) + } + } + } + } } } + func leaveChannel() { + precondition(Thread.isMainThread) + tokenRequestID += 1 + agoraKit?.leaveChannel(nil) + } + + func onDestroy() { + precondition(Thread.isMainThread) + leaveChannel() // Invalidate callbacks even while Token/permission/join is pending. + guard agoraKit != nil else { return } + // Stop case-owned capture, players, timers and observers here. + AgoraRtcEngineKit.destroy() + agoraKit = nil + } + override func willMove(toParent parent: UIViewController?) { super.willMove(toParent: parent) - if parent == nil { - agoraKit?.leaveChannel() - AgoraRtcEngineKit.destroy() - } + if parent == nil { onDestroy() } } } extension Main: AgoraRtcEngineDelegate { func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info) + } } func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error) + DispatchQueue.main.async { [weak self] in + guard let self = self, self.agoraKit === engine else { return } + LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error) + } } } ``` @@ -166,6 +225,12 @@ Key APIs: list 2–5 core SDK methods the case demonstrates. Do not list `joinCh --- +The lifecycle code is a skeleton: wire `requestJoin(channelName:requestPermission:)` from +setup or the Join action, passing the channel from `configs` and the case's actual permission +request. Never replace the permission closure with an unconditional grant in a real case. +Keep setup, join, leave and destroy on the main queue. A user Leave action must call +`leaveChannel()` so pending requests are invalidated, even before the SDK reports joined. + ## Verification Checklist - [ ] Folder created under correct category (Basic / Advanced) @@ -180,6 +245,9 @@ Key APIs: list 2–5 core SDK methods the case demonstrates. Do not list `joinCh - [ ] UI updates inside delegate callbacks dispatched to `DispatchQueue.main` - [ ] Camera/microphone permissions requested before `joinChannel()` - [ ] Case Index row added/updated in `ARCHITECTURE.md` +- [ ] Permission/Token pending → leave/destroy → delayed callback does not join +- [ ] Repeat cleanup, reopen and out-of-order Token responses preserve only the active request +- [ ] Missing required Token and nonzero join return produce a failure state without logging credentials - [ ] Project builds without errors --- diff --git a/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard b/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard index a3a710c5c..ccf8d104e 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard +++ b/iOS/APIExample/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard @@ -9,7 +9,7 @@ - + @@ -61,7 +61,7 @@ - + diff --git a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift index 392ba498c..94c18f5b2 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/ThirdBeautify.swift @@ -30,7 +30,7 @@ class ThirdBeautifyEntry: BaseViewController { // resign channel text field channelTextField.resignFirstResponder() - let actionSheetVC = UIAlertController(title: "Third Beautify".localized, message: nil, preferredStyle: .actionSheet) + let actionSheetVC = UIAlertController(title: "Third-party Beauty".localized, message: nil, preferredStyle: .actionSheet) let sense = UIAlertAction(title: "Sense Beautify".localized, style: .default) { _ in self.jumpHandler(type: .sense) diff --git a/iOS/APIExample/APIExample/ViewController.swift b/iOS/APIExample/APIExample/ViewController.swift index e416322a6..df6c26f39 100644 --- a/iOS/APIExample/APIExample/ViewController.swift +++ b/iOS/APIExample/APIExample/ViewController.swift @@ -31,51 +31,51 @@ class ViewController: AGViewController { MenuItem(name: "Join a channel (Audio)".localized, storyboard: "JoinChannelAudio", controller: ""), MenuItem(name: "Local or remote recording".localized, storyboard: "JoinChannelVideoRecorder", controller: "") ]), - MenuSection(name: "Anvanced", rows: [ + MenuSection(name: "Advanced", rows: [ // MenuItem(name: "Group Video Chat".localized, storyboard: "VideoChat", controller: "VideoChat"), MenuItem(name: "Live Streaming".localized, storyboard: "LiveStreaming", controller: "LiveStreaming"), MenuItem(name: "RTMP Streaming".localized, storyboard: "RTMPStreaming", controller: "RTMPStreaming"), - MenuItem(name: "Video Metadata".localized, storyboard: "VideoMetadata", controller: "VideoMetadata".localized), - MenuItem(name: "Voice Changer".localized, storyboard: "VoiceChanger", controller: ""), - MenuItem(name: "Custom Audio Source".localized, storyboard: "CustomPcmAudioSource", controller: "CustomPcmAudioSource"), + MenuItem(name: "Media Metadata".localized, storyboard: "VideoMetadata", controller: "VideoMetadata".localized), + MenuItem(name: "Voice Effects".localized, storyboard: "VoiceChanger", controller: ""), + MenuItem(name: "Custom Audio Source (PCM)".localized, storyboard: "CustomPcmAudioSource", controller: "CustomPcmAudioSource"), MenuItem(name: "Custom Audio Render".localized, storyboard: "CustomAudioRender", controller: "CustomAudioRender"), - MenuItem(name: "Custom Video Source(Push)".localized, storyboard: "CustomVideoSourcePush", controller: "CustomVideoSourcePush"), - MenuItem(name: "Custom Video Source(Multi)".localized, +// MenuItem(name: "Raw Media Data".localized, storyboard: "RawMediaData", controller: "RawMediaData"), + MenuItem(name: "Raw Audio Data".localized, storyboard: "RawAudioData", controller: ""), + MenuItem(name: "Audio Mixing".localized, storyboard: "AudioMixing", controller: ""), + MenuItem(name: "Audio Waveform".localized, storyboard: "AudioWaveform", controller: ""), + MenuItem(name: "Audio Router (Third-party Player)".localized, storyboard: "AudioRouterPlayer", controller: ""), + MenuItem(name: "Custom Video Source".localized, storyboard: "CustomVideoSourcePush", controller: "CustomVideoSourcePush"), + MenuItem(name: "Multiple Video Sources".localized, storyboard: "CustomVideoSourcePushMulti", controller: "CustomVideoSourcePushMulti"), MenuItem(name: "Custom Video Render".localized, storyboard: "CustomVideoRender", controller: "CustomVideoRender"), -// MenuItem(name: "Raw Media Data".localized, storyboard: "RawMediaData", controller: "RawMediaData"), - MenuItem(name: "Raw Audio Data".localized, storyboard: "RawAudioData", controller: ""), MenuItem(name: "Raw Video Data".localized, storyboard: "RawVideoData", controller: ""), - MenuItem(name: "Picture In Picture(require iOS15)".localized, storyboard: "PictureInPicture", controller: "PictureInPicture"), + MenuItem(name: "Picture In Picture".localized, storyboard: "PictureInPicture", controller: "PictureInPicture"), MenuItem(name: "Simple Filter Extension".localized, storyboard: "SimpleFilter", controller: "SimpleFilter"), + MenuItem(name: "Video Process".localized, storyboard: "VideoProcess", controller: "VideoProcess"), + MenuItem(name: "Local Composite Graph".localized, storyboard: "LocalCompositeGraph", controller: ""), MenuItem(name: "Quick Switch Channel".localized, controller: "QuickSwitchChannel"), MenuItem(name: "Join Multiple Channels".localized, storyboard: "JoinMultiChannel", controller: "JoinMultiChannel"), MenuItem(name: "Stream Encryption".localized, storyboard: "StreamEncryption", controller: ""), - MenuItem(name: "Audio Mixing".localized, storyboard: "AudioMixing", controller: ""), - MenuItem(name: "Precall Test".localized, storyboard: "PrecallTest", controller: ""), + MenuItem(name: "Pre-call Test".localized, storyboard: "PrecallTest", controller: ""), MenuItem(name: "Media Player".localized, storyboard: "MediaPlayer", controller: ""), MenuItem(name: "Screen Share".localized, storyboard: "ScreenShare", controller: ""), - MenuItem(name: "Local Composite Graph".localized, storyboard: "LocalCompositeGraph", controller: ""), - MenuItem(name: "Video Process".localized, storyboard: "VideoProcess", controller: "VideoProcess"), - MenuItem(name: "Agora Beauty".localized, storyboard: "AgoraBeauty", controller: "AgoraBeauty"), - // Hidden because the Rhythm Player APIs are deprecated since RTC SDK 4.6.0. - // MenuItem(name: "Rhythm Player".localized, storyboard: "RhythmPlayer", controller: "RhythmPlayer"), - MenuItem(name: "Create Data Stream".localized, storyboard: "CreateDataStream", controller: ""), + MenuItem(name: "Send Data Stream".localized, storyboard: "CreateDataStream", controller: ""), MenuItem(name: "Media Channel Relay".localized, storyboard: "MediaChannelRelay", controller: ""), MenuItem(name: "Spatial Audio".localized, storyboard: "SpatialAudio", controller: "SpatialAudioMain"), MenuItem(name: "Content Inspect".localized, storyboard: "ContentInspect", controller: ""), - MenuItem(name: "Mutli Camera(iOS13.0+)".localized, storyboard: "MutliCamera", controller: "MutliCamera"), - MenuItem(name: "Ktv copyright music".localized, + MenuItem(name: "Multi Camera".localized, storyboard: "MutliCamera", controller: "MutliCamera"), + MenuItem(name: "KTV Copyright Music".localized, entry: "KtvCopyrightMusic", storyboard: "KtvCopyrightMusic", controller: "KtvCopyrightMusic"), - MenuItem(name: "Third Beautify".localized, storyboard: "ThirdBeautify", controller: ""), + MenuItem(name: "Third-party Beauty".localized, storyboard: "ThirdBeautify", controller: ""), + MenuItem(name: "Beauty 2.0".localized, storyboard: "AgoraBeauty", controller: "AgoraBeauty"), + // Hidden because the Rhythm Player APIs are deprecated since RTC SDK 4.6.0. + // MenuItem(name: "Rhythm Player".localized, storyboard: "RhythmPlayer", controller: "RhythmPlayer"), MenuItem(name: "ARKit".localized, storyboard: "ARKit", controller: ""), - MenuItem(name: "Audio Router(Third Party Player)".localized, storyboard: "AudioRouterPlayer", controller: ""), - MenuItem(name: "Audio Waveform".localized, storyboard: "AudioWaveform", controller: ""), - MenuItem(name: "Transparent Render".localized, storyboard: "TransparentRender", controller: ""), - MenuItem(name: "URL Streaming(RTE Player)".localized, storyboard: "RtePlayer", controller: ""), + MenuItem(name: "Transparent Rendering".localized, storyboard: "TransparentRender", controller: ""), + MenuItem(name: "URL Streaming (RTE Player)".localized, storyboard: "RtePlayer", controller: ""), MenuItem(name: "Simulcast".localized, storyboard: "Simulcast", controller: "SimulcasthEntry"), MenuItem(name: "Multipath".localized, storyboard: "Multipath", controller: "MultipathEntry"), ]) diff --git a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings index 5730c83d5..089d691cb 100644 --- a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings @@ -12,14 +12,13 @@ "Local or remote recording" = "本地/远端录制"; "Live Streaming" = "RTC实时直播/主播/观众"; "RTMP Streaming" = "RTMP旁路推流"; -"Video Metadata" = "SEI消息"; -"Voice Changer" = "美声/音效"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; "Custom Audio Source" = "音频自采集"; -"Custom Audio Source(PCM)" = "音频自采集(PCM)"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; "Custom Audio Render" = "音频自渲染"; "Custom Video Source" = "视频自采集"; -"Custom Video Source(Push)" = "视频自采集(Push)"; -"Custom Video Source(Multi)" = "多路视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; "Custom Video Render" = "视频自渲染(Metal)"; "Quick Switch Channel" = "快速切换频道"; "Join Multiple Channels" = "加入多频道"; @@ -27,7 +26,7 @@ "Audio Mixing" = "音频文件混音"; "Raw Video Data" = "视频裸数据"; "Simple Filter Extension" = "插件系统"; -"Precall Test" = "通话前网络/设备测试"; +"Pre-call Test" = "通话前网络/设备测试"; "Media Player" = "流媒体播放器"; "Screen Share" = "屏幕共享"; "Local Composite Graph" = "本地合图"; @@ -115,12 +114,12 @@ "Broadcaster" = "主播"; "Audience" = "观众"; "Pick Role" = "选择角色"; -"Create Data Stream" = "创建数据流"; +"Send Data Stream" = "发送数据流"; "Raw Audio Data" = "音频裸数据"; "Screen Share + Camera" = "屏幕共享 + 摄像头"; "please input channel name!" = "请输入频道名!"; "Video Process" = "视频增强"; -"Agora Beauty" = "Agora 美颜"; +"Beauty 2.0" = "美颜 2.0"; "Rhythm Player" = "虚拟节拍器"; "Spatial Audio" = "空间音效"; "Please move the red icon to experience the 3D audio effect" = "请移动红色图标体验3D音频效果"; @@ -131,17 +130,17 @@ "SwitchCamera" = "切换摄像头"; "remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; "Screenshot successful" = "截图成功"; -"Picture In Picture(require iOS15)" = "画中画(iOS15支持)"; +"Picture In Picture" = "画中画"; "please input Token!" = "请输入Token"; "please input AppId!" = "请输入APPID"; "stopMicrophoneRecording" = "停止麦克风采集"; -"Ktv copyright music" = "KTV版权音乐"; -"Mutli Camera(iOS13.0+)" = "多路摄像头(iOS13.0+)"; +"KTV Copyright Music" = "KTV版权音乐"; +"Multi Camera" = "多路摄像头"; "Multipath" = "网络传输高级功能"; "Simulcast" = "同源多流"; "Open Rear Camera" = "打开后摄像头"; "Close Rear Camera" = "关闭后摄像头"; -"Third Beautify" = "第三方美颜"; +"Third-party Beauty" = "第三方美颜"; "Sense Beautify" = "商汤美颜"; "FU Beautify" = "相芯美颜"; "license authorization failed, please check whether the license file is correct" = "license授权失败, 请检查license文件是否正确"; @@ -161,7 +160,7 @@ "Yellow" = "黄色"; "preload Channel" = "预加载"; "cancel preload" = "取消预加载"; -"Audio Router(Third Party Player)" = "音频路由(第三方播放器)"; +"Audio Router (Third-party Player)" = "音频路由(第三方播放器)"; "Player Type(ijkplayer/avplayer)" = "播放器选择(ijkplayer/原生)"; "Audio Waveform" = "音频频谱"; @@ -203,9 +202,9 @@ "Powerful Male Singing Voice" = "力量男歌声"; "Dreamy Female Singing Voice" = "梦幻女歌声"; -"Transparent Render"= "透明渲染"; +"Transparent Rendering"= "透明渲染"; -"URL Streaming(RTE Player)"= "URL拉流(RTE Player)"; +"URL Streaming (RTE Player)"= "URL拉流(RTE Player)"; "Makeup Enable" = "美妆开关"; "Eyebrow Style" = "眉毛类型"; diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index 809a1bb05..e947ea055 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -95,41 +95,41 @@ APIExample/ │ └── Advanced/ │ ├── LiveStreaming/ # "Live Streaming" — setClientRole │ ├── RTMPStreaming/ # "RTMP Streaming" — push to CDN - │ ├── VideoMetadata/ # "Video Metadata" — send/receive metadata - │ ├── VoiceChanger/ # "Voice Changer" — voice beautifier/effects - │ ├── CustomPcmAudioSource/ # "Custom Audio Source" — push PCM audio + │ ├── VideoMetadata/ # "Media Metadata" — send/receive metadata + │ ├── VoiceChanger/ # "Voice Effects" — voice beautifier/effects + │ ├── CustomPcmAudioSource/ # "Custom Audio Source (PCM)" — push PCM audio │ ├── CustomAudioRender/ # "Custom Audio Render" — pull audio rendering │ ├── CustomAudioSource/ # (legacy custom audio source) - │ ├── CustomVideoSourcePush/ # "Custom Video Source(Push)" — push external video - │ ├── CustomVideoSourcePushMulti/ # "Custom Video Source(Multi)" — multi-track push + │ ├── CustomVideoSourcePush/ # "Custom Video Source" — push external video + │ ├── CustomVideoSourcePushMulti/ # "Multiple Video Sources" — multi-track push │ ├── CustomVideoRender/ # "Custom Video Render" │ ├── RawAudioData/ # "Raw Audio Data" │ ├── RawVideoData/ # "Raw Video Data" │ ├── RawMediaData/ # (legacy raw media data) - │ ├── PictureInPicture/ # "Picture In Picture (iOS15+)" + │ ├── PictureInPicture/ # "Picture In Picture" │ ├── SimpleFilter/ # "Simple Filter Extension" │ ├── QuickSwitchChannel/ # "Quick Switch Channel" │ ├── JoinMultiChannel/ # "Join Multiple Channels" │ ├── StreamEncryption/ # "Stream Encryption" │ ├── AudioMixing/ # "Audio Mixing" - │ ├── PrecallTest/ # "Precall Test" + │ ├── PrecallTest/ # "Pre-call Test" │ ├── MediaPlayer/ # "Media Player" │ ├── ScreenShare/ # "Screen Share" │ ├── LocalCompositeGraph/ # "Local Composite Graph" │ ├── VideoProcess/ # "Video Process" - │ ├── AgoraBeauty/ # "Agora Beauty" + │ ├── AgoraBeauty/ # "Beauty 2.0" │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 - │ ├── CreateDataStream/ # "Create Data Stream" + │ ├── CreateDataStream/ # "Send Data Stream" │ ├── MediaChannelRelay/ # "Media Channel Relay" │ ├── SpatialAudio/ # "Spatial Audio" │ ├── ContentInspect/ # "Content Inspect" - │ ├── MutliCamera/ # "Multi Camera (iOS13+)" + │ ├── MutliCamera/ # "Multi Camera" │ ├── KtvCopyrightMusic/ # "KTV Copyright Music" - │ ├── ThirdBeautify/ # "Third Beautify" — third-party beauty SDK + │ ├── ThirdBeautify/ # "Third-party Beauty" — third-party beauty SDK │ ├── ARKit/ # "ARKit" - │ ├── AudioRouterPlayer/ # "Audio Router (Third Party Player)" + │ ├── AudioRouterPlayer/ # "Audio Router (Third-party Player)" │ ├── AudioWaveform/ # "Audio Waveform" - │ ├── TransparentRender/ # "Transparent Render" + │ ├── TransparentRender/ # "Transparent Rendering" │ ├── RtePlayer/ # "URL Streaming (RTE Player)" │ ├── Simulcast/ # "Simulcast" │ ├── Multipath/ # "Multipath" @@ -203,10 +203,11 @@ the upsert and review skills. ## Token Flow -```swift -NetworkManager.shared.generateToken(channelName: channelId, uid: uid) { token in - self.agoraKit?.joinChannel(byToken: token, channelId: channelId, uid: uid, mediaOptions: options) -} -``` +Use the guarded permission → Token → join implementation in +[upsert-case](.agents/skills/upsert-case/SKILL.md). Snapshot channel, UID, request generation +and engine identity before asynchronous work. Recheck them on main before each continuation; +leave/destroy invalidates pending requests and destroy clears engine ownership. Weak capture +alone does not protect a still-alive controller whose RTC session has ended. -If `KeyCenter.Certificate` is nil, token generation is skipped and a nil token is used — valid for projects without App Certificate. +A nil/empty Token is allowed only when no App Certificate is configured. Reject a missing +required Token and check the SDK join return code without logging credentials. diff --git a/macOS/APIExample/Examples/Advanced/AgoraBeauty/Base.lproj/AgoraBeauty.storyboard b/macOS/APIExample/Examples/Advanced/AgoraBeauty/Base.lproj/AgoraBeauty.storyboard index d637f1c2d..17fbce8e1 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/Base.lproj/AgoraBeauty.storyboard +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/Base.lproj/AgoraBeauty.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/AgoraBeauty/en.lproj/VideoProcess.strings b/macOS/APIExample/Examples/Advanced/AgoraBeauty/en.lproj/VideoProcess.strings index 48ca105d7..d1a7b597f 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/en.lproj/VideoProcess.strings +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/en.lproj/VideoProcess.strings @@ -20,8 +20,8 @@ /* Class = "NSButtonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ "Lhu-U1-6qh.title" = "Join"; -/* Class = "NSViewController"; title = "Video Process"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Video Process"; +/* Class = "NSViewController"; title = "Beauty 2.0"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "Beauty 2.0"; /* Class = "NSTextFieldCell"; title = "锐利"; ObjectID = "ZOR-gO-Ji2"; */ "ZOR-gO-Ji2.title" = "Sharp"; diff --git a/macOS/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/VideoProcess.strings b/macOS/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/VideoProcess.strings index a3c3cbf19..4300b23c6 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/VideoProcess.strings +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/VideoProcess.strings @@ -16,8 +16,8 @@ /* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ "Lhu-U1-6qh.title" = "加入频道"; -/* Element = "viewController"; title = "Video Process"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "视频增强"; +/* Element = "viewController"; title = "Beauty 2.0"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "美颜 2.0"; /* Element = "textFieldCell"; title = "Sharp"; ObjectID = "ZOR-gO-Ji2"; */ "ZOR-gO-Ji2.title" = "锐利"; diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard b/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard index 6460159ee..8628ffa68 100644 --- a/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/en.lproj/AudioMixing.strings b/macOS/APIExample/Examples/Advanced/AudioMixing/en.lproj/AudioMixing.strings index b9e37bdad..125c6188a 100644 --- a/macOS/APIExample/Examples/Advanced/AudioMixing/en.lproj/AudioMixing.strings +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/en.lproj/AudioMixing.strings @@ -41,8 +41,8 @@ /* Class = "NSButtonCell"; title = "Stop Additional Effect"; ObjectID = "iQT-DV-ynF"; */ "iQT-DV-ynF.title" = "Stop Additional Effect"; -/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "Join Channel Audio"; +/* Class = "NSViewController"; title = "Audio Mixing"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "Audio Mixing"; /* Class = "NSButtonCell"; title = "Pause"; ObjectID = "mcr-Pl-O4W"; */ "mcr-Pl-O4W.title" = "Pause"; diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 550286949..9cec20865 100644 --- a/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -40,8 +40,8 @@ /* Element = "buttonCell"; title = "Stop Additional Effect"; ObjectID = "iQT-DV-ynF"; */ "iQT-DV-ynF.title" = "停止附加音效"; -/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "实时音频通话/直播"; +/* Element = "viewController"; title = "Audio Mixing"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "音频文件混音"; /* Element = "buttonCell"; title = "Pause"; ObjectID = "mcr-Pl-O4W"; */ "mcr-Pl-O4W.title" = "暂停"; diff --git a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard index b828f769d..751338efc 100644 --- a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings index 88411ad15..fa87086aa 100644 --- a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings @@ -4,8 +4,8 @@ /* Element = "buttonCell"; title = "Join"; ObjectID = "5CP-OV-fSv"; */ "5CP-OV-fSv.title" = "加入频道"; -/* Element = "viewController"; title = "Join Multiple Channels"; ObjectID = "IBJ-wZ-9Xx"; */ -"IBJ-wZ-9Xx.title" = "加入多频道"; +/* Element = "viewController"; title = "Media Channel Relay"; ObjectID = "IBJ-wZ-9Xx"; */ +"IBJ-wZ-9Xx.title" = "跨频道流转发"; /* Element = "buttonCell"; title = "Start Relay"; ObjectID = "NQw-hk-hUl"; */ "NQw-hk-hUl.title" = "开始转发"; diff --git a/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.storyboard b/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.storyboard index e412fd4ed..497a202ad 100644 --- a/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.storyboard +++ b/macOS/APIExample/Examples/Advanced/ContentInspect/ContentInspect.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard b/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard index 2e77dcc80..1ccee18fa 100644 --- a/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard +++ b/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard @@ -6,7 +6,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard b/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard index dafa21b00..621258b57 100644 --- a/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard +++ b/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard @@ -6,10 +6,10 @@ - + - + diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings b/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings index b4651c89d..564b99bbe 100644 --- a/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings +++ b/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings @@ -1,5 +1,5 @@ -/* Element = "viewController"; title = "Custom Audio Source"; ObjectID = "8Q5-xy-D8A"; */ -"8Q5-xy-D8A.title" = "音频自采集"; +/* Element = "viewController"; title = "Custom Audio Source (PCM)"; ObjectID = "8Q5-xy-D8A"; */ +"8Q5-xy-D8A.title" = "音频自采集(PCM)"; /* Element = "textFieldCell"; title = "Play the local PCM"; ObjectID = "UzT-xh-vr5"; */ "UzT-xh-vr5.title" = "播放本地 PCM"; diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard b/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard index d7e039b66..ef0510ead 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard +++ b/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings b/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings index 1e91b5ab6..ef69b13d9 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings +++ b/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings @@ -1,5 +1,5 @@ -/* Element = "viewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "jEL-F4-BwV"; */ -"jEL-F4-BwV.title" = "视频自渲染"; +/* Element = "viewController"; title = "Custom Video Render"; ObjectID = "jEL-F4-BwV"; */ +"jEL-F4-BwV.title" = "视频自渲染(Metal)"; /* Element = "box"; title = "Box"; ObjectID = "jwa-os-AjB"; */ "jwa-os-AjB.title" = "Box"; diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard index c9ba97973..2a4ad67af 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings index 2fd84f9b8..028389319 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings @@ -7,5 +7,5 @@ /* Element = "buttonCell"; title = "Join"; ObjectID = "bCD-1H-KfP"; */ "bCD-1H-KfP.title" = "加入频道"; -/* Element = "viewController"; title = "Custom Video Source (Push)"; ObjectID = "sXF-vm-Rrb"; */ -"sXF-vm-Rrb.title" = "视频自采集(Push)"; +/* Element = "viewController"; title = "Custom Video Source"; ObjectID = "sXF-vm-Rrb"; */ +"sXF-vm-Rrb.title" = "视频自采集"; diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/Base.lproj/CustomVideoSourcePushMulti.storyboard b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/Base.lproj/CustomVideoSourcePushMulti.storyboard index 5b74aa98a..73b125ea3 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/Base.lproj/CustomVideoSourcePushMulti.storyboard +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/Base.lproj/CustomVideoSourcePushMulti.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings index 2f13de5df..5b30cad67 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings @@ -10,8 +10,8 @@ /* Element = "buttonCell"; title = "Destory Track"; ObjectID = "fY0-1R-m7R"; */ "fY0-1R-m7R.title" = "销毁Track"; -/* Element = "viewController"; title = "Custom Video Source (Push)"; ObjectID = "sXF-vm-Rrb"; */ -"sXF-vm-Rrb.title" = "音频自采集(Push)"; +/* Element = "viewController"; title = "Multiple Video Sources"; ObjectID = "sXF-vm-Rrb"; */ +"sXF-vm-Rrb.title" = "多路视频源"; /* Element = "buttonCell"; title = "Create VideoTrack"; ObjectID = "tbi-87-lo2"; */ "tbi-87-lo2.title" = "创建采集Track"; diff --git a/macOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard b/macOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard index 58680debc..f39cb8697 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.storyboard b/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.storyboard index 02cefd705..b628d87d1 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.storyboard +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.strings b/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.strings index 6852ecb47..46925cf66 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.strings +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/en.lproj/LiveStreaming.strings @@ -14,8 +14,8 @@ /* Class = "NSMenuItem"; title = "1V3"; ObjectID = "VNU-so-ajb"; */ "VNU-so-ajb.title" = "1V3"; -/* Class = "NSViewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Video Call/Live Streaming"; +/* Class = "NSViewController"; title = "Live Streaming"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "Live Streaming"; /* Class = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ "cH4-ft-u77.title" = "1V8"; diff --git a/macOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings b/macOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings index 301788c94..bd7713af1 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings @@ -23,8 +23,8 @@ /* Class = "NSTextFieldCell"; title = "S or l flow"; ObjectID = "XH3-Ib-cXr"; */ "zOU-d9-jDn.title" = "大小流"; -/* Class = "NSViewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "实时视频通话/直播"; +/* Class = "NSViewController"; title = "Live Streaming"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "RTC实时直播/主播/观众"; /* Class = "NSButtonCell"; title = "Snap Shot"; ObjectID = "cDh-B1-x3E"; */ "hca-Of-3bM.title" = "截图"; diff --git a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/Base.lproj/LocalVideoTranscoding.storyboard b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/Base.lproj/LocalVideoTranscoding.storyboard index c8e0071d6..ef5d02658 100644 --- a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/Base.lproj/LocalVideoTranscoding.storyboard +++ b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/Base.lproj/LocalVideoTranscoding.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/en.lproj/LocalVideoTranscoding.strings b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/en.lproj/LocalVideoTranscoding.strings index 3d58df867..475b91066 100644 --- a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/en.lproj/LocalVideoTranscoding.strings +++ b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/en.lproj/LocalVideoTranscoding.strings @@ -5,8 +5,8 @@ /* Class = "NSButtonCell"; title = "Generate thumbnail"; ObjectID = "G2K-yC-ti4"; */ "G2K-yC-ti4.title" = "Generate thumbnail"; -/* Class = "NSViewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "Stream Encryption"; +/* Class = "NSViewController"; title = "Local Video Transcoding"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "Local Video Transcoding"; /* Class = "NSTextFieldCell"; title = "Virtual Background"; ObjectID = "I7C-7c-uwc"; */ "I7C-7c-uwc.title" = "Virtual Background"; diff --git a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/zh-Hans.lproj/LocalVideoTranscoding.strings b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/zh-Hans.lproj/LocalVideoTranscoding.strings index 3d58df867..3152a1240 100644 --- a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/zh-Hans.lproj/LocalVideoTranscoding.strings +++ b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/zh-Hans.lproj/LocalVideoTranscoding.strings @@ -5,8 +5,8 @@ /* Class = "NSButtonCell"; title = "Generate thumbnail"; ObjectID = "G2K-yC-ti4"; */ "G2K-yC-ti4.title" = "Generate thumbnail"; -/* Class = "NSViewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "Stream Encryption"; +/* Class = "NSViewController"; title = "Local Video Transcoding"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "本地合图"; /* Class = "NSTextFieldCell"; title = "Virtual Background"; ObjectID = "I7C-7c-uwc"; */ "I7C-7c-uwc.title" = "Virtual Background"; diff --git a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/Base.lproj/MultiCameraSourece.storyboard b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/Base.lproj/MultiCameraSourece.storyboard index 63609013d..2104b3617 100644 --- a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/Base.lproj/MultiCameraSourece.storyboard +++ b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/Base.lproj/MultiCameraSourece.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings index 623ca35b0..b284b381f 100644 --- a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings +++ b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings @@ -16,8 +16,8 @@ /* Element = "buttonCell"; title = "Start collection"; ObjectID = "UNg-f5-Jxh"; */ "UNg-f5-Jxh.title" = "开始采集"; -/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "实时视频通话/直播"; +/* Element = "viewController"; title = "Multi Camera"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "多路摄像头"; /* Element = "buttonCell"; title = "Start collecting"; ObjectID = "irM-5L-9uT"; */ "irM-5L-9uT.title" = "开始采集"; diff --git a/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard b/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard index acaf242a0..fbfdb1379 100644 --- a/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings b/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings index cd9ba6a72..a10473ab8 100644 --- a/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings @@ -13,8 +13,8 @@ /* Element = "textFieldCell"; title = "Please say something.."; ObjectID = "BPe-Gx-enC"; */ "BPe-Gx-enC.title" = "尝试说一些话..."; -/* Element = "viewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "通话前测试"; +/* Element = "viewController"; title = "Pre-call Test"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "通话前网络/设备测试"; /* Element = "textFieldCell"; title = "10"; ObjectID = "L6F-q4-SNZ"; */ "L6F-q4-SNZ.title" = "10"; diff --git a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/Base.lproj/QuickSwitchChannel.storyboard b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/Base.lproj/QuickSwitchChannel.storyboard index 3e68fbaee..b9b130cdc 100644 --- a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/Base.lproj/QuickSwitchChannel.storyboard +++ b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/Base.lproj/QuickSwitchChannel.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/en.lproj/QuickSwitchChannel.storyboard b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/en.lproj/QuickSwitchChannel.storyboard index 3e68fbaee..b9b130cdc 100644 --- a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/en.lproj/QuickSwitchChannel.storyboard +++ b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/en.lproj/QuickSwitchChannel.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/zh-Hans.lproj/QuickSwitchChannel.strings b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/zh-Hans.lproj/QuickSwitchChannel.strings index 83843cae5..4a642f40a 100644 --- a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/zh-Hans.lproj/QuickSwitchChannel.strings +++ b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/zh-Hans.lproj/QuickSwitchChannel.strings @@ -7,5 +7,5 @@ /* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ "Lhu-U1-6qh.title" = "加入频道"; -/* Element = "viewController"; title = "Raw Video Data"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "视频裸数据"; +/* Element = "viewController"; title = "Quick Switch Channel"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "快速切换频道"; diff --git a/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard b/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard index d6c7e1329..7e927e3fe 100644 --- a/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings b/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings index 529c8d442..e26ea7277 100644 --- a/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings @@ -7,8 +7,8 @@ /* Element = "buttonCell"; title = "Window Share"; ObjectID = "GY6-hd-Bf5"; */ "GY6-hd-Bf5.title" = "窗口共享"; -/* Element = "viewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "码流加密"; +/* Element = "viewController"; title = "Screen Share"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "屏幕共享"; /* Element = "buttonCell"; title = "Display Share"; ObjectID = "SAX-wa-nOm"; */ "SAX-wa-nOm.title" = "屏幕共享"; diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard b/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard index c49c92385..8cecfb408 100644 --- a/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift b/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift index fe8fbc834..b6106d4e1 100644 --- a/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift @@ -230,7 +230,7 @@ class VoiceChanger: BaseViewController { } func initSelectVoiceChangerPicker() { selectVoiceChangerPicker.isEnabled = false - selectVoiceChangerPicker.label.stringValue = "Voice Changer".localized + selectVoiceChangerPicker.label.stringValue = "Voice Effects".localized selectVoiceChangerPicker.picker.addItems(withTitles: voiceChangers.map { $0.description() }) selectVoiceChangerPicker.onSelectChanged {[weak self] in guard let self = self else {return} diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings index 27a5b96ee..579a8177d 100644 --- a/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings @@ -37,8 +37,8 @@ /* Element = "textFieldCell"; title = "125hz"; ObjectID = "iEy-1i-vf4"; */ "iEy-1i-vf4.title" = "125hz"; -/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "实时音频通话/直播"; +/* Element = "viewController"; title = "Voice Effects"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "美声/音效"; /* Element = "textFieldCell"; title = "8Khz"; ObjectID = "k68-jy-Jcs"; */ "k68-jy-Jcs.title" = "8Khz"; diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard b/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard index a2876a881..2341812d2 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings b/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings index a28c024f8..be0b8697e 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings @@ -19,5 +19,5 @@ /* Element = "box"; title = "Box"; ObjectID = "j41-op-nLI"; */ "j41-op-nLI.title" = "Box"; -/* Element = "viewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "实时音频通话/直播"; +/* Element = "viewController"; title = "Join a channel (Audio)"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "实时语音通话/直播"; diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/Base.lproj/JoinChannelVideoRecorder.storyboard b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/Base.lproj/JoinChannelVideoRecorder.storyboard index a047335f0..c2b2df136 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/Base.lproj/JoinChannelVideoRecorder.storyboard +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/Base.lproj/JoinChannelVideoRecorder.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings index a4e7973ad..a6738ac2a 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings @@ -4,5 +4,5 @@ /* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ "Lhu-U1-6qh.title" = "加入频道"; -/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "实时视频通话/直播"; +/* Element = "viewController"; title = "Local or remote recording"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "本地/远端录制"; diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/Base.lproj/JoinChannelVideoToken.storyboard b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/Base.lproj/JoinChannelVideoToken.storyboard index b89b1a122..5961baeb9 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/Base.lproj/JoinChannelVideoToken.storyboard +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/Base.lproj/JoinChannelVideoToken.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings index 71f76642d..1dbab4355 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings @@ -13,8 +13,8 @@ /* Element = "textFieldCell"; title = "AppID"; ObjectID = "UCG-Zk-hiI"; */ "UCG-Zk-hiI.title" = "AppID"; -/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "实时视频通话/直播"; +/* Element = "viewController"; title = "Join a channel (Token)"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "实时视频通话/直播(Token验证)"; /* Element = "tokenFieldCell"; placeholderString = "Please the input token"; ObjectID = "ZMh-gp-u55"; */ "ZMh-gp-u55.placeholderString" = "请输入Token"; diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard b/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard index 6f0b9d323..5aa09149f 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard @@ -9,7 +9,7 @@ - + diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings index a4e7973ad..f04530f99 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings @@ -4,5 +4,5 @@ /* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ "Lhu-U1-6qh.title" = "加入频道"; -/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ +/* Element = "viewController"; title = "Join a channel (Video)"; ObjectID = "YjT-yy-DnJ"; */ "YjT-yy-DnJ.title" = "实时视频通话/直播"; diff --git a/macOS/APIExample/ViewController.swift b/macOS/APIExample/ViewController.swift index f79cb8272..ae523281e 100644 --- a/macOS/APIExample/ViewController.swift +++ b/macOS/APIExample/ViewController.swift @@ -25,35 +25,35 @@ class MenuController: NSViewController { MenuItem(name: "Join a channel (Video)".localized, identifier: "menuCell", controller: "JoinChannelVideo", storyboard: "JoinChannelVideo"), MenuItem(name: "Join a channel (Audio)".localized, identifier: "menuCell", controller: "JoinChannelAudio", storyboard: "JoinChannelAudio"), MenuItem(name: "Local or remote recording".localized, identifier: "menuCell", controller: "JoinChannelVideoRecorder", storyboard: "JoinChannelVideoRecorder"), - MenuItem(name: "Anvanced", identifier: "headerCell"), + MenuItem(name: "Advanced", identifier: "headerCell"), MenuItem(name: "Live Streaming".localized, identifier: "menuCell", controller: "LiveStreaming", storyboard: "LiveStreaming"), MenuItem(name: "RTMP Streaming".localized, identifier: "menuCell", controller: "RTMPStreaming", storyboard: "RTMPStreaming"), - MenuItem(name: "Custom Video Source(Push)".localized, identifier: "menuCell", controller: "CustomVideoSourcePush", storyboard: "CustomVideoSourcePush"), - MenuItem(name: "Custom Video Source(Multi)".localized, identifier: "menuCell", controller: "CustomVideoSourcePushMulti", storyboard: "CustomVideoSourcePushMulti"), - MenuItem(name: "Custom Video Render".localized, identifier: "menuCell", controller: "CustomVideoRender", storyboard: "CustomVideoRender"), - MenuItem(name: "Custom Audio Source".localized, identifier: "menuCell", controller: "CustomAudioSource", storyboard: "CustomAudioSource"), + MenuItem(name: "Voice Effects".localized, identifier: "menuCell", controller: "VoiceChanger", storyboard: "VoiceChanger"), + MenuItem(name: "Custom Audio Source (PCM)".localized, identifier: "menuCell", controller: "CustomAudioSource", storyboard: "CustomAudioSource"), MenuItem(name: "Custom Audio Render".localized, identifier: "menuCell", controller: "CustomAudioRender", storyboard: "CustomAudioRender"), // MenuItem(name: "Raw Media Data".localized, identifier: "menuCell", controller: "RawMediaData", storyboard: "RawMediaData"), - MenuItem(name: "Raw Video Data".localized, identifier: "menuCell", controller: "RawVideoData", storyboard: "RawVideoData"), MenuItem(name: "Raw Audio Data".localized, identifier: "menuCell", controller: "RawAudioData", storyboard: "RawAudioData"), + MenuItem(name: "Audio Mixing".localized, identifier: "menuCell", controller: "AudioMixing", storyboard: "AudioMixing"), + MenuItem(name: "Custom Video Source".localized, identifier: "menuCell", controller: "CustomVideoSourcePush", storyboard: "CustomVideoSourcePush"), + MenuItem(name: "Multiple Video Sources".localized, identifier: "menuCell", controller: "CustomVideoSourcePushMulti", storyboard: "CustomVideoSourcePushMulti"), + MenuItem(name: "Custom Video Render".localized, identifier: "menuCell", controller: "CustomVideoRender", storyboard: "CustomVideoRender"), + MenuItem(name: "Raw Video Data".localized, identifier: "menuCell", controller: "RawVideoData", storyboard: "RawVideoData"), + MenuItem(name: "Simple Filter Extension".localized, identifier: "menuCell", controller: "SimpleFilter", storyboard: "SimpleFilter"), + MenuItem(name: "Video Process".localized, identifier: "menuCell", controller: "Video Process", storyboard: "VideoProcess"), + MenuItem(name: "Local Video Transcoding".localized, identifier: "menuCell", controller: "LocalVideoTranscoding", storyboard: "LocalVideoTranscoding"), + MenuItem(name: "Quick Switch Channel".localized, identifier: "menuCell", controller: "QuickSwitchChannel", storyboard: "QuickSwitchChannel"), MenuItem(name: "Join Multiple Channels".localized, identifier: "menuCell", controller: "JoinMultipleChannel", storyboard: "JoinMultiChannel"), MenuItem(name: "Stream Encryption".localized, identifier: "menuCell", controller: "StreamEncryption", storyboard: "StreamEncryption"), + MenuItem(name: "Pre-call Test".localized, identifier: "menuCell", controller: "PrecallTest", storyboard: "PrecallTest"), + MenuItem(name: "Media Player".localized, identifier: "menuCell", controller: "MediaPlayer", storyboard: "MediaPlayer"), MenuItem(name: "Screen Share".localized, identifier: "menuCell", controller: "ScreenShare", storyboard: "ScreenShare"), - MenuItem(name: "Local Video Transcoding".localized, identifier: "menuCell", controller: "LocalVideoTranscoding", storyboard: "LocalVideoTranscoding"), + MenuItem(name: "Send Data Stream".localized, identifier: "menuCell", controller: "CreateDataStream", storyboard: "CreateDataStream"), MenuItem(name: "Media Channel Relay".localized, identifier: "menuCell", controller: "ChannelMediaRelay", storyboard: "ChannelMediaRelay"), - MenuItem(name: "Audio Mixing".localized, identifier: "menuCell", controller: "AudioMixing", storyboard: "AudioMixing"), - MenuItem(name: "Voice Changer".localized, identifier: "menuCell", controller: "VoiceChanger", storyboard: "VoiceChanger"), - MenuItem(name: "Precall Test".localized, identifier: "menuCell", controller: "PrecallTest", storyboard: "PrecallTest"), - MenuItem(name: "Create Data Stream".localized, identifier: "menuCell", controller: "CreateDataStream", storyboard: "CreateDataStream"), - MenuItem(name: "Simple Filter".localized, identifier: "menuCell", controller: "SimpleFilter", storyboard: "SimpleFilter"), - MenuItem(name: "Video Process".localized, identifier: "menuCell", controller: "Video Process", storyboard: "VideoProcess"), - MenuItem(name: "Agora Beauty".localized, identifier: "menuCell", controller: "AgoraBeauty", storyboard: "AgoraBeauty"), - MenuItem(name: "Media Player".localized, identifier: "menuCell", controller: "MediaPlayer", storyboard: "MediaPlayer"), - MenuItem(name: "Quick Switch Channel".localized, identifier: "menuCell", controller: "QuickSwitchChannel", storyboard: "QuickSwitchChannel"), MenuItem(name: "Spatial Audio".localized, identifier: "menuCell", controller: "SpatialAudio", storyboard: "SpatialAudio"), MenuItem(name: "Content Inspect".localized, identifier: "menuCell", controller: "ContentInspect", storyboard: "ContentInspect"), - MenuItem(name: "Multi Camera Sourece".localized, identifier: "menuCell", controller: "MultiCameraSourece", storyboard: "MultiCameraSourece"), - MenuItem(name: "URL Streaming(RTE Player)".localized, identifier: "menuCell", controller: "RtePlayer", storyboard: "RtePlayer"), + MenuItem(name: "Multi Camera".localized, identifier: "menuCell", controller: "MultiCameraSourece", storyboard: "MultiCameraSourece"), + MenuItem(name: "Beauty 2.0".localized, identifier: "menuCell", controller: "AgoraBeauty", storyboard: "AgoraBeauty"), + MenuItem(name: "URL Streaming (RTE Player)".localized, identifier: "menuCell", controller: "RtePlayer", storyboard: "RtePlayer"), MenuItem(name: "Simulcast".localized, identifier: "menuCell", controller: "Simulcast", storyboard: "Simulcast"), MenuItem(name: "Multipath".localized, identifier: "menuCell", controller: "Multipath", storyboard: "Multipath"), ] diff --git a/macOS/APIExample/zh-Hans.lproj/Localizable.strings b/macOS/APIExample/zh-Hans.lproj/Localizable.strings index 07cc24609..a2f061bc5 100644 --- a/macOS/APIExample/zh-Hans.lproj/Localizable.strings +++ b/macOS/APIExample/zh-Hans.lproj/Localizable.strings @@ -12,12 +12,12 @@ "Local or remote recording" = "本地/远端录制"; "Live Streaming" = "RTC实时直播/主播/观众"; "RTMP Streaming" = "RTMP旁路推流"; -"Video Metadata" = "SEI消息"; -"Voice Changer" = "美声/音效"; -"Custom Audio Source" = "音频自采集"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; "Custom Audio Render" = "音频自渲染"; -"Custom Video Source(Push)" = "视频自采集(Push)"; -"Custom Video Source(Multi)" = "多路视频自采集"; +"Custom Video Source" = "视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; "Create Video Track" = "创建采集Track"; "Destory Track" = "销毁Track"; "Custom Video Render" = "视频自渲染(Metal)"; @@ -26,11 +26,11 @@ "Join Multiple Channels" = "加入多频道"; "Stream Encryption" = "音视频流加密"; "Audio Mixing" = "音频文件混音"; -"Precall Test" = "通话前网络/设备测试"; +"Pre-call Test" = "通话前网络/设备测试"; "Media Player" = "流媒体播放器"; "Screen Share" = "屏幕共享"; "Media Channel Relay" = "跨频道流转发"; -"URL Streaming(RTE Player)" = "URL拉流(RTE Player)"; +"URL Streaming (RTE Player)" = "URL拉流(RTE Player)"; "Set Resolution" = "设置视频分辨率"; "Set Fps" = "设置视频帧率"; @@ -162,7 +162,7 @@ "Voice Pitch" = "声调"; "Set Audio Effect Params" = "设置参数"; "Equalization Band" = "波段增益"; -"Create Data Stream" = "创建数据流"; +"Send Data Stream" = "发送数据流"; "Send Message" = "发送消息"; "Input Message" = "输入消息"; "Send" = "发送"; @@ -171,10 +171,10 @@ "Raw Video Data" = "视频裸数据"; "SnapShot" = "截图"; "Video Source" = "视频源选择"; -"Simple Filter" = "插件系统"; +"Simple Filter Extension" = "插件系统"; "Enable Grey" = "开启去色"; "Video Process" = "视频增强"; -"Agora Beauty" = "Agora 美颜"; +"Beauty 2.0" = "美颜 2.0"; "Low light Enhancement" = "暗光增强"; "Video Denoise" = "视频降噪"; "Color Enhancement" = "色彩增强"; @@ -208,7 +208,7 @@ "Pink" = "粉色"; "Purple" = "紫色"; "Yellow" = "黄色"; -"Multi Camera Sourece" = "多路摄像头采集"; +"Multi Camera" = "多路摄像头"; "Start Capture" = "开始采集"; "Start Publish" = "开始推流"; "Stop Capture" = "停止采集"; diff --git a/windows/APIExample/APIExample/APIExampleDlg.cpp b/windows/APIExample/APIExample/APIExampleDlg.cpp index dfd230ed6..765f7cb69 100755 --- a/windows/APIExample/APIExample/APIExampleDlg.cpp +++ b/windows/APIExample/APIExample/APIExampleDlg.cpp @@ -225,42 +225,40 @@ void CAPIExampleDlg::InitSceneDialog() //advanced list // m_vecAdvanced.push_back(advancedRtmpInject); m_vecAdvanced.push_back(advancedRtmpStreaming); - m_vecAdvanced.push_back(advancedCrossChannel); m_vecAdvanced.push_back(advancedMetadata); - m_vecAdvanced.push_back(advancedMediaPlayer); - m_vecAdvanced.push_back(advancedMediaRecorder); - m_vecAdvanced.push_back(advancedScreenCap); + m_vecAdvanced.push_back(advancedBeautyAudio); m_vecAdvanced.push_back(advancedAudioProfile); - m_vecAdvanced.push_back(advancedAudioMixing); // m_vecAdvanced.push_back(advancedAudioEffect); - m_vecAdvanced.push_back(advancedCustomVideoCapture); - m_vecAdvanced.push_back(advancedOriginalVideo); m_vecAdvanced.push_back(advancedCustomAudioCapture); m_vecAdvanced.push_back(advancedOriginalAudio); - m_vecAdvanced.push_back(advancedCustomEncrypt); - m_vecAdvanced.push_back(advancedMultiChannel); + m_vecAdvanced.push_back(advancedAudioMixing); + m_vecAdvanced.push_back(advancedCustomVideoCapture); m_vecAdvanced.push_back(advancedMultiVideoSource); m_vecAdvanced.push_back(advancedMultiVideoSourceTracks); + m_vecAdvanced.push_back(advancedOriginalVideo); + m_vecAdvanced.push_back(AdvancedLocalVideoTranscoding); + m_vecAdvanced.push_back(advancedMultiChannel); + m_vecAdvanced.push_back(advancedMediaEncrypt); + m_vecAdvanced.push_back(advancedCustomEncrypt); m_vecAdvanced.push_back(advancedPerCallTest); + m_vecAdvanced.push_back(advancedMediaPlayer); + m_vecAdvanced.push_back(advancedMediaRecorder); + m_vecAdvanced.push_back(advancedScreenCap); + m_vecAdvanced.push_back(advancedCrossChannel); + m_vecAdvanced.push_back(SpatialAudio); + m_vecAdvanced.push_back(beautyFace); + m_vecAdvanced.push_back(beautyFace2); + m_vecAdvanced.push_back(TransparentBackground); + m_vecAdvanced.push_back(RteUrlPlayer); m_vecAdvanced.push_back(advancedAudioVolume); //m_vecAdvanced.push_back(advancedReportInCall); m_vecAdvanced.push_back(advancedRegionConn); - m_vecAdvanced.push_back(advancedMediaEncrypt); - m_vecAdvanced.push_back(AdvancedLocalVideoTranscoding); - m_vecAdvanced.push_back(beautyFace); - m_vecAdvanced.push_back(beautyFace2); - m_vecAdvanced.push_back(advancedBeautyAudio); - m_vecAdvanced.push_back(SpatialAudio); + m_vecAdvanced.push_back(advancedSimulcast); + m_vecAdvanced.push_back(Multipath); m_pMultiChannelDlg = new CAgoraMultiChannelDlg(&m_staMainArea); m_pMultiChannelDlg->Create(CAgoraMultiChannelDlg::IDD); m_pMultiChannelDlg->MoveWindow(&rcWnd); - //transparent bg - m_vecAdvanced.push_back(TransparentBackground); - m_vecAdvanced.push_back(RteUrlPlayer); - m_vecAdvanced.push_back(Multipath); - m_vecAdvanced.push_back(advancedSimulcast); - //inject m_pRtmpInjectDlg = new CAgoraRtmpInjectionDlg(&m_staMainArea); m_pRtmpInjectDlg->Create(CAgoraRtmpInjectionDlg::IDD); diff --git a/windows/APIExample/APIExample/en.ini b/windows/APIExample/APIExample/en.ini index 3c70dbad8..525c346ef 100755 --- a/windows/APIExample/APIExample/en.ini +++ b/windows/APIExample/APIExample/en.ini @@ -15,38 +15,38 @@ Common.Ctrl.ClientRole=Client Role Agora.ClientRole.Broadcaster=Broadcaster Agora.ClientRole.Audience=Audience -Basic.LiveBroadcasting=LiveBroadcasting -Basic.JoinChannelVideoByToken=JoinChannelVideoByToken -Advanced.RtmpStreaming=Rtmp Streaming +Basic.LiveBroadcasting=Live Streaming +Basic.JoinChannelVideoByToken=Join a channel (Token) +Advanced.RtmpStreaming=RTMP Streaming Advanced.RtmpInject=Rtmp Inject -Advanced.Metadata=Metadata +Advanced.Metadata=Media Metadata Advanced.Beauty=Beauty -Advanced.BeautyAudio=Beauty Audio +Advanced.BeautyAudio=Voice Effects Advanced.AudioProfile=Audio Profile Advanced.AudioMixing=Audio Mixing Advanced.ScreenCap=Screen Share -Advanced.CustomVideoCapture=Custom Video Capture -Advanced.OriginalVideo=Original Video -Advanced.OriginalAudio=Original Audio -Advanced.CustomAudioCapture=Custom Audio Capture -Advanced.MediaPlayer=MediaPlayer -Advanced.MediaRecorder=MediaRecorder -Advanced.MultiChannel=Join Multi Channel -Advanced.MultiVideoSource=Multi Video Sources +Advanced.CustomVideoCapture=Custom Video Source +Advanced.OriginalVideo=Raw Video Data +Advanced.OriginalAudio=Raw Audio Data +Advanced.CustomAudioCapture=Custom Audio Source (PCM) +Advanced.MediaPlayer=Media Player +Advanced.MediaRecorder=Media Recorder +Advanced.MultiChannel=Join Multiple Channels +Advanced.MultiVideoSource=Multiple Video Sources Advanced.MultiVideoSourceTracks=Multi Video Source Tracks -Advanced.MediaEncrypt=Media Encrypt -Advanced.CustomEncrypt=Custom Encrypt -Advanced.MediaPlayer=MediaPlayer +Advanced.MediaEncrypt=Stream Encryption +Advanced.CustomEncrypt=Custom Encryption +Advanced.MediaPlayer=Media Player LiveBroadcasting.Ctrl.Persons=Persons Advanced.AudioEffect=Audio Effect -Advanced.MultiChannel=Multi Channel -Advanced.PerCallTest=PerCallTest -Advanced.AudioVolume=AudioVolume +Advanced.MultiChannel=Join Multiple Channels +Advanced.PerCallTest=Pre-call Test +Advanced.AudioVolume=Audio Volume Advanced.ReportInCall=ReportInCall Advanced.RegionConn=Region Connection -Advanced.CrossChannel=CrossChannel +Advanced.CrossChannel=Media Channel Relay Advanced.AudioEffect=Audio Effect Advanced.LocalVideoTranscoding=Local Video Transcoding LiveBroadcasting.Ctrl.Persons=Persons @@ -281,8 +281,8 @@ LiveBroadcasting.Ctrl.FristFrameTip = Tip LiveBroadcasting.Ctrl.FristFrameTipContent = It cannot be turned off after it is turned on, and it takes effect only when both the main and auxiliary ends are turned on. Is it open? LiveBroadcasting.Ctrl.VideoImage = Video Image -Beauty.Face = BeautyFace -Beauty.Face.2 = BeautyFace2.0 +Beauty.Face = Beauty Face +Beauty.Face.2 = Beauty 2.0 BeautyAudio.Ctrl.ReverbPreSet=ReverbPreSet BeautyAudio.Ctrl.SetAudioChange=SetAudioChange @@ -307,8 +307,8 @@ Metadata.Video=Video Metadata Metadata.Audio=Audio Metadata Metadata.Send=Send -TransparentBackground=TransparentBackground -RteUrlPlayer=RTE player +TransparentBackground=Transparent Rendering +RteUrlPlayer=URL Streaming (RTE Player) VideoEnhance=Video Enhance From 0448b60ba4e77b5630deeba504bc7f4a3480de76 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 17 Sep 2026 12:01:43 +0800 Subject: [PATCH 44/49] refactor(macos): modernize screen capture and require macOS 12 Use RTC SDK screen enumeration for local video transcoding and remove WindowsCenter. Adopt @main and align the app, Pods, and documentation on a macOS 12 deployment target. --- macOS/APIExample.xcodeproj/project.pbxproj | 8 +- macOS/APIExample/AppDelegate.swift | 2 +- macOS/APIExample/Common/WindowsCenter.swift | 251 ------------------ .../LocalVideoTranscoding.swift | 18 +- macOS/ARCHITECTURE.md | 2 +- macOS/Podfile | 8 +- macOS/README.md | 3 +- macOS/README.zh.md | 3 +- 8 files changed, 24 insertions(+), 271 deletions(-) delete mode 100644 macOS/APIExample/Common/WindowsCenter.swift diff --git a/macOS/APIExample.xcodeproj/project.pbxproj b/macOS/APIExample.xcodeproj/project.pbxproj index f19574923..e96ccef99 100644 --- a/macOS/APIExample.xcodeproj/project.pbxproj +++ b/macOS/APIExample.xcodeproj/project.pbxproj @@ -39,7 +39,6 @@ 034C62712525A35800296ECF /* StreamEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62702525A35700296ECF /* StreamEncryption.swift */; }; 034C62772525C68D00296ECF /* AgoraCustomEncryption.mm in Sources */ = {isa = PBXBuildFile; fileRef = 034C62752525C68C00296ECF /* AgoraCustomEncryption.mm */; }; 034C627C2526C43900296ECF /* ScreenShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C627A2526C43900296ECF /* ScreenShare.swift */; }; - 034C62872528255F00296ECF /* WindowsCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62862528255F00296ECF /* WindowsCenter.swift */; }; 034C628A25282D5D00296ECF /* JoinMultiChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C628925282D5D00296ECF /* JoinMultiChannel.swift */; }; 034C62912528327800296ECF /* ChannelMediaRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C628F2528327800296ECF /* ChannelMediaRelay.swift */; }; 034C62932528474D00296ECF /* StatisticsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62922528474D00296ECF /* StatisticsInfo.swift */; }; @@ -211,7 +210,6 @@ 034C62752525C68C00296ECF /* AgoraCustomEncryption.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AgoraCustomEncryption.mm; sourceTree = ""; }; 034C62762525C68C00296ECF /* AgoraCustomEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AgoraCustomEncryption.h; sourceTree = ""; }; 034C627A2526C43900296ECF /* ScreenShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShare.swift; sourceTree = ""; }; - 034C62862528255F00296ECF /* WindowsCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowsCenter.swift; sourceTree = ""; }; 034C628925282D5D00296ECF /* JoinMultiChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinMultiChannel.swift; sourceTree = ""; }; 034C628F2528327800296ECF /* ChannelMediaRelay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMediaRelay.swift; sourceTree = ""; }; 034C62922528474D00296ECF /* StatisticsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticsInfo.swift; sourceTree = ""; }; @@ -649,7 +647,6 @@ 5770E2C0258C580E00812A80 /* Component */, 57887A7F25885FC2006E962A /* Settings */, 034C62922528474D00296ECF /* StatisticsInfo.swift */, - 034C62862528255F00296ECF /* WindowsCenter.swift */, 034C626325257EA600296ECF /* GlobalSettings.swift */, 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */, 034C62742525C68C00296ECF /* CustomEncryption */, @@ -1265,7 +1262,6 @@ 03B12DA4250E8F7F00E55818 /* AgoraExtension.swift in Sources */, 0336A1C725034F4700D61B7F /* AudioWriteToFile.m in Sources */, 671BD67927E09E3B0076D5E1 /* AgoraPcmSourcePush.swift in Sources */, - 034C62872528255F00296ECF /* WindowsCenter.swift in Sources */, 034C62912528327800296ECF /* ChannelMediaRelay.swift in Sources */, 033A9F9E252EA86A00BC26E1 /* CustomVideoSourcePush.swift in Sources */, 8BD4AE73272513FF00E95B87 /* SimpleFilter.swift in Sources */, @@ -1663,7 +1659,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -1719,7 +1715,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; + MACOSX_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; diff --git a/macOS/APIExample/AppDelegate.swift b/macOS/APIExample/AppDelegate.swift index 026ae9bbb..1e76dcc66 100644 --- a/macOS/APIExample/AppDelegate.swift +++ b/macOS/APIExample/AppDelegate.swift @@ -8,7 +8,7 @@ import Cocoa -@NSApplicationMain +@main class AppDelegate: NSObject, NSApplicationDelegate { diff --git a/macOS/APIExample/Common/WindowsCenter.swift b/macOS/APIExample/Common/WindowsCenter.swift deleted file mode 100644 index 704b958a4..000000000 --- a/macOS/APIExample/Common/WindowsCenter.swift +++ /dev/null @@ -1,251 +0,0 @@ -// -// WindowsCenter.swift -// AgoraVideoCall -// -// Created by GongYuhua on 6/14/16. -// Copyright © 2016 Agora. All rights reserved. -// - -import CoreGraphics - -#if os(iOS) -import UIKit -#else -import Cocoa -#endif - -enum WindowType: Int { - case window, screen -} - -enum ApplicationType { - case web, ppt, keynote, word, pages, preview, other -} - -class Window { - fileprivate(set) var type: WindowType = .window - fileprivate(set) var id: UInt32 = 0 - fileprivate(set) var name: String! - fileprivate(set) var image: NSImage! - fileprivate(set) var width: CGFloat = 0 - fileprivate(set) var height: CGFloat = 0 - var size: CGSize { - return CGSize(width: width, height: height) - } - - init?(windowDic: NSDictionary) { - if let layerNumber = windowDic[Window.convertCFString(kCGWindowLayer)] { - let cfNumber = layerNumber as! CFNumber - let layer = Window.convertCFNumber(cfNumber) - guard layer == 0 else { - return nil - } - } - - if let alphaNumber = windowDic[Window.convertCFString(kCGWindowAlpha)] { - let cfNumber = alphaNumber as! CFNumber - let alpha = Window.convertCFNumber(cfNumber) - if alpha == 0 { - return nil - } - } - - if windowDic[Window.convertCFString(kCGWindowName)] == nil { - return nil - } - - guard let idNumber = windowDic[Window.convertCFString(kCGWindowNumber)] else { - return nil - } - - let cfNumber = idNumber as! CFNumber - let id = Window.convertCFNumber(cfNumber) - - var name: String? - if let ownerName = windowDic[Window.convertCFString(kCGWindowOwnerName)] { - let cfName: CFString = ownerName as! CFString - name = Window.convertCFString(cfName) - if name == "Agora Video Call" { - return nil - } - } - - guard let image = Window.image(of: id) else { - return nil - } - - self.id = id - self.name = name ?? "Unknown" - self.image = image - self.width = image.size.width - self.height = image.size.height - self.type = .window - } - - init?(screenId: CGDirectDisplayID, name: String) { - self.name = name - self.id = screenId - self.type = .screen - guard let image = Window.imageOfScreenId(self.id) else { - return - } - self.image = image - self.width = image.size.width - self.height = image.size.height - } - - fileprivate init() {} - - static func fullScreenWindow() -> Window { - let window = Window() - window.name = "Full Screen" - window.image = imageOfFullScreen() - if let main = NSScreen.screens.first { - let scale = main.backingScaleFactor - window.width = main.frame.size.width * scale - window.height = main.frame.size.height * scale - } - - return window - } - - static func image(of windowId: CGWindowID) -> NSImage? { - if let screenShot = CGWindowListCreateImage(CGRect.null, .optionIncludingWindow, CGWindowID(windowId), CGWindowImageOption.boundsIgnoreFraming) { - let bitmapRep = NSBitmapImageRep(cgImage: screenShot) - let image = NSImage() - image.addRepresentation(bitmapRep) - - if image.size.width == 1 { - return nil - } else { - return image - } - } else { - return nil - } - } - - fileprivate static func imageOfScreenId(_ screenId: CGDirectDisplayID) -> NSImage? { - if let screenShot = CGDisplayCreateImage(screenId) { - let bitmapRep = NSBitmapImageRep(cgImage: screenShot) - let image = NSImage() - image.addRepresentation(bitmapRep) - - if image.size.width == 1 { - return nil - } else { - return image - } - } else { - return nil - } - } - - fileprivate static func imageOfFullScreen() -> NSImage { - if let screenShot = CGWindowListCreateImage(CGRect.infinite, .optionOnScreenOnly, CGWindowID(0), CGWindowImageOption()) { - let bitmapRep = NSBitmapImageRep(cgImage: screenShot) - let image = NSImage() - image.addRepresentation(bitmapRep) - return image - } else { - return NSImage() - } - } -} - -class WindowList { - var items = [Window]() - - func getList() { - var list = [Window]() - - var webList = [Window]() - var pptList = [Window]() - var keynoteList = [Window]() - var wordList = [Window]() - var pagesList = [Window]() - var previewList = [Window]() - var otherList = [Window]() - - // add screens - let screens = NSScreen.screens - for (index, screen) in screens.enumerated() { - guard let screenId = screen.deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID else { - continue - } - if let window = Window(screenId: screenId, name: "Screen \(index + 1)") { - list.append(window) - } - } - - // add windows - if let windowDicCFArray = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], 0) { - let windowDicList = windowDicCFArray as NSArray - - for windowElement in windowDicList { - let windowDic = windowElement - if let windowDic = windowDic as? NSDictionary { - if let window = Window(windowDic: windowDic) { - let appType = typeOfApplication(with: window.name) - switch appType { - case .web: - webList.append(window) - case .ppt: - pptList.append(window) - case .keynote: - keynoteList.append(window) - case .word: - wordList.append(window) - case .pages: - pagesList.append(window) - case .preview: - previewList.append(window) - case .other: - otherList.append(window) - } - } - } - } - } - let temp = webList + pptList + keynoteList + wordList - list += temp + pagesList + previewList + otherList - - self.items = list - } - - private func typeOfApplication(with name: String) -> ApplicationType { - if name.contains("Google Chrome") || name.contains("Safari") { - return .web - } else if name.contains("PowerPoint") { - return .ppt - } else if name.contains("Microsoft") { - return .word - } else if name.contains("Keynote") { - return .keynote - } else if name.contains("Pages") { - return .pages - } else if name.contains("Preview") { - return .preview - } else { - return .other - } - } - - private func isHighPriortyWindow(with name: String) -> Bool { - return (name.contains("Microsoft") && !name.contains("Outlook") && !name.contains("Teams")) - || name.contains("Google Chrome") - } -} - -extension Window { - class func convertCFString(_ cfString: CFString) -> String { - let string = cfString as NSString - return string as String - - } - - class func convertCFNumber(_ cfNumber: CFNumber) -> UInt32 { - let number = cfNumber as NSNumber - return number.uint32Value - } -} diff --git a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift index 225546f5b..810b9083e 100644 --- a/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift +++ b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift @@ -21,13 +21,12 @@ class LocalVideoTranscoding: BaseViewController { layoutVideos(2) } - var windowManager: WindowList = WindowList() - var windowlist:[Window] = [], screenlist:[Window] = [] + var screenlist: [AgoraScreenCaptureSourceInfo] = [] /** --- Screen Picker --- */ @IBOutlet weak var selectScreenPicker: Picker! - var selectedScreen: Window? { + var selectedScreen: AgoraScreenCaptureSourceInfo? { let index = self.selectScreenPicker.indexOfSelectedItem if index >= 0 && index < screenlist.count { return screenlist[index] @@ -36,9 +35,14 @@ class LocalVideoTranscoding: BaseViewController { } } func initSelectScreenPicker() { - screenlist = windowManager.items.filter({$0.type == .screen}) + let sources = agoraKit.getScreenCaptureSources(withThumbSize: .zero, iconSize: .zero, includeScreen: true) + screenlist = sources?.filter { $0.type == .screen } ?? [] selectScreenPicker.label.stringValue = "Screen Share".localized - selectScreenPicker.picker.addItems(withTitles: screenlist.map {"\($0.name ?? "Unknown")(\($0.id))"}) + selectScreenPicker.picker.removeAllItems() + selectScreenPicker.picker.addItems(withTitles: screenlist.enumerated().map { index, source in + let name = source.sourceName.isEmpty ? "Screen \(index + 1)" : source.sourceName + return "\(name)(\(source.sourceId))" + }) } var isScreenSharing: Bool = false { didSet { @@ -109,7 +113,7 @@ class LocalVideoTranscoding: BaseViewController { params.highLightWidth = 5 params.highLightColor = .green params.highLighted = true - let result = agoraKit.startScreenCapture(byDisplayId: UInt32(screen.id), regionRect: .zero, captureParams: params) + let result = agoraKit.startScreenCapture(byDisplayId: UInt32(screen.sourceId), regionRect: .zero, captureParams: params) if result == 0 { isScreenSharing = true @@ -248,8 +252,6 @@ class LocalVideoTranscoding: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - // prepare window manager and list - windowManager.getList() // Do view setup here. let config = AgoraRtcEngineConfig() config.appId = KeyCenter.AppId diff --git a/macOS/ARCHITECTURE.md b/macOS/ARCHITECTURE.md index a911585a0..cbfd2078b 100644 --- a/macOS/ARCHITECTURE.md +++ b/macOS/ARCHITECTURE.md @@ -95,7 +95,7 @@ All examples share utilities from `APIExample/Common/`: | CustomVideoSourcePushMulti | `Examples/Advanced/CustomVideoSourcePushMulti/` | `setExternalVideoSource()`, `pushVideoFrame()` with multiple sources | Multiple custom video sources | | JoinMultiChannel | `Examples/Advanced/JoinMultiChannel/` | `createRtcChannel()`, `joinChannel()` on multiple channels | Join and manage multiple channels simultaneously | | LiveStreaming | `Examples/Advanced/LiveStreaming/` | `setClientRole()`, `addVideoWatermark()` with `WatermarkConfig`, `startRtmpStreamWithTranscoding()`, `stopRtmpStream()` | Live streaming with watermark configuration and RTMP transcoding | -| LocalVideoTranscoding | `Examples/Advanced/LocalVideoTranscoding/` | `startLocalVideoTranscoding()`, `updateLocalTranscodingConfig()`, `stopLocalVideoTranscoding()` | Local video transcoding and composition | +| LocalVideoTranscoding | `Examples/Advanced/LocalVideoTranscoding/` | `getScreenCaptureSources()`, `startLocalVideoTranscoder()`, `updateLocalTranscoderConfiguration()`, `stopLocalVideoTranscoder()` | Local video transcoding and composition | | MediaPlayer | `Examples/Advanced/MediaPlayer/` | `createMediaPlayer()`, `open()`, `play()`, `pause()`, `stop()` | Media file playback and control | | MultiCameraSourece | `Examples/Advanced/MultiCameraSourece/` | `enumerateDevices()`, `setDevice()` with multiple cameras | Multiple camera source selection | | Multipath | `Examples/Advanced/Multipath/` | `enableMultipath()`, `setMultipathConfig()` | Multipath redundancy for reliability | diff --git a/macOS/Podfile b/macOS/Podfile index 39dace798..f0e61fc21 100644 --- a/macOS/Podfile +++ b/macOS/Podfile @@ -1,4 +1,5 @@ -# Uncomment the next line to define a global platform for your project +macos_deployment_target = '12.0' +platform :osx, macos_deployment_target def common_pods @@ -24,7 +25,10 @@ post_install do |installer| # system("sh .download_script.sh 4.7.0 true") installer.pods_project.targets.each do |target| target.build_configurations.each do |config| - config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.13' + deployment_target = config.build_settings['MACOSX_DEPLOYMENT_TARGET'] + if deployment_target.nil? || Gem::Version.new(deployment_target) < Gem::Version.new(macos_deployment_target) + config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = macos_deployment_target + end end end end diff --git a/macOS/README.md b/macOS/README.md index 44a26a884..5eedc3a92 100644 --- a/macOS/README.md +++ b/macOS/README.md @@ -7,7 +7,8 @@ This project presents you a set of API examples to help you understand how to us ## Prerequisites -- Xcode 13.0+ +- macOS 12.0 or later +- Xcode 13.1+ ## Quick Start diff --git a/macOS/README.zh.md b/macOS/README.zh.md index eb433d3d5..2863bc596 100644 --- a/macOS/README.zh.md +++ b/macOS/README.zh.md @@ -6,7 +6,8 @@ ## 环境准备 -- Xcode 13.0 或更高版本 +- macOS 12.0 或更高版本 +- Xcode 13.1 或更高版本 ## 快速开始 From 481ac658817e815a3d26768be014bf50fd1061e6 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 17 Sep 2026 14:59:01 +0800 Subject: [PATCH 45/49] fix(ios): adopt scene lifecycle while preserving iOS 12 Use storyboard scene windows on iOS 13 and later, retaining AppDelegate startup on iOS 12 and the original SwiftUI iOS 14 deployment target. Resolve UI presentation through scene windows and keep Floaty log controls in the navigation hierarchy, with local safe-area and share-sheet anchors. Document lifecycle ownership, deployment and Xcode requirements, independent workspaces, and the audio project's lack of extension targets. Validation: native deployment-target typechecks; three non-audio app builds per repository using the existing local SDK; Scene lifecycle probes and Floaty component regressions on iPhone/iPad simulators; repository policy and AI asset checks. Audio SDK builds and actual iOS 12/RTC device checks remain pending unavailable dependencies and devices. --- iOS/AGENTS.md | 1 + .../project.pbxproj | 4 ++ .../APIExample-Audio/AppDelegate.swift | 11 ++- .../Common/AgoraExtension.swift | 2 +- .../Common/LogViewController.swift | 70 +++++++++++++++++++ .../Common/NetworkManager/ToastView.swift | 20 +++--- .../APIExample-Audio/Info.plist | 21 +++++- .../APIExample-Audio/SceneDelegate.swift | 7 ++ .../APIExample-Audio/ViewController.swift | 26 ++----- iOS/APIExample-Audio/ARCHITECTURE.md | 17 +++++ iOS/APIExample-Audio/README.md | 3 +- iOS/APIExample-Audio/README.zh.md | 3 +- .../APIExample-OC.xcodeproj/project.pbxproj | 8 ++- iOS/APIExample-OC/APIExample-OC/AppDelegate.m | 7 ++ .../Common/Views/AlertManager.swift | 4 +- .../Common/Views/ToastView.swift | 20 +++--- iOS/APIExample-OC/APIExample-OC/Info.plist | 19 +++++ .../APIExample-OC/SceneDelegate.h | 9 +++ .../APIExample-OC/SceneDelegate.m | 4 ++ iOS/APIExample-OC/ARCHITECTURE.md | 12 ++++ iOS/APIExample-OC/README.md | 3 +- iOS/APIExample-OC/README.zh.md | 3 +- .../Common/AgoraExtension.swift | 2 +- .../Common/NetworkManager/ToastView.swift | 22 +++--- .../APIExample-SwiftUI/ContentView.swift | 4 +- iOS/APIExample-SwiftUI/ARCHITECTURE.md | 6 ++ .../APIExample.xcodeproj/project.pbxproj | 4 ++ iOS/APIExample/APIExample/AppDelegate.swift | 11 ++- .../APIExample/Common/AgoraExtension.swift | 2 +- .../APIExample/Common/AlertManager.swift | 6 +- .../APIExample/Common/LogViewController.swift | 70 +++++++++++++++++++ .../Common/NetworkManager/ToastView.swift | 20 +++--- iOS/APIExample/APIExample/Info.plist | 21 +++++- iOS/APIExample/APIExample/SceneDelegate.swift | 7 ++ .../APIExample/ViewController.swift | 28 ++------ iOS/APIExample/ARCHITECTURE.md | 17 +++++ iOS/APIExample/README.md | 3 +- iOS/APIExample/README.zh.md | 3 +- iOS/ARCHITECTURE.md | 23 +++++- 39 files changed, 412 insertions(+), 111 deletions(-) create mode 100644 iOS/APIExample-Audio/APIExample-Audio/SceneDelegate.swift create mode 100644 iOS/APIExample-OC/APIExample-OC/SceneDelegate.h create mode 100644 iOS/APIExample-OC/APIExample-OC/SceneDelegate.m create mode 100644 iOS/APIExample/APIExample/SceneDelegate.swift diff --git a/iOS/AGENTS.md b/iOS/AGENTS.md index cca86ab28..2072d9a13 100644 --- a/iOS/AGENTS.md +++ b/iOS/AGENTS.md @@ -24,6 +24,7 @@ SDK version: each project's `Podfile` specifies the version. ## Architecture Red Lines - Do NOT share source files, storyboards, or SDK dependencies between projects +- Preserve the existing minimum iOS versions and availability checks; see [Deployment Targets and Lifecycle](ARCHITECTURE.md#deployment-targets-and-lifecycle). - Do NOT add video rendering APIs (`enableVideo`, `setupLocalVideo`) to `APIExample-Audio/` - Do NOT call SDK APIs on a background thread without dispatching UI updates to the main thread - Do NOT commit `KeyCenter.swift` / `KeyCenter.m` with real App IDs or certificates diff --git a/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj b/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj index cdd261fb7..06fbaebeb 100644 --- a/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj +++ b/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 03BEED0B251C4446005E78F4 /* audiomixing.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 03BEED0A251C4446005E78F4 /* audiomixing.mp3 */; }; 03BEED0D251CAB9C005E78F4 /* audioeffect.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 03BEED0C251CAB9C005E78F4 /* audioeffect.mp3 */; }; 03D13BD02448758900B599B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D13BCF2448758900B599B3 /* AppDelegate.swift */; }; + 6B1F9C169FBD07EE9D70CFCD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */; }; 03D13BD42448758900B599B3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D13BD32448758900B599B3 /* ViewController.swift */; }; 03D13BD72448758900B599B3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03D13BD52448758900B599B3 /* Main.storyboard */; }; 03D13BD92448758B00B599B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03D13BD82448758B00B599B3 /* Assets.xcassets */; }; @@ -124,6 +125,7 @@ 03BEED0C251CAB9C005E78F4 /* audioeffect.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = audioeffect.mp3; sourceTree = ""; }; 03D13BCC2448758900B599B3 /* APIExample-Audio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "APIExample-Audio.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 03D13BCF2448758900B599B3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 03D13BD32448758900B599B3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 03D13BD62448758900B599B3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 03D13BD82448758B00B599B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -466,6 +468,7 @@ children = ( 03D13BDD2448758B00B599B3 /* Info.plist */, 03D13BCF2448758900B599B3 /* AppDelegate.swift */, + BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */, 03D13BD82448758B00B599B3 /* Assets.xcassets */, 03D13BDA2448758B00B599B3 /* LaunchScreen.storyboard */, 033A9F2C252D737900BC26E1 /* Localizable.strings */, @@ -694,6 +697,7 @@ 034C625E2524A06800296ECF /* VoiceChanger.swift in Sources */, E7A49CBC29023BD300F06DD4 /* AlertManager.swift in Sources */, 03D13BD02448758900B599B3 /* AppDelegate.swift in Sources */, + 6B1F9C169FBD07EE9D70CFCD /* SceneDelegate.swift in Sources */, A7847F922458062900469187 /* StatisticsInfo.swift in Sources */, 03BCEC762449EB5000ED7177 /* LogViewController.swift in Sources */, E74877CA28A2611C00CA2F58 /* ToastView.swift in Sources */, diff --git a/iOS/APIExample-Audio/APIExample-Audio/AppDelegate.swift b/iOS/APIExample-Audio/APIExample-Audio/AppDelegate.swift index b30a136d5..74bcad1a9 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/AppDelegate.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/AppDelegate.swift @@ -8,13 +8,20 @@ import UIKit -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + if #available(iOS 13.0, *) { + // UIKit creates the window using the scene storyboard configuration. + } else { + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() + self.window = window + window.makeKeyAndVisible() + } return true } diff --git a/iOS/APIExample-Audio/APIExample-Audio/Common/AgoraExtension.swift b/iOS/APIExample-Audio/APIExample-Audio/Common/AgoraExtension.swift index c70221b87..95aa77815 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Common/AgoraExtension.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Common/AgoraExtension.swift @@ -305,7 +305,7 @@ extension UIAlertController { extension UIApplication { /// The top most view controller static var topMostViewController: UIViewController? { - return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController + return UIViewController.keyWindow?.rootViewController?.visibleViewController } } diff --git a/iOS/APIExample-Audio/APIExample-Audio/Common/LogViewController.swift b/iOS/APIExample-Audio/APIExample-Audio/Common/LogViewController.swift index ae52c2ecd..9b5858c10 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Common/LogViewController.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Common/LogViewController.swift @@ -8,6 +8,76 @@ import UIKit import Foundation +import Floaty + +// Floaty.global creates a window without a scene. Keep this button in the app's view hierarchy. +final class LogFloatingButton: Floaty { + override init() { + super.init() + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + func show(in container: UIView) { + if superview !== container { + container.addSubview(self) + } + container.bringSubviewToFront(self) + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + updateSafeAreaPadding() + } + + override func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + updateSafeAreaPadding() + } + + private func configure() { + // Floaty 4.2 reads AppDelegate.window for safe-area placement, which is nil with scenes. + relativeToSafeArea = false + isDraggable = true + addItem(title: "Send Logs") { [weak self] _ in + self?.shareLogs() + } + addItem(title: "Clean Up") { _ in + LogUtils.cleanUp() + } + } + + private func updateSafeAreaPadding() { + let insets = superview?.safeAreaInsets ?? .zero + let horizontal = 14 + insets.right + let vertical = 14 + insets.bottom + if paddingX != horizontal { paddingX = horizontal } + if paddingY != vertical { paddingY = vertical } + } + + private func shareLogs() { + guard var presenter = window?.rootViewController else { return } + while let presented = presenter.presentedViewController { + presenter = presented + } + guard !(presenter is UIActivityViewController), !presenter.isBeingDismissed else { return } + + LogUtils.writeAppLogsToDisk() + let activity = UIActivityViewController( + activityItems: [URL(fileURLWithPath: LogUtils.logFolder(), isDirectory: true)], + applicationActivities: nil + ) + activity.modalPresentationStyle = .popover + activity.popoverPresentationController?.sourceView = self + activity.popoverPresentationController?.sourceRect = bounds + presenter.present(activity, animated: true) + } +} + enum LogLevel { case info, warning, error diff --git a/iOS/APIExample-Audio/APIExample-Audio/Common/NetworkManager/ToastView.swift b/iOS/APIExample-Audio/APIExample-Audio/Common/NetworkManager/ToastView.swift index 7e346d8ae..7fb5cfa6c 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Common/NetworkManager/ToastView.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/Common/NetworkManager/ToastView.swift @@ -196,17 +196,17 @@ class ToastView: UIView { } extension UIViewController { static var keyWindow: UIWindow? { - // Get connected scenes if #available(iOS 13.0, *) { - return UIApplication.shared.connectedScenes - // Keep only active scenes, onscreen and visible to the user - .filter { $0.activationState == .foregroundActive } - // Keep only the first `UIWindowScene` - .first(where: { $0 is UIWindowScene }) - // Get its associated windows - .flatMap({ $0 as? UIWindowScene })?.windows - // Finally, keep only the key window - .first(where: \.isKeyWindow) + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + // Prefer the active scene; startup and system dialogs can temporarily make it inactive. + for state in [UIScene.ActivationState.foregroundActive, .foregroundInactive] { + if let window = scenes.filter({ $0.activationState == state }) + .flatMap({ $0.windows }) + .first(where: \.isKeyWindow) { + return window + } + } + return nil } else { return UIApplication.shared.keyWindow } diff --git a/iOS/APIExample-Audio/APIExample-Audio/Info.plist b/iOS/APIExample-Audio/APIExample-Audio/Info.plist index 7ad85ab0a..b3c16910a 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/Info.plist +++ b/iOS/APIExample-Audio/APIExample-Audio/Info.plist @@ -37,8 +37,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 @@ -56,5 +54,24 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + diff --git a/iOS/APIExample-Audio/APIExample-Audio/SceneDelegate.swift b/iOS/APIExample-Audio/APIExample-Audio/SceneDelegate.swift new file mode 100644 index 000000000..220944d29 --- /dev/null +++ b/iOS/APIExample-Audio/APIExample-Audio/SceneDelegate.swift @@ -0,0 +1,7 @@ +import UIKit + +@available(iOS 13.0, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + // UIKit creates this window from Main.storyboard in the scene configuration. + var window: UIWindow? +} diff --git a/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift b/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift index 84375eb77..c919a70c7 100644 --- a/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift +++ b/iOS/APIExample-Audio/APIExample-Audio/ViewController.swift @@ -7,7 +7,6 @@ // import UIKit -import Floaty struct MenuSection { var name: String @@ -39,25 +38,14 @@ class ViewController: AGViewController { MenuItem(name: "Spatial Audio".localized, storyboard: "SpatialAudio", controller: "SpatialAudio"), ]), ] - override func viewDidLoad() { - super.viewDidLoad() - Floaty.global.button.addItem(title: "Send Logs", handler: {item in - LogUtils.writeAppLogsToDisk() - let activity = UIActivityViewController(activityItems: [NSURL(fileURLWithPath: LogUtils.logFolder(), isDirectory: true)], applicationActivities: nil) - activity.modalPresentationStyle = .popover - if UIDevice.current.userInterfaceIdiom == .pad { - activity.popoverPresentationController?.sourceView = Floaty.global.button - } - self.present(activity, animated: true, completion: nil) - }) - - Floaty.global.button.addItem(title: "Clean Up", handler: {item in - LogUtils.cleanUp() - }) - Floaty.global.button.isDraggable = true - Floaty.global.show() + private let logButton = LogFloatingButton() + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // The navigation view survives pushes to individual examples. + logButton.show(in: navigationController?.view ?? view) } - + @IBAction func onSettings(_ sender:UIBarButtonItem) { let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) guard let settingsViewController = storyBoard.instantiateViewController(withIdentifier: "settings") as? SettingsViewController else { return } diff --git a/iOS/APIExample-Audio/ARCHITECTURE.md b/iOS/APIExample-Audio/ARCHITECTURE.md index 4a46642ae..48f5d3d3d 100644 --- a/iOS/APIExample-Audio/ARCHITECTURE.md +++ b/iOS/APIExample-Audio/ARCHITECTURE.md @@ -1,5 +1,21 @@ # ARCHITECTURE.md — APIExample-Audio +## Application Lifecycle + +The app retains iOS 12 support. On iOS 12, `AppDelegate` creates the +window and loads the initial controller from `Main.storyboard`. On iOS 13 and later, +`Info.plist` declares a single application scene: UIKit loads `Main.storyboard` and +assigns its window to `SceneDelegate`. Scene declarations and window APIs are guarded +by iOS availability checks; the window helper retains its iOS 12 fallback. +Do not rebuild the root controller on scene activation or end RTC sessions merely because +the scene enters the background. Use a build toolchain that supports the deployment target; +adopting the scene lifecycle does not require raising the minimum iOS version. + +`ViewController` owns a `LogFloatingButton` defined in `Common/LogViewController.swift`. +It is attached to the navigation container after appearance so it remains available in +example pages on both iOS 12 and later. Its safe-area placement and share sheet use its +own view/window hierarchy; do not use `Floaty.global`, which creates a window without a scene. + ## Case Index | Case | Path | Key APIs | Description | @@ -23,6 +39,7 @@ APIExample-Audio/ ├── Podfile # CocoaPods dependencies (AgoraAudio_iOS, Floaty, AGEVideoLayout) └── APIExample-Audio/ ├── AppDelegate.swift + ├── SceneDelegate.swift # Window owned by the application scene ├── ViewController.swift # Root menu controller — MenuItem registration lives here ├── Info.plist ├── APIExample.entitlements diff --git a/iOS/APIExample-Audio/README.md b/iOS/APIExample-Audio/README.md index 72b200780..9ac15aa51 100644 --- a/iOS/APIExample-Audio/README.md +++ b/iOS/APIExample-Audio/README.md @@ -13,7 +13,8 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ## Prerequisites -- Xcode 13.0+ +- iOS 12.0 or later +- Xcode 13.0 or later with support for an iOS 12 deployment target ([Xcode support](https://developer.apple.com/support/xcode/)) - Physical iOS device (iPhone or iPad) - iOS simulator is NOT supported diff --git a/iOS/APIExample-Audio/README.zh.md b/iOS/APIExample-Audio/README.zh.md index 3e4872fee..b757a1891 100644 --- a/iOS/APIExample-Audio/README.zh.md +++ b/iOS/APIExample-Audio/README.zh.md @@ -14,7 +14,8 @@ ## 环境准备 -- Xcode 13.0 或更高版本 +- iOS 12.0 或更高版本 +- Xcode 13.0 或更高版本,且该版本支持将最低部署目标设为 iOS 12([Xcode 支持范围](https://developer.apple.com/support/xcode/)) - iPhone 或 iPad 真机 - 不支持 iOS 模拟器 diff --git a/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj b/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj index 2b2a2baa0..a358f4740 100644 --- a/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj +++ b/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ C2C764A5CC78FAC392BE5584 /* Pods_Agora_ScreenShare_Extension_OC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27A6720DD8507EB2C642C0FD /* Pods_Agora_ScreenShare_Extension_OC.framework */; }; DD8A1F7F2CA50749001CEC51 /* AgoraPCMPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = DD8A1F7E2CA50749001CEC51 /* AgoraPCMPlayer.m */; }; E70ADDF12A5D004F009947CF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E70ADDF02A5D004F009947CF /* AppDelegate.m */; }; + E07E25502B2E7E4203C5B06A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E08D17F052D0A72FB0F67299 /* SceneDelegate.m */; }; E70ADDF72A5D004F009947CF /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E70ADDF62A5D004F009947CF /* ViewController.m */; }; E70ADDFA2A5D004F009947CF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E70ADDF82A5D004F009947CF /* Main.storyboard */; }; E70ADDFC2A5D0050009947CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E70ADDFB2A5D0050009947CF /* Assets.xcassets */; }; @@ -198,7 +199,9 @@ E22416FC36DE1212EE1E1760 /* Pods-Agora-ScrrenShare-Extension-OC.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Agora-ScrrenShare-Extension-OC.release.xcconfig"; path = "Target Support Files/Pods-Agora-ScrrenShare-Extension-OC/Pods-Agora-ScrrenShare-Extension-OC.release.xcconfig"; sourceTree = ""; }; E70ADDEC2A5D004F009947CF /* APIExample-OC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "APIExample-OC.app"; sourceTree = BUILT_PRODUCTS_DIR; }; E70ADDEF2A5D004F009947CF /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 1DE07CD517FCB9D0AF085070 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; E70ADDF02A5D004F009947CF /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + E08D17F052D0A72FB0F67299 /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; E70ADDF52A5D004F009947CF /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; E70ADDF62A5D004F009947CF /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; E70ADDFB2A5D0050009947CF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -462,7 +465,9 @@ E70ADE0B2A5D00B1009947CF /* Common */, E70ADE0C2A5D00C0009947CF /* Resources */, E70ADDEF2A5D004F009947CF /* AppDelegate.h */, + 1DE07CD517FCB9D0AF085070 /* SceneDelegate.h */, E70ADDF02A5D004F009947CF /* AppDelegate.m */, + E08D17F052D0A72FB0F67299 /* SceneDelegate.m */, E70ADDF52A5D004F009947CF /* ViewController.h */, E70ADDF62A5D004F009947CF /* ViewController.m */, E70ADDF82A5D004F009947CF /* Main.storyboard */, @@ -1313,6 +1318,7 @@ E70ADEE62A6A71FE009947CF /* ExternalAudio.mm in Sources */, E70ADDF72A5D004F009947CF /* ViewController.m in Sources */, E70ADDF12A5D004F009947CF /* AppDelegate.m in Sources */, + E07E25502B2E7E4203C5B06A /* SceneDelegate.m in Sources */, E70ADEEB2A6A727E009947CF /* UIColor+CSRGB.m in Sources */, E70ADE7A2A60F214009947CF /* AlertManager.swift in Sources */, E70ADE402A5D4D05009947CF /* JSONObject.swift in Sources */, @@ -1624,7 +1630,6 @@ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Request Mic Access"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; @@ -1666,7 +1671,6 @@ INFOPLIST_KEY_NSMicrophoneUsageDescription = "Request Mic Access"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; diff --git a/iOS/APIExample-OC/APIExample-OC/AppDelegate.m b/iOS/APIExample-OC/APIExample-OC/AppDelegate.m index 61b6bc6ed..00c87a196 100644 --- a/iOS/APIExample-OC/APIExample-OC/AppDelegate.m +++ b/iOS/APIExample-OC/APIExample-OC/AppDelegate.m @@ -15,6 +15,13 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + if (@available(iOS 13.0, *)) { + // UIKit creates the window using the scene storyboard configuration. + } else { + self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + self.window.rootViewController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateInitialViewController]; + [self.window makeKeyAndVisible]; + } return YES; } diff --git a/iOS/APIExample-OC/APIExample-OC/Common/Views/AlertManager.swift b/iOS/APIExample-OC/APIExample-OC/Common/Views/AlertManager.swift index 5e66e458f..f02dd06c9 100644 --- a/iOS/APIExample-OC/APIExample-OC/Common/Views/AlertManager.swift +++ b/iOS/APIExample-OC/APIExample-OC/Common/Views/AlertManager.swift @@ -8,8 +8,8 @@ import UIKit import AVFoundation -public let cl_screenWidht = UIScreen.main.bounds.width -public let cl_screenHeight = UIScreen.main.bounds.height +public var cl_screenWidht: CGFloat { UIViewController.keyWindow?.bounds.width ?? 0 } +public var cl_screenHeight: CGFloat { UIViewController.keyWindow?.bounds.height ?? 0 } class AlertManager: NSObject { private struct AlertViewCache { var view: UIView? diff --git a/iOS/APIExample-OC/APIExample-OC/Common/Views/ToastView.swift b/iOS/APIExample-OC/APIExample-OC/Common/Views/ToastView.swift index 02291ec91..22216f8f3 100644 --- a/iOS/APIExample-OC/APIExample-OC/Common/Views/ToastView.swift +++ b/iOS/APIExample-OC/APIExample-OC/Common/Views/ToastView.swift @@ -204,17 +204,17 @@ class ToastView: UIView { } extension UIViewController { static var keyWindow: UIWindow? { - // Get connected scenes if #available(iOS 13.0, *) { - return UIApplication.shared.connectedScenes - // Keep only active scenes, onscreen and visible to the user - .filter { $0.activationState == .foregroundActive } - // Keep only the first `UIWindowScene` - .first(where: { $0 is UIWindowScene }) - // Get its associated windows - .flatMap({ $0 as? UIWindowScene })?.windows - // Finally, keep only the key window - .first(where: \.isKeyWindow) + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + // Prefer the active scene; startup and system dialogs can temporarily make it inactive. + for state in [UIScene.ActivationState.foregroundActive, .foregroundInactive] { + if let window = scenes.filter({ $0.activationState == state }) + .flatMap({ $0.windows }) + .first(where: \.isKeyWindow) { + return window + } + } + return nil } else { return UIApplication.shared.keyWindow } diff --git a/iOS/APIExample-OC/APIExample-OC/Info.plist b/iOS/APIExample-OC/APIExample-OC/Info.plist index 8a4ef01cb..542135717 100644 --- a/iOS/APIExample-OC/APIExample-OC/Info.plist +++ b/iOS/APIExample-OC/APIExample-OC/Info.plist @@ -9,5 +9,24 @@ UIFileSharingEnabled + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + diff --git a/iOS/APIExample-OC/APIExample-OC/SceneDelegate.h b/iOS/APIExample-OC/APIExample-OC/SceneDelegate.h new file mode 100644 index 000000000..1fdaad29f --- /dev/null +++ b/iOS/APIExample-OC/APIExample-OC/SceneDelegate.h @@ -0,0 +1,9 @@ +#import + +API_AVAILABLE(ios(13.0)) +@interface SceneDelegate : UIResponder + +// UIKit creates this window from Main.storyboard in the scene configuration. +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/iOS/APIExample-OC/APIExample-OC/SceneDelegate.m b/iOS/APIExample-OC/APIExample-OC/SceneDelegate.m new file mode 100644 index 000000000..073fa4aae --- /dev/null +++ b/iOS/APIExample-OC/APIExample-OC/SceneDelegate.m @@ -0,0 +1,4 @@ +#import "SceneDelegate.h" + +@implementation SceneDelegate +@end diff --git a/iOS/APIExample-OC/ARCHITECTURE.md b/iOS/APIExample-OC/ARCHITECTURE.md index 90265818e..6ad0d46e5 100644 --- a/iOS/APIExample-OC/ARCHITECTURE.md +++ b/iOS/APIExample-OC/ARCHITECTURE.md @@ -1,5 +1,16 @@ # ARCHITECTURE.md — APIExample-OC +## Application Lifecycle + +The app and its extensions retain iOS 12 support. On iOS 12, `AppDelegate` creates the +window and loads the initial controller from `Main.storyboard`. On iOS 13 and later, +`Info.plist` declares a single application scene: UIKit loads `Main.storyboard` and +assigns its window to `SceneDelegate`. Scene declarations and window APIs are guarded +by iOS availability checks; the window helper retains its iOS 12 fallback. +Do not rebuild the root controller on scene activation or end RTC sessions merely because +the scene enters the background. Use a build toolchain that supports the deployment target; +adopting the scene lifecycle does not require raising the minimum iOS version. + ## Case Index | Case | Path | Key APIs | Description | @@ -48,6 +59,7 @@ APIExample-OC/ └── APIExample-OC/ ├── main.m ├── AppDelegate.h / .m + ├── SceneDelegate.h / .m # Window owned by the application scene ├── ViewController.h / .m # Root menu controller — MenuItem registration lives here ├── Info.plist ├── APIExample-Bridging-Header.h diff --git a/iOS/APIExample-OC/README.md b/iOS/APIExample-OC/README.md index 579ab6fcb..40296c295 100644 --- a/iOS/APIExample-OC/README.md +++ b/iOS/APIExample-OC/README.md @@ -13,7 +13,8 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ## Prerequisites -- Xcode 13.0+ +- iOS 12.0 or later +- Xcode 13.0 or later with support for an iOS 12 deployment target ([Xcode support](https://developer.apple.com/support/xcode/)) - Physical iOS device (iPhone or iPad) - iOS simulator is NOT supported diff --git a/iOS/APIExample-OC/README.zh.md b/iOS/APIExample-OC/README.zh.md index 35a6d2da0..5f5c788f6 100644 --- a/iOS/APIExample-OC/README.zh.md +++ b/iOS/APIExample-OC/README.zh.md @@ -14,7 +14,8 @@ ## 环境准备 -- Xcode 13.0 或更高版本 +- iOS 12.0 或更高版本 +- Xcode 13.0 或更高版本,且该版本支持将最低部署目标设为 iOS 12([Xcode 支持范围](https://developer.apple.com/support/xcode/)) - iPhone 或 iPad 真机 - 不支持 iOS 模拟器 diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/AgoraExtension.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/AgoraExtension.swift index f2119fdfe..3523d7c8c 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/AgoraExtension.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/AgoraExtension.swift @@ -257,7 +257,7 @@ extension UIAlertController { extension UIApplication { /// The top most view controller static var topMostViewController: UIViewController? { - return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController + return UIViewController.keyWindow?.rootViewController?.visibleViewController } } diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/NetworkManager/ToastView.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/NetworkManager/ToastView.swift index 030540406..5b359b0e3 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/NetworkManager/ToastView.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Common/NetworkManager/ToastView.swift @@ -202,20 +202,16 @@ class ToastView: UIView { } extension UIViewController { static var keyWindow: UIWindow? { - // Get connected scenes - if #available(iOS 13.0, *) { - return UIApplication.shared.connectedScenes - // Keep only active scenes, onscreen and visible to the user - .filter { $0.activationState == .foregroundActive } - // Keep only the first `UIWindowScene` - .first(where: { $0 is UIWindowScene }) - // Get its associated windows - .flatMap({ $0 as? UIWindowScene })?.windows - // Finally, keep only the key window - .first(where: \.isKeyWindow) - } else { - return UIApplication.shared.keyWindow + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + // Prefer the active scene; startup and system dialogs can temporarily make it inactive. + for state in [UIScene.ActivationState.foregroundActive, .foregroundInactive] { + if let window = scenes.filter({ $0.activationState == state }) + .flatMap({ $0.windows }) + .first(where: \.isKeyWindow) { + return window + } } + return nil } static func cl_topViewController(_ viewController: UIViewController? = nil) -> UIViewController? { let viewController = viewController ?? keyWindow?.rootViewController diff --git a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift index dacca3ad8..496436f7e 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift @@ -150,9 +150,7 @@ struct MenuItemView: View { let alert = UIAlertController(title: "Unsupported", message: "Picture in Picture is not supported on this version of iOS.", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) - if let rootViewController = UIApplication.shared.windows.first?.rootViewController { - rootViewController.present(alert, animated: true, completion: nil) - } + UIViewController.cl_topViewController()?.present(alert, animated: true) } } diff --git a/iOS/APIExample-SwiftUI/ARCHITECTURE.md b/iOS/APIExample-SwiftUI/ARCHITECTURE.md index 2c426a947..012383828 100644 --- a/iOS/APIExample-SwiftUI/ARCHITECTURE.md +++ b/iOS/APIExample-SwiftUI/ARCHITECTURE.md @@ -1,5 +1,11 @@ # ARCHITECTURE.md — APIExample-SwiftUI +## Application Lifecycle + +The app and its broadcast extension retain their original iOS 14 minimum. `APIExample_SwiftUIApp` +uses the SwiftUI `App` lifecycle and `WindowGroup`; do not add a UIKit app/scene delegate. +UIKit presentation helpers resolve key windows from foreground `UIWindowScene` instances. + ## Case Index | Case | Path | Key APIs | Description | diff --git a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index 9f8cbbf7f..e35f4f17d 100644 --- a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj +++ b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ 03BEED0B251C4446005E78F4 /* audiomixing.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 03BEED0A251C4446005E78F4 /* audiomixing.mp3 */; }; 03BEED0D251CAB9C005E78F4 /* audioeffect.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 03BEED0C251CAB9C005E78F4 /* audioeffect.mp3 */; }; 03D13BD02448758900B599B3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D13BCF2448758900B599B3 /* AppDelegate.swift */; }; + 6B1F9C169FBD07EE9D70CFCD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */; }; 03D13BD42448758900B599B3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D13BD32448758900B599B3 /* ViewController.swift */; }; 03D13BD72448758900B599B3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03D13BD52448758900B599B3 /* Main.storyboard */; }; 03D13BD92448758B00B599B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03D13BD82448758B00B599B3 /* Assets.xcassets */; }; @@ -327,6 +328,7 @@ 03BEED0C251CAB9C005E78F4 /* audioeffect.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = audioeffect.mp3; sourceTree = ""; }; 03D13BCC2448758900B599B3 /* APIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = APIExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 03D13BCF2448758900B599B3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 03D13BD32448758900B599B3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 03D13BD62448758900B599B3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 03D13BD82448758B00B599B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -1129,6 +1131,7 @@ E7163F8929651D8900EBBD55 /* AR.scnassets */, 03D13BDD2448758B00B599B3 /* Info.plist */, 03D13BCF2448758900B599B3 /* AppDelegate.swift */, + BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */, 03D13BD82448758B00B599B3 /* Assets.xcassets */, 03D13BDA2448758B00B599B3 /* LaunchScreen.storyboard */, 033A9F2C252D737900BC26E1 /* Localizable.strings */, @@ -1873,6 +1876,7 @@ 5744CE0925BA99FF0099AB66 /* VideoChat.swift in Sources */, 034C625E2524A06800296ECF /* VoiceChanger.swift in Sources */, 03D13BD02448758900B599B3 /* AppDelegate.swift in Sources */, + 6B1F9C169FBD07EE9D70CFCD /* SceneDelegate.swift in Sources */, 0385768225224A88003C369A /* JoinChannelVideo.swift in Sources */, A7847F922458062900469187 /* StatisticsInfo.swift in Sources */, E7163F88296414F700EBBD55 /* ARVideoRenderer.swift in Sources */, diff --git a/iOS/APIExample/APIExample/AppDelegate.swift b/iOS/APIExample/APIExample/AppDelegate.swift index 0291a7f04..375a50ba1 100644 --- a/iOS/APIExample/APIExample/AppDelegate.swift +++ b/iOS/APIExample/APIExample/AppDelegate.swift @@ -8,13 +8,20 @@ import UIKit -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + if #available(iOS 13.0, *) { + // UIKit creates the window using the scene storyboard configuration. + } else { + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() + self.window = window + window.makeKeyAndVisible() + } return true } } diff --git a/iOS/APIExample/APIExample/Common/AgoraExtension.swift b/iOS/APIExample/APIExample/Common/AgoraExtension.swift index 537c0dec1..dcc11f7d7 100644 --- a/iOS/APIExample/APIExample/Common/AgoraExtension.swift +++ b/iOS/APIExample/APIExample/Common/AgoraExtension.swift @@ -269,7 +269,7 @@ extension UIAlertController { extension UIApplication { /// The top most view controller static var topMostViewController: UIViewController? { - return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController + return UIViewController.keyWindow?.rootViewController?.visibleViewController } } diff --git a/iOS/APIExample/APIExample/Common/AlertManager.swift b/iOS/APIExample/APIExample/Common/AlertManager.swift index 1227ec561..3f0c517cf 100644 --- a/iOS/APIExample/APIExample/Common/AlertManager.swift +++ b/iOS/APIExample/APIExample/Common/AlertManager.swift @@ -11,7 +11,7 @@ import AVFoundation extension UIScreen { static var currentScreenSize: CGSize { // Get the main window's available size - guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else { + guard let window = UIViewController.keyWindow else { return CGSize.zero } @@ -26,8 +26,8 @@ extension UIScreen { } } -let cl_screenWidht = UIScreen.currentScreenSize.width -let cl_screenHeight = UIScreen.currentScreenSize.height +var cl_screenWidht: CGFloat { UIScreen.currentScreenSize.width } +var cl_screenHeight: CGFloat { UIScreen.currentScreenSize.height } class AlertManager: NSObject { private struct AlertViewCache { var view: UIView? diff --git a/iOS/APIExample/APIExample/Common/LogViewController.swift b/iOS/APIExample/APIExample/Common/LogViewController.swift index 0b5974952..c5d850692 100644 --- a/iOS/APIExample/APIExample/Common/LogViewController.swift +++ b/iOS/APIExample/APIExample/Common/LogViewController.swift @@ -8,6 +8,76 @@ import UIKit import Foundation +import Floaty + +// Floaty.global creates a window without a scene. Keep this button in the app's view hierarchy. +final class LogFloatingButton: Floaty { + override init() { + super.init() + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } + + func show(in container: UIView) { + if superview !== container { + container.addSubview(self) + } + container.bringSubviewToFront(self) + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + updateSafeAreaPadding() + } + + override func safeAreaInsetsDidChange() { + super.safeAreaInsetsDidChange() + updateSafeAreaPadding() + } + + private func configure() { + // Floaty 4.2 reads AppDelegate.window for safe-area placement, which is nil with scenes. + relativeToSafeArea = false + isDraggable = true + addItem(title: "Send Logs") { [weak self] _ in + self?.shareLogs() + } + addItem(title: "Clean Up") { _ in + LogUtils.cleanUp() + } + } + + private func updateSafeAreaPadding() { + let insets = superview?.safeAreaInsets ?? .zero + let horizontal = 14 + insets.right + let vertical = 14 + insets.bottom + if paddingX != horizontal { paddingX = horizontal } + if paddingY != vertical { paddingY = vertical } + } + + private func shareLogs() { + guard var presenter = window?.rootViewController else { return } + while let presented = presenter.presentedViewController { + presenter = presented + } + guard !(presenter is UIActivityViewController), !presenter.isBeingDismissed else { return } + + LogUtils.writeAppLogsToDisk() + let activity = UIActivityViewController( + activityItems: [URL(fileURLWithPath: LogUtils.logFolder(), isDirectory: true)], + applicationActivities: nil + ) + activity.modalPresentationStyle = .popover + activity.popoverPresentationController?.sourceView = self + activity.popoverPresentationController?.sourceRect = bounds + presenter.present(activity, animated: true) + } +} + enum LogLevel { case info, warning, error diff --git a/iOS/APIExample/APIExample/Common/NetworkManager/ToastView.swift b/iOS/APIExample/APIExample/Common/NetworkManager/ToastView.swift index d959c1b09..50f2292cf 100644 --- a/iOS/APIExample/APIExample/Common/NetworkManager/ToastView.swift +++ b/iOS/APIExample/APIExample/Common/NetworkManager/ToastView.swift @@ -220,17 +220,17 @@ class ToastView: UIView { } extension UIViewController { static var keyWindow: UIWindow? { - // Get connected scenes if #available(iOS 13.0, *) { - return UIApplication.shared.connectedScenes - // Keep only active scenes, onscreen and visible to the user - .filter { $0.activationState == .foregroundActive } - // Keep only the first `UIWindowScene` - .first(where: { $0 is UIWindowScene }) - // Get its associated windows - .flatMap({ $0 as? UIWindowScene })?.windows - // Finally, keep only the key window - .first(where: \.isKeyWindow) + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + // Prefer the active scene; startup and system dialogs can temporarily make it inactive. + for state in [UIScene.ActivationState.foregroundActive, .foregroundInactive] { + if let window = scenes.filter({ $0.activationState == state }) + .flatMap({ $0.windows }) + .first(where: \.isKeyWindow) { + return window + } + } + return nil } else { return UIApplication.shared.keyWindow } diff --git a/iOS/APIExample/APIExample/Info.plist b/iOS/APIExample/APIExample/Info.plist index 08cfa7552..516fa2f36 100644 --- a/iOS/APIExample/APIExample/Info.plist +++ b/iOS/APIExample/APIExample/Info.plist @@ -51,8 +51,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 @@ -70,5 +68,24 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + diff --git a/iOS/APIExample/APIExample/SceneDelegate.swift b/iOS/APIExample/APIExample/SceneDelegate.swift new file mode 100644 index 000000000..220944d29 --- /dev/null +++ b/iOS/APIExample/APIExample/SceneDelegate.swift @@ -0,0 +1,7 @@ +import UIKit + +@available(iOS 13.0, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + // UIKit creates this window from Main.storyboard in the scene configuration. + var window: UIWindow? +} diff --git a/iOS/APIExample/APIExample/ViewController.swift b/iOS/APIExample/APIExample/ViewController.swift index df6c26f39..432796bd6 100644 --- a/iOS/APIExample/APIExample/ViewController.swift +++ b/iOS/APIExample/APIExample/ViewController.swift @@ -7,7 +7,6 @@ // import UIKit -import Floaty import AgoraRtcKit struct MenuSection { @@ -80,27 +79,14 @@ class ViewController: AGViewController { MenuItem(name: "Multipath".localized, storyboard: "Multipath", controller: "MultipathEntry"), ]) ] - override func viewDidLoad() { - super.viewDidLoad() - Floaty.global.button.addItem(title: "Send Logs", handler: { _ in - LogUtils.writeAppLogsToDisk() - let activity = UIActivityViewController(activityItems: [NSURL(fileURLWithPath: LogUtils.logFolder(), - isDirectory: true)], - applicationActivities: nil) - activity.modalPresentationStyle = .popover - if UIDevice.current.userInterfaceIdiom == .pad { - activity.popoverPresentationController?.sourceView = Floaty.global.button - } - self.present(activity, animated: true, completion: nil) - }) - - Floaty.global.button.addItem(title: "Clean Up", handler: { _ in - LogUtils.cleanUp() - }) - Floaty.global.button.isDraggable = true - Floaty.global.show() + private let logButton = LogFloatingButton() + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + // The navigation view survives pushes to individual examples. + logButton.show(in: navigationController?.view ?? view) } - + @IBAction func onSettings(_ sender: UIBarButtonItem) { let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) guard let settingsViewController = storyBoard.instantiateViewController(withIdentifier: "settings") as? SettingsViewController else { diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index e947ea055..0ced7a5b8 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -1,5 +1,21 @@ # ARCHITECTURE.md — APIExample +## Application Lifecycle + +The app and its extensions retain iOS 12 support. On iOS 12, `AppDelegate` creates the +window and loads the initial controller from `Main.storyboard`. On iOS 13 and later, +`Info.plist` declares a single application scene: UIKit loads `Main.storyboard` and +assigns its window to `SceneDelegate`. Scene declarations and window APIs are guarded +by iOS availability checks; the window helper retains its iOS 12 fallback. +Do not rebuild the root controller on scene activation or end RTC sessions merely because +the scene enters the background. Use a build toolchain that supports the deployment target; +adopting the scene lifecycle does not require raising the minimum iOS version. + +`ViewController` owns a `LogFloatingButton` defined in `Common/LogViewController.swift`. +It is attached to the navigation container after appearance so it remains available in +example pages on both iOS 12 and later. Its safe-area placement and share sheet use its +own view/window hierarchy; do not use `Floaty.global`, which creates a window without a scene. + ## Case Index | Case | Path | Key APIs | Description | @@ -61,6 +77,7 @@ APIExample/ ├── libs/ # Local SDK frameworks (when not using CocoaPods) └── APIExample/ ├── AppDelegate.swift + ├── SceneDelegate.swift # Window owned by the application scene ├── ViewController.swift # Root menu controller — MenuItem registration lives here ├── Info.plist ├── APIExample.entitlements diff --git a/iOS/APIExample/README.md b/iOS/APIExample/README.md index 2b9a3a3f5..1c7af182a 100644 --- a/iOS/APIExample/README.md +++ b/iOS/APIExample/README.md @@ -13,7 +13,8 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ## Prerequisites -- Xcode 13.0+ +- iOS 12.0 or later +- Xcode 13.0 or later with support for an iOS 12 deployment target ([Xcode support](https://developer.apple.com/support/xcode/)) - Physical iOS device (iPhone or iPad) - iOS simulator is NOT supported diff --git a/iOS/APIExample/README.zh.md b/iOS/APIExample/README.zh.md index 79f4bbc11..913ab7724 100644 --- a/iOS/APIExample/README.zh.md +++ b/iOS/APIExample/README.zh.md @@ -14,7 +14,8 @@ ## 环境准备 -- Xcode 13.0 或更高版本 +- iOS 12.0 或更高版本 +- Xcode 13.0 或更高版本,且该版本支持将最低部署目标设为 iOS 12([Xcode 支持范围](https://developer.apple.com/support/xcode/)) - iPhone 或 iPad 真机 - 不支持 iOS 模拟器 diff --git a/iOS/ARCHITECTURE.md b/iOS/ARCHITECTURE.md index 6648ac527..988151661 100644 --- a/iOS/ARCHITECTURE.md +++ b/iOS/ARCHITECTURE.md @@ -1,10 +1,31 @@ # ARCHITECTURE.md -Four independent iOS example projects sharing one Xcode workspace, each managing dependencies via CocoaPods. +Four independent iOS example projects, each with its own Xcode workspace and CocoaPods dependencies. For internal details of each project, see the project-level `ARCHITECTURE.md`. --- +## Deployment Targets and Lifecycle + +| Project | Minimum iOS | App lifecycle | +| --- | --- | --- | +| `APIExample` | 12.0 | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | +| `APIExample-Audio` | 12.0 | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | +| `APIExample-OC` | 12.0 | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | +| `APIExample-SwiftUI` | 14.0 | SwiftUI `App` and `WindowGroup` | + +The app and extension deployment targets remain unchanged. The Audio project has no app +extension. Choose an Xcode version that supports the project's deployment target; see +[Apple's Xcode support table](https://developer.apple.com/support/xcode/). Adopting the +scene lifecycle does not require increasing the minimum iOS version. + +The three UIKit apps configure a single application scene and retain the iOS 12 startup +path. SwiftUI keeps its existing scene configuration. See +[Apple TN3187](https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle) +and each project's architecture document for window ownership and UI presentation rules. + +--- + ## APIExample — Full Demo - Language: Swift From 59235ae1e75fa2c5cab5db1428efb49e0f22319c Mon Sep 17 00:00:00 2001 From: zhangwei Date: Thu, 17 Sep 2026 21:48:42 +0800 Subject: [PATCH 46/49] fix(ios): support Xcode 27 and adapt audio call layout --- .../APIExample.xcodeproj/project.pbxproj | 16 ++++++++++++++++ .../Base.lproj/JoinChannelAudio.storyboard | 5 ++--- iOS/APIExample/ARCHITECTURE.md | 7 +++++-- iOS/APIExample/Podfile | 16 ++++++++++++++-- iOS/APIExample/README.md | 4 ++-- iOS/APIExample/README.zh.md | 4 ++-- iOS/ARCHITECTURE.md | 9 ++++++--- 7 files changed, 47 insertions(+), 14 deletions(-) diff --git a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index e35f4f17d..6410afadd 100644 --- a/iOS/APIExample/APIExample.xcodeproj/project.pbxproj +++ b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj @@ -2388,6 +2388,8 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Agora-ScreenShare-Extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2422,6 +2424,8 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "Agora-ScreenShare-Extension/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2495,6 +2499,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -2551,6 +2557,8 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -2578,6 +2586,8 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = APIExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2649,6 +2659,8 @@ FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = APIExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2721,6 +2733,8 @@ INFOPLIST_FILE = SimpleFilter/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2758,6 +2772,8 @@ INFOPLIST_FILE = SimpleFilter/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphoneos27.*]" = 15.0; + "IPHONEOS_DEPLOYMENT_TARGET[sdk=iphonesimulator27.*]" = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard index 7f445d4e2..4eca5274c 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard @@ -327,7 +327,6 @@ - @@ -336,8 +335,8 @@ - - + + diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index 0ced7a5b8..511c72ccc 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -2,7 +2,10 @@ ## Application Lifecycle -The app and its extensions retain iOS 12 support. On iOS 12, `AppDelegate` creates the +The app and its extensions default to iOS 12. SDK-conditional build settings raise the +minimum to iOS 15 when using iOS 27 SDKs, and the Podfile applies the same minimum to Pods. +Use a toolchain supporting iOS 12 deployment for packages intended for iOS 12 devices. +On iOS 12, `AppDelegate` creates the window and loads the initial controller from `Main.storyboard`. On iOS 13 and later, `Info.plist` declares a single application scene: UIKit loads `Main.storyboard` and assigns its window to `SceneDelegate`. Scene declarations and window APIs are guarded @@ -23,7 +26,7 @@ own view/window hierarchy; do not use `Floaty.global`, which creates a window wi | JoinChannelVideo | `Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift` | `joinChannel()`, `setupLocalVideo()`, `setupRemoteVideo()` | Basic video call — join channel and render local/remote video | | JoinChannelVideo(Token) | `Examples/Basic/JoinChannelVideo(Token)/JoinChannelVideoToken.swift` | `joinChannel(byToken:)`, `setupLocalVideo()`, `setupRemoteVideo()` | Video call with token authentication | | JoinChannelVideo(Recorder) | `Examples/Basic/JoinChannelVideo(Recorder)/JoinChannelVideoRecorder.swift` | `createMediaRecorder()`, `joinChannel()`, `setupLocalVideo()` | Local and remote stream recording | -| JoinChannelAudio | `Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift` | `joinChannel()`, `setAudioProfile()`, `enableAudioVolumeIndication()`, `adjustRecordingSignalVolume()` | Basic audio call with volume and in-ear monitoring controls | +| JoinChannelAudio | `Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift` | `joinChannel()`, `setAudioProfile()`, `enableAudioVolumeIndication()`, `adjustRecordingSignalVolume()` | Basic audio call with relay, scenario, volume and in-ear monitoring controls; participant area adapts to available height | | LiveStreaming | `Examples/Advanced/LiveStreaming/LiveStreaming.swift` | `setClientRole()`, `setVideoScenario()`, `preloadChannel()`, `enableCameraCenterStage()` | Interactive live streaming with role switching and camera features | | RTMPStreaming | `Examples/Advanced/RTMPStreaming/RTMPStreaming.swift` | `startRtmpStreamWithoutTranscoding()`, `startRtmpStream(withTranscoding:)`, `updateRtmpTranscoding()`, `stopRtmpStream()` | Push stream to CDN with optional transcoding | | VideoMetadata | `Examples/Advanced/VideoMetadata/VideoMetadata.swift` | `setMediaMetadataDataSource()`, `setMediaMetadataDelegate()` | Send and receive metadata attached to video stream | diff --git a/iOS/APIExample/Podfile b/iOS/APIExample/Podfile index 790c29df8..de8cee708 100644 --- a/iOS/APIExample/Podfile +++ b/iOS/APIExample/Podfile @@ -1,5 +1,4 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '12.0' #source 'https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git' @@ -40,3 +39,16 @@ end pre_install do |installer| # system("sh .download_script.sh 4.7.0 true") end + +# Xcode 27 SDKs require iOS 15; retain each pod's default for older SDKs. +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + current = Gem::Version.new(config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] || '12.0') + minimum = [current, Gem::Version.new('15.0')].max.to_s + %w[iphoneos27.* iphonesimulator27.*].each do |sdk| + config.build_settings["IPHONEOS_DEPLOYMENT_TARGET[sdk=#{sdk}]"] = minimum + end + end + end +end diff --git a/iOS/APIExample/README.md b/iOS/APIExample/README.md index 1c7af182a..7dc358493 100644 --- a/iOS/APIExample/README.md +++ b/iOS/APIExample/README.md @@ -13,8 +13,8 @@ After users upgrade their iOS devices to iOS 14.0, and use an app that integrate ## Prerequisites -- iOS 12.0 or later -- Xcode 13.0 or later with support for an iOS 12 deployment target ([Xcode support](https://developer.apple.com/support/xcode/)) +- iOS 12.0 or later; builds using Xcode 27 SDKs require iOS 15.0 or later +- Xcode 13.0 or later with support for the intended deployment target ([Xcode support](https://developer.apple.com/support/xcode/)). iOS 27 SDKs automatically select an iOS 15 deployment target for the app, extensions, and Pods; use a compatible older toolchain for iOS 12 packages. - Physical iOS device (iPhone or iPad) - iOS simulator is NOT supported diff --git a/iOS/APIExample/README.zh.md b/iOS/APIExample/README.zh.md index 913ab7724..de12e402f 100644 --- a/iOS/APIExample/README.zh.md +++ b/iOS/APIExample/README.zh.md @@ -14,8 +14,8 @@ ## 环境准备 -- iOS 12.0 或更高版本 -- Xcode 13.0 或更高版本,且该版本支持将最低部署目标设为 iOS 12([Xcode 支持范围](https://developer.apple.com/support/xcode/)) +- iOS 12.0 或更高版本;使用 Xcode 27 SDK 构建的安装包要求 iOS 15.0 或更高版本 +- Xcode 13.0 或更高版本,且支持所需部署目标([Xcode 支持范围](https://developer.apple.com/support/xcode/))。使用 iOS 27 SDK 时,主 App、扩展和 Pods 自动采用 iOS 15 部署目标;需要兼容 iOS 12 的安装包时,使用支持该目标的旧工具链。 - iPhone 或 iPad 真机 - 不支持 iOS 模拟器 diff --git a/iOS/ARCHITECTURE.md b/iOS/ARCHITECTURE.md index 988151661..db5a88948 100644 --- a/iOS/ARCHITECTURE.md +++ b/iOS/ARCHITECTURE.md @@ -9,13 +9,16 @@ For internal details of each project, see the project-level `ARCHITECTURE.md`. | Project | Minimum iOS | App lifecycle | | --- | --- | --- | -| `APIExample` | 12.0 | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | +| `APIExample` | 12.0 by default; 15.0 with iOS 27 SDKs | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | | `APIExample-Audio` | 12.0 | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | | `APIExample-OC` | 12.0 | AppDelegate window on iOS 12; SceneDelegate on iOS 13+ | | `APIExample-SwiftUI` | 14.0 | SwiftUI `App` and `WindowGroup` | -The app and extension deployment targets remain unchanged. The Audio project has no app -extension. Choose an Xcode version that supports the project's deployment target; see +APIExample uses SDK-conditional iOS 15 deployment targets for the app and extensions +when building with iOS 27 SDKs. Its Podfile applies the same minimum to generated Pods +targets. The default iOS 12 configuration remains for toolchains that support it; an +iOS 27 SDK build produces an app requiring iOS 15 or later. The Audio project has no app +extension. Choose an Xcode version that supports the effective deployment target; see [Apple's Xcode support table](https://developer.apple.com/support/xcode/). Adopting the scene lifecycle does not require increasing the minimum iOS version. From d6f8c8ade0fc479067f77ee72acea3702ce5fa88 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 22 Sep 2026 08:19:34 +0800 Subject: [PATCH 47/49] build(android): migrate RTC dependencies to official Maven repositories --- Android/APIExample-Audio/cloud_build.sh | 3 +-- Android/APIExample-Audio/settings.gradle | 10 ++++++++++ Android/APIExample-Compose/cloud_build.sh | 3 +-- Android/APIExample-Compose/settings.gradle.kts | 10 ++++++++++ Android/APIExample/cloud_build.sh | 5 ----- Android/APIExample/settings.gradle | 12 +++++++++++- docs/ai/project-matrix.md | 13 +++++++++++++ 7 files changed, 46 insertions(+), 10 deletions(-) diff --git a/Android/APIExample-Audio/cloud_build.sh b/Android/APIExample-Audio/cloud_build.sh index c91e13ae9..ce4f1ff5e 100755 --- a/Android/APIExample-Audio/cloud_build.sh +++ b/Android/APIExample-Audio/cloud_build.sh @@ -33,8 +33,7 @@ else fi fi -#change android maven to china repos -sed -ie "s#google()#maven { url \"https\://maven.aliyun.com/repository/public\" }\n google()#g" settings.gradle +# Maven repositories are configured in settings; mirror only the Gradle distribution. sed -ie "s#https://services.gradle.org/distributions#https://mirrors.cloud.tencent.com/gradle#g" gradle/wrapper/gradle-wrapper.properties set_local_property() { diff --git a/Android/APIExample-Audio/settings.gradle b/Android/APIExample-Audio/settings.gradle index a12f07671..e9d5a552d 100644 --- a/Android/APIExample-Audio/settings.gradle +++ b/Android/APIExample-Audio/settings.gradle @@ -14,6 +14,16 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + // Resolve RTC SDK modules and their infrastructure dependencies from the official source. + exclusiveContent { + forRepository { + maven { url = 'https://download.agora.io/maven/' } + } + filter { + includeGroup("io.agora.rtc") + includeGroup("io.agora.infra") + } + } google() mavenCentral() } diff --git a/Android/APIExample-Compose/cloud_build.sh b/Android/APIExample-Compose/cloud_build.sh index 7ae30393a..413ce55b7 100755 --- a/Android/APIExample-Compose/cloud_build.sh +++ b/Android/APIExample-Compose/cloud_build.sh @@ -33,8 +33,7 @@ else fi fi -#change android maven to china repos -sed -ie "s#google()#maven { url = uri(\"https://maven.aliyun.com/repository/public\") }\n google()#g" settings.gradle.kts +# Maven repositories are configured in settings; mirror only the Gradle distribution. sed -ie "s#https://services.gradle.org/distributions#https://mirrors.cloud.tencent.com/gradle#g" gradle/wrapper/gradle-wrapper.properties set_local_property() { diff --git a/Android/APIExample-Compose/settings.gradle.kts b/Android/APIExample-Compose/settings.gradle.kts index 1328c935c..5413d59a3 100644 --- a/Android/APIExample-Compose/settings.gradle.kts +++ b/Android/APIExample-Compose/settings.gradle.kts @@ -14,6 +14,16 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + // Resolve RTC SDK modules and their infrastructure dependencies from the official source. + exclusiveContent { + forRepository { + maven { url = uri("https://download.agora.io/maven/") } + } + filter { + includeGroup("io.agora.rtc") + includeGroup("io.agora.infra") + } + } google() mavenCentral() } diff --git a/Android/APIExample/cloud_build.sh b/Android/APIExample/cloud_build.sh index 459d1d4bd..c953faf31 100755 --- a/Android/APIExample/cloud_build.sh +++ b/Android/APIExample/cloud_build.sh @@ -30,11 +30,6 @@ else fi fi -#change android maven to china repos -#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" diff --git a/Android/APIExample/settings.gradle b/Android/APIExample/settings.gradle index ba715ab54..5e054d3b9 100644 --- a/Android/APIExample/settings.gradle +++ b/Android/APIExample/settings.gradle @@ -14,6 +14,16 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + // Resolve RTC SDK modules and their infrastructure dependencies from the official source. + exclusiveContent { + forRepository { + maven { url = 'https://download.agora.io/maven/' } + } + filter { + includeGroup("io.agora.rtc") + includeGroup("io.agora.infra") + } + } google() mavenCentral() maven { @@ -29,4 +39,4 @@ if (simpleFilter.toBoolean()) { } if (streamEncrypt.toBoolean()) { include ':agora-stream-encrypt' -} \ No newline at end of file +} diff --git a/docs/ai/project-matrix.md b/docs/ai/project-matrix.md index 1969d08f9..e340e456b 100644 --- a/docs/ai/project-matrix.md +++ b/docs/ai/project-matrix.md @@ -15,6 +15,19 @@ change contract; this document defines the stable project inventory and dependen | macOS AppKit | `macOS/` | `AgoraRtcEngine_macOS` in `Podfile` | `macOS/AGENTS.md` | | Windows MFC | `windows/APIExample/` | Local `../../sdk` or public `Agora_Native_SDK_for_Windows_*_FULL.zip` | `windows/AGENTS.md` | +## Android SDK source + +All three Android projects resolve `io.agora.rtc` and `io.agora.infra` exclusively +from [the official Agora Maven repository](https://download.agora.io/maven/) when a local +`../../sdk` directory is absent. Repository routing is declared in each project's +`settings.gradle` or `settings.gradle.kts`; use the fixed `rtc_sdk_version` from +`gradle.properties`. Plugin and other third-party dependency repositories are separate. + +The checked-in settings are included in source packages. Cloud build adapters do +not inject an Aliyun Maven repository for RTC dependencies. Validate Maven resolution +without a local SDK and with a fresh dependency cache; compiling with a bundled SDK does +not validate the public repository. + ## Scope Rules - SDK version, dependency, credential plumbing, CI, and packaging changes require review of From 437a0ed97aa794838048b696b688e5abdedc6635 Mon Sep 17 00:00:00 2001 From: zhangwei Date: Tue, 22 Sep 2026 20:40:49 +0800 Subject: [PATCH 48/49] fix(ci): align lightweight pull request compile checks --- .github/ci/README.md | 19 ++++++++++--------- .github/ci/policy/check_templates.py | 2 +- .../template_fixtures/kotlin_dependencies.kt | 3 +-- .../template_fixtures/kotlin_scenarios.kt | 4 ++-- .github/workflows/compile.yml | 15 ++++----------- AGENTS.md | 4 ++-- .../references/composable-template.kt | 3 +-- README.kr.md | 1 + README.md | 1 + README.zh.md | 1 + docs/ai/README.md | 8 ++++---- 11 files changed, 28 insertions(+), 33 deletions(-) diff --git a/.github/ci/README.md b/.github/ci/README.md index 9a59ea15b..c74c621ad 100644 --- a/.github/ci/README.md +++ b/.github/ci/README.md @@ -17,8 +17,9 @@ After creating the venv described above, run the packaging helper tests from the Keep the Jenkins entry point and `build/` paths stable: external jobs and existing scripts refer to them directly. A lack of repository-local callers does not establish that an -external entry point is unused. GitHub compilation uses public SDK dependencies and -produces compile evidence; Jenkins owns release artifacts and signing evidence. +external entry point is unused. GitHub compilation runs for pull requests targeting `main`, +uses public SDK dependencies, and produces compile evidence; Jenkins owns release artifacts +and signing evidence. ## Template checks @@ -46,10 +47,10 @@ supplies the Kotlin CLI. Missing compilers, missing template sections or missing the check rather than silently skipping it. No SDK download or credentials are needed for these controlled regressions. -`compile.yml` additionally calls `policy/stage_compile_templates.py` before compiling Android -Audio/Compose and all four iOS projects. The helper adds complete template sources to the -actual app target, including Xcode Sources membership, so its SDK and project interfaces are -checked with the normal build. It intentionally mutates the checkout: run it only in a -disposable CI/scratch checkout, never a developer working tree with pending changes. It does -not add menu entries or run the example. Template/fixture checks do not replace the normal -application build, permission UI integration, case registration or device validation. +`compile.yml` builds the checked-in application targets without staging generated template +sources, keeping the pull request gate focused on application compilation and public dependency +resolution. To check a complete template against a real project interface, run +`policy/stage_compile_templates.py` only in a disposable CI/scratch checkout before that +project's normal build. The helper mutates source and Xcode project files; never run it in a +developer working tree with pending changes. Template/fixture checks do not replace permission +UI integration, case registration or device validation. diff --git a/.github/ci/policy/check_templates.py b/.github/ci/policy/check_templates.py index 2510e3d57..6b2ac1990 100644 --- a/.github/ci/policy/check_templates.py +++ b/.github/ci/policy/check_templates.py @@ -2,7 +2,7 @@ """Compile/execute canonical template code. Missing compilers or inputs fail, never skip. Native lifecycle tests use controlled dependencies. android-sdk checks real SDK interfaces; -Compile CI additionally stages complete templates into their actual application targets. +stage_compile_templates.py can add complete templates to disposable application checkouts. """ import argparse import os diff --git a/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt b/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt index d3bda0f94..1dcdb6bcc 100644 --- a/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt +++ b/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt @@ -8,9 +8,8 @@ class Handler(looper: Looper) { fun drain() { while (queue.isNotEmpty()) queue.removeFirst()() } } } -object BuildConfig { const val AGORA_APP_ID = "" } +object BuildConfig { const val AGORA_APP_ID = ""; var AGORA_APP_CERT = "" } object SettingPreferences { fun getArea() = 0 } -object AgoraConfig { var certificate = ""; fun getAppCertificate() = certificate } object TokenUtils { data class Request(val channel: String, val uid: Int, val callback: (String?) -> Unit) val requests = mutableListOf() diff --git a/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt b/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt index f7a776617..3b2c82566 100644 --- a/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt +++ b/.github/ci/policy/tests/template_fixtures/kotlin_scenarios.kt @@ -27,9 +27,9 @@ fun main() { check(!sut.beginJoin("new-before-old-permission-returns")) sut.onPermissionResult(true); check(TokenUtils.requests.size == count) check(sut.beginJoin("denied")); sut.onPermissionResult(false); check(TokenUtils.requests.size == count) - AgoraConfig.certificate = "enabled-in-test" + BuildConfig.AGORA_APP_CERT = "enabled-in-test" request("missing-token"); respond(count, null); check(joins() == 2) - AgoraConfig.certificate = "" + BuildConfig.AGORA_APP_CERT = "" request("no-certificate"); respond(count + 1, null); check(joins() == 3) sut.leave(); RtcEngine.joinResult = -1 request("join-error"); respond(count + 2); check(errors.any { it.contains("joinChannel failed") }) diff --git a/.github/workflows/compile.yml b/.github/workflows/compile.yml index 772799177..5ca135c9b 100644 --- a/.github/workflows/compile.yml +++ b/.github/workflows/compile.yml @@ -1,7 +1,7 @@ name: Compile CI on: - push: + pull_request: branches: - main workflow_dispatch: @@ -42,14 +42,11 @@ jobs: ${{ matrix.project }}/**/*.gradle* ${{ matrix.project }}/gradle/wrapper/gradle-wrapper.properties - uses: android-actions/setup-android@v3 + with: + packages: "platform-tools platforms;android-35 build-tools;35.0.0" - name: Configure compile-only App ID working-directory: ${{ matrix.project }} run: printf 'AGORA_APP_ID=%s\nAGORA_APP_CERT=\n' "$PLACEHOLDER_APP_ID" > local.properties - - name: Stage template compilation checks - if: matrix.name == 'Audio' || matrix.name == 'Compose' - env: - TEMPLATE_PROJECT: ${{ matrix.project }} - run: python3 .github/ci/policy/stage_compile_templates.py "$TEMPLATE_PROJECT" - name: Compile working-directory: ${{ matrix.project }} run: ./gradlew --no-daemon --stacktrace :app:assembleDebug @@ -108,10 +105,6 @@ jobs: - name: Install CocoaPods dependencies working-directory: ${{ matrix.project }} run: pod install --repo-update - - name: Stage template compilation checks - env: - TEMPLATE_PROJECT: ${{ matrix.project }} - run: python3 .github/ci/policy/stage_compile_templates.py "$TEMPLATE_PROJECT" - name: Compile working-directory: ${{ matrix.project }} env: @@ -167,4 +160,4 @@ jobs: - name: Compile working-directory: windows/APIExample shell: pwsh - run: msbuild APIExample.sln -m /p:Configuration=Release /p:Platform=Win32 + run: msbuild APIExample.sln -m /p:Configuration=Release /p:Platform=Win32 /p:PlatformToolset=v143 diff --git a/AGENTS.md b/AGENTS.md index 950c690ad..e63206c84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ evidence in `.github/pull_request_template.md`. | `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 | -| `.github/workflows/compile.yml` | Post-release compile-only checks using a placeholder App ID | +| `.github/workflows/compile.yml` | Pull request compile-only checks using a placeholder App ID | | `.github/workflows/repository-policy.yml` | Remote sensitive-information, commit-message, and AI asset checks | | `.github/ci/build/` | Jenkins release packaging entry points | | `.github/ci/README.md` | CI ownership, entry points, and local verification | @@ -78,4 +78,4 @@ Never commit a real App ID, App Certificate, or token. The mechanism differs by The iOS, macOS, and Windows credential files are tracked placeholder files, not git-ignored. Editing one to build locally leaves a real credential in a tracked file, so never stage it. The safety net is the gitleaks pre-commit hook (see Git Hooks above), not `.gitignore` — do not rely on git ignoring these paths. -GitHub Actions compilation uses an all-zero 32-character placeholder App ID and does not run RTC sessions. It must not use repository secrets, App Certificates, or tokens. The compile workflow runs automatically only after changes reach `main`, when the referenced SDK version is available from public package repositories. +GitHub Actions compilation uses an all-zero 32-character placeholder App ID and does not run RTC sessions. It must not use repository secrets, App Certificates, or tokens. The compile workflow runs for pull requests targeting `main` and can be started manually; referenced SDK versions must be available from public package repositories before the pull request can pass. diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt index 49dc9e351..e78af86f6 100644 --- a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import io.agora.api.example.compose.BuildConfig import io.agora.api.example.compose.data.SettingPreferences -import io.agora.api.example.compose.utils.AgoraConfig import io.agora.api.example.compose.utils.TokenUtils import io.agora.rtc2.ChannelMediaOptions import io.agora.rtc2.IRtcEngineEventHandler @@ -126,7 +125,7 @@ private class ExampleCaseSession( onError("Permission denied") return } - val tokenRequired = AgoraConfig.getAppCertificate().isNotEmpty() + val tokenRequired = BuildConfig.AGORA_APP_CERT.isNotEmpty() // genToken supports projects without an App Certificate as well. TokenUtils.genToken(request.channel, request.uid) { token -> mainHandler.post { diff --git a/README.kr.md b/README.kr.md index 151e1c48e..a89196f4c 100644 --- a/README.kr.md +++ b/README.kr.md @@ -13,6 +13,7 @@ | Android | Java | [Android/APIExample-Audio](Android/APIExample-Audio) | [RTC Java Audio SDK](https://docs.agora.io/en/sdks?platform=android) | | iOS | Swift | [iOS/APIExample](iOS/APIExample) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | | iOS | Objective-C | [iOS/APIExample-OC](iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | +| iOS | Swift | [iOS/APIExample-SwiftUI](iOS/APIExample-SwiftUI) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | | iOS | Swift | [iOS/APIExample-Audio](iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://docs.agora.io/en/sdks?platform=ios) | | macOS | Swift | [macOS](macOS) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=macos) | | Windows | C++ | [windows](windows) | [RTC C++ Video SDK](https://docs.agora.io/en/sdks?platform=windows) | diff --git a/README.md b/README.md index 6b9919c44..c173f771b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ This repository contains sample projects for the Agora RTC Native SDK, including | Android | Java | [Android/APIExample-Audio](Android/APIExample-Audio) | [RTC Java Audio SDK](https://docs.agora.io/en/sdks?platform=android) | | iOS | Swift | [iOS/APIExample](iOS/APIExample) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | | iOS | Objective-C | [iOS/APIExample-OC](iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | +| iOS | Swift | [iOS/APIExample-SwiftUI](iOS/APIExample-SwiftUI) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=ios) | | iOS | Swift | [iOS/APIExample-Audio](iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://docs.agora.io/en/sdks?platform=ios) | | macOS | Swift | [macOS](macOS) | [RTC Objective-C Video SDK](https://docs.agora.io/en/sdks?platform=macos) | | Windows | C++ | [windows](windows) | [RTC C++ Video SDK](https://docs.agora.io/en/sdks?platform=windows) | diff --git a/README.zh.md b/README.zh.md index 3810305a0..609ea2a5d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,6 +13,7 @@ | Android | Java | [Android/APIExample-Audio](Android/APIExample-Audio) | [RTC Java Audio SDK](https://doc.shengwang.cn/doc/rtc/android/resources) | | iOS | Swift | [iOS/APIExample](iOS/APIExample) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | | iOS | Objective-C | [iOS/APIExample-OC](iOS/APIExample-OC) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | +| iOS | Swift | [iOS/APIExample-SwiftUI](iOS/APIExample-SwiftUI) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | | iOS | Swift | [iOS/APIExample-Audio](iOS/APIExample-Audio) | [RTC Objective-C Audio SDK](https://doc.shengwang.cn/doc/rtc/ios/resources) | | macOS | Swift | [macOS](macOS) | [RTC Objective-C Video SDK](https://doc.shengwang.cn/doc/rtc/macos/resources) | | Windows | C++ | [windows](windows) | [RTC C++ Video SDK](https://doc.shengwang.cn/doc/rtc/windows/resources) | diff --git a/docs/ai/README.md b/docs/ai/README.md index 126588e39..a1d043c4d 100644 --- a/docs/ai/README.md +++ b/docs/ai/README.md @@ -41,10 +41,10 @@ Reuse it for later checks; do not install the validator into the system Python e The same checks run in `.github/workflows/repository-policy.yml` for pushes and pull requests. That workflow also compiles and executes selected Android/iOS template lifecycle regressions. After editing creation templates, run the relevant native checks documented in -[CI template checks](../../.github/ci/README.md#template-checks). The post-release compile -workflow also builds the complete Android Audio/Compose and four iOS templates in their real -application targets. Structural validation alone does not establish template API correctness -or safe asynchronous cleanup. +[CI template checks](../../.github/ci/README.md#template-checks). The `main` pull request +compile workflow builds the checked-in applications without generating additional template +sources. Structural validation alone does not establish template API correctness or safe +asynchronous cleanup. The requirement contract owns shared behavior. Platform implementations may differ only when the SDK, UI framework, permission model, or operating system requires it, and the From 9f0bcd03641c5464006d9fa9d5558f6973a819db Mon Sep 17 00:00:00 2001 From: zhangwei Date: Sat, 10 Oct 2026 08:14:30 +0800 Subject: [PATCH 49/49] fix(ios): disable optional beauty pods in public CI --- iOS/APIExample/Podfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS/APIExample/Podfile b/iOS/APIExample/Podfile index de8cee708..225da67a9 100644 --- a/iOS/APIExample/Podfile +++ b/iOS/APIExample/Podfile @@ -21,8 +21,8 @@ target 'APIExample' do # pod 'SwiftLint', '~> 0.53.0' pod 'SnapKit', '~> 5.7.0' - pod 'SenseLib', :path => 'sense.podspec' - pod 'fuLib', :path => 'fu.podspec' + # pod 'SenseLib', :path => 'sense.podspec' + # pod 'fuLib', :path => 'fu.podspec' end target 'Agora-ScreenShare-Extension' do