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/.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/.github/ci/README.md b/.github/ci/README.md new file mode 100644 index 000000000..c74c621ad --- /dev/null +++ b/.github/ci/README.md @@ -0,0 +1,56 @@ +# 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.*` 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 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: + +```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 runs for pull requests targeting `main`, +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` 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/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/build_ios.sh b/.github/ci/build/build_ios.sh index 96b54690d..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/ @@ -152,6 +154,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/build_mac.sh b/.github/ci/build/build_mac.sh index ac53610c3..e9ef06451 100644 --- a/.github/ci/build/build_mac.sh +++ b/.github/ci/build/build_mac.sh @@ -91,8 +91,6 @@ if [ -z "$sdk_url" -o "$sdk_url" = "none" ]; then echo "sdk_url is empty" echo unzip_name: $unzip_name mkdir -p ./$unzip_name/samples - cp -rf ./macOS ./$unzip_name/samples/APIExample || exit 1 - ls -al ./$unzip_name/samples/API-Example/ else sdk_url_flag=true zip_name=${sdk_url##*/} @@ -111,20 +109,25 @@ else rm -f ./$unzip_name/Package.swift mkdir ./$unzip_name/samples - cp -rf ./macOS ./$unzip_name/samples/APIExample || exit 1 - ls -al ./$unzip_name/samples/API-Example/ +fi + +cp -rf ./macOS ./$unzip_name/samples/APIExample || exit 1 +ls -al ./$unzip_name/samples/API-Example/ +if [ $sdk_url_flag = true ]; then mv ./$unzip_name/samples/APIExample/sdk.podspec ./$unzip_name/ 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" @@ -144,4 +147,8 @@ if [ $compress_apiexample = true ]; then ls -al $WORKSPACE/ fi - +if [ $compile_project = true ]; then + cd ./$unzip_name/samples/APIExample + ./cloud_build.sh || exit 1 + cd - +fi diff --git a/.github/ci/build/build_windows.bat b/.github/ci/build/build_windows.bat index a848d9a6c..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)" ) @@ -116,11 +123,11 @@ if "%compile_project%"=="true" ( echo "Compiling project to generate executable..." cd windows\APIExample call cloud_build.bat - if %ERRORLEVEL% NEQ 0 ( + if errorlevel 1 ( echo Build failed! - exit /b %ERRORLEVEL% + exit /b 1 ) cd ..\.. ) else ( echo "Skipping project compilation (compile_project=false)" -) \ No newline at end of file +) 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/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/.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/.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/.github/ci/policy/check_templates.py b/.github/ci/policy/check_templates.py new file mode 100644 index 000000000..6b2ac1990 --- /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; +stage_compile_templates.py can add complete templates to disposable application checkouts. +""" +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/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/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..1dcdb6bcc --- /dev/null +++ b/.github/ci/policy/tests/template_fixtures/kotlin_dependencies.kt @@ -0,0 +1,54 @@ +// 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 = ""; var AGORA_APP_CERT = "" } +object SettingPreferences { fun getArea() = 0 } +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..3b2c82566 --- /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) + BuildConfig.AGORA_APP_CERT = "enabled-in-test" + request("missing-token"); respond(count, null); check(joins() == 2) + 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") }) + // 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/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/compile.yml b/.github/workflows/compile.yml new file mode 100644 index 000000000..5ca135c9b --- /dev/null +++ b/.github/workflows/compile.yml @@ -0,0 +1,163 @@ +name: Compile CI + +on: + pull_request: + 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 + 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: 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 /p:PlatformToolset=v143 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/repository-policy.yml b/.github/workflows/repository-policy.yml new file mode 100644 index 000000000..949a4f44b --- /dev/null +++ b/.github/workflows/repository-policy.yml @@ -0,0 +1,231 @@ +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 + + 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/.gitignore b/.gitignore index 606b91bdf..e6cd698e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ xcuserdata .DS_Store +.venv/ AgoraRtcKit.framework */libs /sdk +/scope +Android/APIExample/app/src/main/assets/beauty_agora/ diff --git a/.gitleaks.toml b/.gitleaks.toml index 856063fee..4a15002b9 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -33,29 +33,17 @@ 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" -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" @@ -124,22 +112,11 @@ 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)$''', '''(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 af39f90f7..e63206c84 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,13 +25,24 @@ Each platform directory contains its own `AGENTS.md` with platform-specific rule | macOS | `macOS/AGENTS.md` | | Windows | `windows/AGENTS.md` | +## AI Engineering Workflow + +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. + +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 1. Never share source files, build scripts, or SDK dependencies between platforms. 2. Each platform manages its own SDK version — check the platform-level config file before assuming a version. 3. All examples follow the same structural pattern within their platform: one self-contained class per API feature, managing its own engine lifecycle. 4. Always call the SDK's leave-channel and destroy APIs when an example screen is closed. -5. SDK event/delegate callbacks may arrive on a background thread — always dispatch UI updates to the main thread. +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 @@ -40,17 +51,31 @@ Each platform directory contains its own `AGENTS.md` with platform-specific rule | `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` | 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 | ## 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 -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 | `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 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/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/.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..50ccaf625 --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md @@ -0,0 +1,54 @@ +--- +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 + +## 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/SKILL.md b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md new file mode 100644 index 000000000..bfc8c3491 --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,50 @@ +--- +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. + +## 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. [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 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 + +- 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..6efe7ee2b --- /dev/null +++ b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java @@ -0,0 +1,76 @@ +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + +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(); + 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. + } + } + + 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-Audio/ARCHITECTURE.md b/Android/APIExample-Audio/ARCHITECTURE.md index 1aab8f934..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 - │ │ ├── RhythmPlayer.java # [16] "Rhythm Player" — metronome/rhythm playback + │ │ ├── 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,12 +74,12 @@ 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 | | 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/README.md b/Android/APIExample-Audio/README.md index dcb062296..3254f8010 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 @@ -38,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 2567a8725..7bab1e8a2 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 真机设备 - 支持模拟器 @@ -37,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..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, @@ -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 c87d5c383..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 @@ -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(); @@ -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..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, @@ -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-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/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-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-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/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-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/.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..11ee023f8 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md @@ -0,0 +1,59 @@ +--- +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. +--- + +## 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. + +## 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 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 + +- 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 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 new file mode 100644 index 000000000..317628845 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,52 @@ +--- +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. [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 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. + +## 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` +- 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 + +- Adding XML, Fragment, or ViewBinding code +- Skipping docs or registration because the Composable compiles + +## Never + +- Never use `rememberSaveable` for `RtcEngine` +- 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 new file mode 100644 index 000000000..e78af86f6 --- /dev/null +++ b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt @@ -0,0 +1,191 @@ +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.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.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.IRtcEngineEventHandler.RtcStats +import io.agora.rtc2.RtcEngine +import io.agora.rtc2.RtcEngineConfig + +@Composable +fun ExampleCase() { + val context = LocalContext.current + var channelName by rememberSaveable { mutableStateOf("") } + var isJoined by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + val session = remember { + ExampleCaseSession(onJoined = { isJoined = it }, onError = { errorMessage = it }) + } + DisposableEffect(session) { + session.start(context) + onDispose { session.dispose() } + } + + 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 + if (session.beginJoin(newChannelName)) { + errorMessage = null + permissionLauncher.launch(requiredPermissions) + } + }, + 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 = BuildConfig.AGORA_APP_CERT.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 = {} + ) +} + +@Composable +private fun ExampleCaseView( + channelName: String, + isJoined: Boolean, + errorMessage: String?, + onJoinClick: (String) -> Unit, + onLeaveClick: () -> Unit +) { + // 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 3e63c9283..5a4a3dea7 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, 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())`. - `IRtcEngineEventHandler` callbacks are safe to mutate Compose state directly (snapshot system is thread-safe). @@ -29,9 +30,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..829dcbaa6 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 @@ -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 - │ ├── RhythmPlayer.kt # Advanced: "Rhythm Player" — metronome playback + │ ├── 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 | @@ -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 | @@ -127,11 +127,11 @@ 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`: +**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() } ) @@ -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 @@ -160,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() } } @@ -184,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-Compose/README.md b/Android/APIExample-Compose/README.md index e7455d89a..c555d4003 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 @@ -37,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 d5b3cc5c0..96d04f9ec 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 真机设备 - 支持模拟器 @@ -36,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-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 50b4f63f5..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 @@ -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 @@ -43,29 +42,28 @@ 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_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 +) 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/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-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/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-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/.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..4eb431f07 --- /dev/null +++ b/Android/APIExample/.agents/skills/review-case/SKILL.md @@ -0,0 +1,46 @@ +--- +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. 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. + +## 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 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 new file mode 100644 index 000000000..219efc5df --- /dev/null +++ b/Android/APIExample/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,52 @@ +--- +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. [fragment-template.java](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()` 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 new file mode 100644 index 000000000..27eb9a868 --- /dev/null +++ b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java @@ -0,0 +1,48 @@ +// 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(); + } + // 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 b5c9e8bd7..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. @@ -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 diff --git a/Android/APIExample/ARCHITECTURE.md b/Android/APIExample/ARCHITECTURE.md index b95e84cab..f4eb9b4e3 100644 --- a/Android/APIExample/ARCHITECTURE.md +++ b/Android/APIExample/ARCHITECTURE.md @@ -45,51 +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 - │ │ ├── FaceCapture.java # [23] "Face Capture" — face detection - │ │ ├── VideoQuickSwitch.java # [24] "Quick Switch Channel" — fast channel switching - │ │ ├── JoinMultipleChannel.java # [25] "Join Multiple Channel" — multi-channel join - │ │ ├── ChannelEncryption.java # [26] "Media Stream Encryption" — built-in encryption - │ │ ├── PlayAudioFiles.java # [27] "Play Audio Files" — audio mixing - │ │ ├── PreCallTest.java # [28] "Pre-call Tests" — network/device test before joining - │ │ ├── MediaPlayer.java # [29] "MediaPlayer" — play media files - │ │ ├── MediaRecorder.java # [30] "Local/Remote MediaRecorder" — record media streams - │ │ ├── ScreenSharing.java # [31] "Screen Sharing" — screen capture & share - │ │ ├── VideoProcessExtension.java # [32] "Video Process Extension" — video filter extension - │ │ ├── LocalVideoTranscoding.java # [33] "LocalVideoTranscoding" — local video compositing - │ │ ├── RhythmPlayer.java # [34] "Rhythm Player" — metronome/rhythm playback + │ │ ├── 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/ @@ -119,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 | @@ -128,7 +127,6 @@ APIExample/ | Raw Video Data | `advanced/ProcessRawData.java` | `registerVideoFrameObserver()`, `startPreview()` | Demonstrates processing raw video data through the video frame observer | | Simple Extension | `advanced/SimpleExtension.java` | `enableExtension()`, `setExtensionProperty()`, `enableAudioVolumeIndication()` | Demonstrates loading and configuring a custom audio/video extension | | Picture In Picture | `advanced/PictureInPicture.java` | `joinChannel()`, `setupLocalVideo()`, `enableVideo()` | Demonstrates Android Picture-in-Picture mode during a video call | -| Face Capture | `advanced/FaceCapture.java` | `enableExtension()`, `setExtensionProperty()`, `registerVideoFrameObserver()`, `registerFaceInfoObserver()` | Demonstrates face capture and lip-sync driven video using extensions | | Quick Switch Channel | `advanced/VideoQuickSwitch.java` | `joinChannel()`, `leaveChannel()`, `startPreview()`, `setClientRole()` | Demonstrates fast channel switching for audience members | | Join Multiple Channel | `advanced/JoinMultipleChannel.java` | `joinChannel()`, `joinChannelEx()`, `leaveChannelEx()`, `startPreview()`, `takeSnapshotEx()` | Demonstrates joining two channels simultaneously using RtcEngineEx | | Media Stream Encryption | `advanced/ChannelEncryption.java` | `enableEncryption()` | Demonstrates built-in media stream encryption | @@ -136,19 +134,19 @@ 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 | `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 from the case list because the Rhythm Player APIs are deprecated since RTC SDK 4.6.0 | | Send Data Stream | `advanced/SendDataStream.java` | `createDataStream()`, `sendStreamMessage()` | Demonstrates sending and receiving data channel messages | | Relay Streams across Channels | `advanced/HostAcrossChannel.java` | `startOrUpdateChannelMediaRelay()`, `stopChannelMediaRelay()`, `pauseAllChannelMediaRelay()`, `resumeAllChannelMediaRelay()` | Demonstrates relaying media streams from one channel to another | | Spatial Audio | `advanced/SpatialSound.java` | `enableAudio()`, `setRemoteUserSpatialAudioParams()`, `createMediaPlayer()` | Demonstrates 3D spatial audio positioning for remote users | | Content Inspect | `advanced/ContentInspect.java` | `enableContentInspect()` | Demonstrates real-time content moderation on video streams | -| Third-party beauty | `advanced/ThirdPartyBeauty.java` | `registerVideoFrameObserver()` | Demonstrates integration with third-party beauty SDKs (e.g. FaceUnity) | +| 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 | -| Agora beauty 2.0 | `advanced/AgoraBeauty.java` | `enableVirtualBackground()`, `setFaceShapeAreaOptions()` | 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/README.md b/Android/APIExample/README.md index f80ba4096..1f6a42f2a 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,14 +46,41 @@ 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: -#### Agora +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. -1. Contact Agora technical support to obtain beauty resources -2. Put the beauty resources in the corresponding path +#### Agora Beauty 2.0 -| Agora Beauty Resources | Location | -|------------------------|----------------------------------| -| beauty resources | app/src/main/assets/beauty_agora | +The Agora Beauty 2.0 material package is not included in this repository. Before building locally: + +1. Obtain `AgoraBeautyMaterial.zip` from Agora technical support. +2. Extract `AgoraBeautyMaterial.zip` to `app/src/main/assets/beauty_agora/`. + +The extracted resources should have the following structure: + +```text +app/src/main/assets/beauty_agora/ +└── beauty_material_functional/ + ├── config.json + └── ... +``` + +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/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 @@ -97,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 @@ -117,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 e38beb419..0a1e94fd7 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,14 +43,38 @@ 本项目包含第三方美颜集成示例,在没有配置资源和证书的情况下,默认是无法启用的。资源证书配置方法如下: -#### Agora 美颜 +Gradle 配置阶段会根据 `app/vendors.gradle` 自动下载并缓存商汤 9.3.1 Android 依赖库,因此首次 Gradle Sync 需要联网。该下载不包含下方列出的商汤素材和许可证,这些文件仍需单独提供。 -1. 联系 Agora 技术支持获取美颜资源 -2. 将美颜资源放到对应路径下 +#### Agora 美颜 2.0 -| 美颜资源 | 项目路径 | -|------|----------------------------------| -| 美颜资源 | app/src/main/assets/beauty_agora | +Agora 美颜 2.0 资源包不包含在仓库中。本地构建前: + +1. 从 Agora 技术支持获取 `AgoraBeautyMaterial.zip`。 +2. 将 `AgoraBeautyMaterial.zip` 解压到 `app/src/main/assets/beauty_agora/`。 + +解压后的资源目录结构如下: + +```text +app/src/main/assets/beauty_agora/ +└── beauty_material_functional/ + ├── config.json + └── ... +``` + +该资源目录已被 Git 忽略。 + +#### 商汤美颜 + +1. 联系商汤客服获取美颜sdk下载链接以及证书 +2. 解压美颜sdk,并将以下资源复制到对应路径 + +| 商汤SDK文件/目录 | 项目路径 | +|----------------------------------------------------------------------|----------------------------------------------------------| +| Android/models | app/src/main/assets/beauty_sensetime/models | +| 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 | #### 相芯美颜 @@ -91,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 @@ -110,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/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraBase.h index acc302ea3..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 @@ -292,6 +292,20 @@ enum CHANNEL_PROFILE_TYPE { CHANNEL_PROFILE_COMMUNICATION_1v1 __deprecated = 4, }; +/** + * The channel types. + */ +enum CHANNEL_TYPE { + /** + * 0: Standard channel type. + */ + CHANNEL_TYPE_STANDARD = 0, + /** + * 1: Channel type with a large-scale scenario, for example, more than 200 broadcasters. + */ + CHANNEL_TYPE_LARGE_SCALE = 1, +}; + /** * The warning codes. */ @@ -403,6 +417,10 @@ enum WARN_CODE_TYPE { * 1032: Audio Device Module: The playback audio volume is too low. */ WARN_ADM_PLAYOUT_AUDIO_LOWLEVEL = 1032, + /** + * 1033: Audio device module: The audio capturing device is occupied. + */ + WARN_ADM_RECORD_IS_OCCUPIED = 1033, /** * 1040: Audio device module: An exception occurs with the audio drive. * Choose one of the following solutions: @@ -789,6 +807,71 @@ enum ERROR_CODE_TYPE { * camera permission is granted. */ ERR_VDM_CAMERA_NOT_AUTHORIZED = 1501, + + // Video Effect (Beauty) errors: 1700~1799 + /** + * 1700: The video effect asset is invalid. The asset bundle may be corrupted, + * the configuration file cannot be parsed, or required fields are missing. + */ + ERR_VIDEOEFFECT_ASSET_INVALID = 1700, + /** + * 1701: Failed to save the video effect configuration. The file write + * operation failed, possibly due to insufficient disk space or permission issues. + */ + ERR_VIDEOEFFECT_SAVE_FAILED = 1701, + /** + * 1702: The video effect engine is in an invalid state. The beauty processing + * engine is not initialized or the render engine instance does not exist. + */ + ERR_VIDEOEFFECT_ENGINE_INVALID = 1702, + /** + * 1704: The target video effect node is not active. Call + * addOrUpdateVideoEffect to activate the node first. + */ + ERR_VIDEOEFFECT_NODE_NOT_ACTIVE = 1704, + /** + * 1705: The video effect parameter is invalid. For example, the nodeId, + * actionId, option, or key is invalid or empty. + */ + ERR_VIDEOEFFECT_INVALID_PARAM = 1705, + /** + * 1706: The video effect is not supported on this device. + */ + ERR_VIDEOEFFECT_NOT_SUPPORTED = 1706, + /** + * 1707: The video effect bundle path is invalid. The path is empty or + * does not exist on the file system. + */ + ERR_VIDEOEFFECT_INVALID_BUNDLE_PATH = 1707, + /** + * 2007: Audio Device Module: An error occurs in starting the application loopback. + */ + ERR_ADM_APPLICATION_LOOPBACK = 2007, + /** + * 2008: Audio Device Module: The application loopback was stopped unexpectedly, typically due to the application exiting or the user disabling loopback. + */ + ERR_ADM_APPLICATION_LOOPBACK_STOPPED = 2008, + /** + * 2009: Audio Device Module: An error occurred while starting the system loopback. + * This may be due to system restrictions, device conflicts, or other internal errors. + */ + ERR_ADM_SYSTEM_LOOPBACK = 2009, + /** + * 2010: Audio Device Module: The system loopback was stopped unexpectedly, typically due to the user disabling loopback or a system error occurred. + */ + ERR_ADM_SYSTEM_LOOPBACK_STOPPED = 2010, + /** + * 2011: Audio Device Module: No permission to start the loopback. + */ + ERR_ADM_LOOPBACK_NO_PERMISSION = 2011, + /** + * 2012: Audio Device Module: Silent detected in loopback. + */ + ERR_ADM_LOOPBACK_SILENT_DETECTED = 2012, + /** + * 2013: Audio Device Module: Silent recovered in loopback. + */ + ERR_ADM_LOOPBACK_SILENT_RECOVERED = 2013, }; enum LICENSE_ERROR_TYPE { @@ -1128,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, /** @@ -2253,11 +2337,12 @@ struct SimulcastStreamConfig { /** * @brief Configure video streams of different quality levels. * - * @since v4.6.0 + * @technical preview */ struct SimulcastConfig { /** * @brief Index of video streams of different quality levels. + * @technical preview */ enum StreamLayerIndex { /** @@ -2295,7 +2380,7 @@ struct SimulcastConfig { }; /** * @brief Configures the parameters of a specific layer in multi-quality video streams. - * + * @technical preview * @details * Used to configure the resolution, frame rate, and enable status of a specific layer in * multi-quality video streams. @@ -2631,7 +2716,6 @@ struct WatermarkConfig { enum MultipathMode { /** * Duplicate mode, the same piece of data is redundantly transmitted over all available paths. - * @technical preview */ Duplicate= 0, /** @@ -5155,6 +5239,8 @@ enum CONNECTION_CHANGED_REASON_TYPE { CONNECTION_CHANGED_CLIENT_IP_ADDRESS_CHANGED_BY_USER = 18, /** * 19: The user joined the same channel from different devices with the same UID. + * @note Joining a channel with the same UID is an undefined behavior. Agora does not guarantee + * `reason` 19 to be triggered every time such a behavior occurs. */ CONNECTION_CHANGED_SAME_UID_LOGIN = 19, /** @@ -5341,6 +5427,11 @@ struct VideoCanvas { */ media::base::VIDEO_MODULE_POSITION position; + /** + * The clockwise rotation information. You can set it as 0, 90, 180 or 270. + */ + VIDEO_ORIENTATION rotation; + VideoCanvas() : uid(0), subviewUid(0), @@ -5353,7 +5444,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt) : uid(0), @@ -5367,7 +5459,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt, uid_t u) : uid(u), @@ -5381,7 +5474,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt, uid_t u, uid_t subu) @@ -5396,7 +5490,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} }; /** @@ -5485,7 +5580,7 @@ struct FaceShapeAreaOptions { FACE_SHAPE_AREA_NONE = -1, /** * (100): Head, used to achieve a smaller head effect. The value range is 0 to 100, and the default - * value is 50. The larger the value, the more noticeable the adjustment. + * value is 0. The larger the value, the more noticeable the adjustment. */ FACE_SHAPE_AREA_HEADSCALE = 100, /** @@ -5532,9 +5627,15 @@ struct FaceShapeAreaOptions { * opposite direction. */ FACE_SHAPE_AREA_CHIN = 108, + /** + * (109): Face Small, reduces the overall face size for a slimmer appearance. + * The value range is [0, 100]. The default value is 0. + * The larger the value, the more pronounced the overall face size reduction effect. + */ + FACE_SHAPE_AREA_FACESMALL = 109, /** * (200): Eyes, used to achieve a larger eye effect. The value range is 0 to 100, and the default - * value is 50. The larger the value, the more noticeable the adjustment. + * value is 0. The larger the value, the more noticeable the adjustment. */ FACE_SHAPE_AREA_EYESCALE = 200, /** @@ -5577,6 +5678,12 @@ struct FaceShapeAreaOptions { * @since v4.6.0 */ FACE_SHAPE_AREA_EYEOUTERCORNER = 206, + /** + * (207): Eye Angle, adjusts the tilt angle of the eyes (cat-eye effect). + * The value range is [-100, 100]. The default value is 0. + * The larger the value, the more the outer corners of the eyes are lifted upward. + */ + FACE_SHAPE_AREA_EYEANGLE = 207, /** * (300): Nose length, used to achieve a longer nose effect. The range is [-100, 100], with a * default value of 0. @@ -5589,7 +5696,7 @@ struct FaceShapeAreaOptions { */ FACE_SHAPE_AREA_NOSEWIDTH = 301, /** - * (302): Nose wing adjustment. The value range is 0 to 100, and the default value is 10. The larger + * (302): Nose wing adjustment. The value range is 0 to 100, and the default value is 0. The larger * the value, the more noticeable the adjustment. * @since v4.6.0 */ @@ -5601,19 +5708,19 @@ struct FaceShapeAreaOptions { */ FACE_SHAPE_AREA_NOSEROOT = 303, /** - * (304): Nose bridge adjustment. The value range is 0 to 100, and the default value is 50. The + * (304): Nose bridge adjustment. The value range is 0 to 100, and the default value is 0. The * larger the value, the more noticeable the adjustment. * @since v4.6.0 */ FACE_SHAPE_AREA_NOSEBRIDGE = 304, /** - * (305): Nose tip adjustment. The value range is 0 to 100, and the default value is 50. The larger + * (305): Nose tip adjustment. The value range is 0 to 100, and the default value is 0. The larger * the value, the more noticeable the adjustment. * @since v4.6.0 */ FACE_SHAPE_AREA_NOSETIP = 305, /** - * (306): Overall nose adjustment. The range is [-100, 100], with a default value of 50. The greater + * (306): Overall nose adjustment. The range is [-100, 100], with a default value of 0. The greater * the absolute value, the more noticeable the adjustment. Negative values indicate the opposite * direction. * @since v4.6.0 @@ -5621,7 +5728,7 @@ struct FaceShapeAreaOptions { FACE_SHAPE_AREA_NOSEGENERAL = 306, /** * (400): Mouth, used to achieve a larger mouth effect. The range is [-100, 100], with a default - * value of 20. The greater the absolute value, the more noticeable the adjustment. Negative values + * value of 0. The greater the absolute value, the more noticeable the adjustment. Negative values * indicate the opposite direction. * @since v4.6.0 */ @@ -6097,6 +6204,75 @@ struct AudioTrackConfig { AudioTrackConfig() : enableLocalPlayback(true),enableAudioProcessing(false) {} }; +/** The type of loopback audio source mode +*/ +enum LOOPBACK_AUDIO_TRACK_TYPE { + /** + * 0: loopback the whole system + */ + LOOPBACK_SYSTEM = 0, + /** + * 1: loopback the whole system exclude self + */ + LOOPBACK_SYSTEM_EXCLUDE_SELF = 1, + /** + * 2: loopback the specific application + */ + LOOPBACK_APPLICATION = 2, + /** + * 3: loopback the specific process + */ + LOOPBACK_PROCESS = 3, +}; + +/** Defines options for a custom loopback audio track. */ +struct LoopbackAudioTrackConfig { + /** + * Specifies the loopback source type. + * Possible values are defined by LOOPBACK_AUDIO_TRACK_TYPE (e.g., system audio, specific application, or process). + * Default: LOOPBACK_SYSTEM. + */ + LOOPBACK_AUDIO_TRACK_TYPE loopbackType; + + /** + * Initial playback volume for the loopback audio, valid in the range [0, 400]. + * 0: mute; 100: original volume; 400: maximum amplified volume. + * Default is 100. + */ + int volume; + + /** + * Playback device name to capture loopback audio from. + * + * Platform-specific notes: + * - **Windows:** Not supported for now. The `deviceName` parameter is ignored on Windows. + * - **macOS:** For `LOOPBACK_SYSTEM` or `LOOPBACK_SYSTEM_EXCLUDE_SELF`, if set, uses the named virtual device; if NULL, uses the default. + * For `LOOPBACK_APPLICATION` and `LOOPBACK_PROCESS`, ignored. + * + * If set, the name must exactly match the target playback device. + * Set to NULL to use the platform default selection. + */ + const char* deviceName; + + /** + * Name of the application to capture audio from. + * Only used if loopbackType is LOOPBACK_APPLICATION; otherwise ignored. + * Must be an exact, case-sensitive match of the application name. + * Set to NULL if not capturing a specific application. + */ + const char* appName; + + /** + * Process ID of the target application to capture audio from. + * Only used if loopbackType is LOOPBACK_PROCESS; otherwise ignored. + * Default is -1. + */ + unsigned int processId; + + LoopbackAudioTrackConfig() + : loopbackType(LOOPBACK_SYSTEM), volume(100), deviceName(NULL), appName(NULL), processId(-1) {} +}; + /** * Preset local voice reverberation options. * bitmap allocation: @@ -6484,6 +6660,7 @@ struct ScreenAudioParameters { struct ScreenCaptureParameters { /** + * @technical preview * Determines whether to capture system audio during screen sharing: * - `true`: Capture. * - `false`: (Default) Do not capture. @@ -6494,6 +6671,7 @@ struct ScreenCaptureParameters { */ bool captureAudio; /** + * @technical preview * The audio configuration for the shared screen stream. * @note This parameter only takes effect when `captureAudio` is `true`. * See `ScreenAudioParameters`. diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h index 9a287d374..be639d56d 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaBase.h @@ -572,6 +572,7 @@ enum VIDEO_PIXEL_FORMAT { /** * 17: The ID3D11TEXTURE2D format. Currently supported types are `DXGI_FORMAT_B8G8R8A8_UNORM`, * `DXGI_FORMAT_B8G8R8A8_TYPELESS` and `DXGI_FORMAT_NV12`. + * @note This enumeration applies to screen-captured video only. Camera-captured video is not supported. */ VIDEO_TEXTURE_ID3D11TEXTURE2D = 17, /** @@ -1170,7 +1171,7 @@ struct VideoFrame { }; /** - * The IVideoFrameObserver class. + * @brief The IVideoFrameObserver class. */ class IVideoFrameObserver { public: @@ -1386,7 +1387,10 @@ struct SnapshotConfig { const char* filePath; /** - * The position of the snapshot video frame in the video pipeline. See `VIDEO_MODULE_POSITION`. + * The position of the video observation. See `VIDEO_MODULE_POSITION`. + * Allowed values vary depending on the `uid` parameter passed in `takeSnapshot` or `takeSnapshotEx`: + * - `uid` = 0: Position 2, 4 and 8 are allowed. + * - `uid` != 0: Only position 2 is allowed. */ media::base::VIDEO_MODULE_POSITION position; SnapshotConfig() :filePath(NULL), position(media::base::POSITION_PRE_ENCODER) {} @@ -1688,13 +1692,14 @@ class IAudioFrameObserverBase { */ virtual bool onEarMonitoringAudioFrame(AudioFrame& audioFrame) = 0; /** - * Occurs when the before-mixing playback audio frame is received. - * @param channelId The channel name - * @param userId ID of the remote user. - * @param audioFrame The reference to the audio frame: AudioFrame. + * @brief Retrieves the audio frame before mixing of subscribed remote users. + * + * @param channelId The channel ID. + * @param userId The ID of subscribed remote users. + * @param audioFrame The raw audio data. See `AudioFrame`. + * * @return - * - true: The before-mixing playback audio frame is valid and is encoded and sent. - * - false: The before-mixing playback audio frame is invalid and is not encoded or sent. + * Without practical meaning. */ virtual bool onPlaybackAudioFrameBeforeMixing(const char* channelId, base::user_id_t userId, AudioFrame& audioFrame) { @@ -1949,7 +1954,7 @@ class IVideoEncodedFrameObserver { }; /** - * The IVideoFrameObserver class. + * @brief The IVideoFrameObserver class. */ class IVideoFrameObserver { public: @@ -2022,7 +2027,7 @@ class IVideoFrameObserver { VideoFrame& videoFrame) = 0; /** - * @brief Occurs each time the SDK receives a video frame before encoding. + * @brief Occurs each time the SDK receives a pre-encoded video frame. * * @details * After you successfully register the video frame observer, the SDK triggers this callback each @@ -2455,6 +2460,9 @@ struct MediaRecorderConfiguration { videoSourceType(rtc::VIDEO_SOURCE_CAMERA_PRIMARY) {} }; +/** + * @brief The IFaceInfoObserver class. + */ class IFaceInfoObserver { public: /** @@ -2528,6 +2536,9 @@ struct RecorderInfo { : fileName(name), durationMs(dur), fileSize(size) {} }; +/** + * @brief The IMediaRecorderObserver class. + */ class IMediaRecorderObserver { public: /** diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h index ceb0642ea..db76901cc 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/AgoraMediaPlayerTypes.h @@ -500,7 +500,7 @@ struct PlayerUpdatedInfo { }; /** - * The custom data source provides a data stream input callback, and the player will continue to call back this interface, requesting the user to fill in the data that needs to be played. + * @brief The custom data source provides a data stream input callback, and the player will continue to call back this interface, requesting the user to fill in the data that needs to be played. */ class IMediaPlayerCustomDataProvider { public: 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/IAgoraMediaPlayerSource.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaPlayerSource.h index 563497881..7fd741095 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaPlayerSource.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaPlayerSource.h @@ -28,6 +28,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the unique source ID of the media player source. * @return + * @technical preview * - >=0: The source ID of this media player source. * - < 0: Failure. */ @@ -35,6 +36,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Opens a media file with a specified URL. + * @technical preview * @param url The path of the media file. Both the local path and online path are supported. * @param startPos The starting position (ms) for playback. Default value is 0. * @return @@ -45,6 +47,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Opens a media file with a media file source. + * @technical preview * @param source Media file source that you want to play, see `MediaSource` * @return * - 0: Success. @@ -54,6 +57,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Plays the media file. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -62,6 +66,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Pauses the playback. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -70,6 +75,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Stops the playback. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -78,6 +84,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Resumes the playback. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -86,6 +93,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Sets the playback position of the media file. + * @technical preview * @param newPos The new playback position (ms). * @return * - 0: Success. @@ -95,6 +103,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the duration of the media file. + * @technical preview * @param [out] duration A reference to the duration of the media file. * @return * - 0: Success. @@ -104,6 +113,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the current playback position of the media file. + * @technical preview * @param [out] pos A reference to the current playback position (ms). * @return * - 0: Success. @@ -113,6 +123,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the number of the media streams in the media source. + * @technical preview * @param [out] count The number of the media streams in the media source. * @return * - 0: Success. @@ -122,6 +133,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the detailed information of a media stream. + * @technical preview * @param index The index of the media stream. * @param [out] info The detailed information of the media stream. See \ref media::base::PlayerStreamInfo "PlayerStreamInfo" for details. * @return @@ -132,6 +144,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Sets whether to loop the media file for playback. + * @technical preview * @param loopCount The number of times of looping the media file. * - 0: Play the media file once. * - 1: Play the media file twice. @@ -144,6 +157,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Changes the playback speed. + * @technical preview * @param speed The playback speed ref [50-400]. * @return * - 0: Success. @@ -153,6 +167,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Selects an audio track of the media file for playback. + * @technical preview * @param index The index of the audio track in media file. * @return * - 0: Success. @@ -162,6 +177,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Selects multi audio track of the media file for playback or publish to channel. + * @technical preview * @param playoutTrackIndex The index of the audio track in media file for local playback. * @param publishTrackIndex The index of the audio track in the media file published to the remote. * @@ -179,6 +195,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Changes the player option before playing a file. + * @technical preview * @param key The key of the option paramemter. * @param value The value of option parameter. * @return @@ -189,6 +206,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Changes the player option before playing a file. + * @technical preview * @param key The key of the option paramemter. * @param value The value of option parameter. * @return @@ -199,6 +217,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Takes a screenshot when playing a video file. + * @technical preview * @param filename The filename of the screenshot file. * @return * - 0: Success. @@ -208,6 +227,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Selects internal subtitles for a video file. + * @technical preview * @param index The index of the internal subtitles. * @return * - 0: Success. @@ -217,6 +237,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Sets an external subtitle file for a video file. + * @technical preview * @param url The URL of the subtitle file. * @return * - 0: Success. @@ -226,6 +247,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the playback state. + * @technical preview * @return The current playback state. See {@link media::base::MEDIA_PLAYER_STATE MEDIA_PLAYER_STATE} for details. */ virtual media::base::MEDIA_PLAYER_STATE getState() = 0; @@ -233,6 +255,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Registers a media player source observer. * + * @technical preview * Once the media player source observer is registered, you can use the observer to monitor the state change of the media player. * @param observer The pointer to the IMediaPlayerSourceObserver object. * @return @@ -243,6 +266,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Releases the media player source observer. + * @technical preview * @param observer The pointer to the IMediaPlayerSourceObserver object. * @return * - 0: Success. @@ -252,7 +276,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Registers the audio frame observer. - * + * @technical preview * @param observer The pointer to the {@link media::IAudioPcmFrameSink observer} object. * @return * - 0: Success. @@ -262,6 +286,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Releases the audio frame observer. + * @technical preview * @param observer The pointer to the {@link media::IAudioPcmFrameSink observer} object. * @return * - 0: Success. @@ -271,6 +296,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Open the Agora CDN media source. + * @technical preview * @param src The src of the media file that you want to play. * @param startPos The playback position (ms). * @@ -284,7 +310,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the number of Agora CDN lines. - * + * @technical preview * @deprecated 4.6.0 * * @return @@ -296,7 +322,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Switch Agora CDN lines. - * + * @technical preview * @deprecated 4.6.0 * * @param index Specific CDN line index. @@ -308,7 +334,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the line of the current CDN. - * + * @technical preview * @deprecated 4.6.0 * * @return @@ -319,7 +345,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Enable automatic CDN line switching. - * + * @technical preview * @deprecated 4.6.0 * * @param enable Whether enable. @@ -331,7 +357,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Update the CDN source token and timestamp. - * + * @technical preview * @deprecated 4.6.0 * * @param token token. @@ -344,7 +370,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Switch the CDN source when open a media through "openWithAgoraCDNSrc" API - * + * @technical preview * @deprecated 4.6.0 * * @param src Specific src. @@ -357,6 +383,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Switch the media source when open a media through "open" API + * @technical preview * @param src Specific src. * @param syncPts Live streaming must be set to false. * @return @@ -367,6 +394,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Preload a media source + * @technical preview * @param src Specific src. * @param startPos The starting position (ms) for playback. Default value is 0. * @return @@ -377,6 +405,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Unload a preloaded media source + * @technical preview * @param src Specific src. * @return * - 0: Success. @@ -386,6 +415,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Play a pre-loaded media source + * @technical preview * @param src Specific src. * @return * - 0: Success. diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaRecorder.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaRecorder.h index b10ee2b37..46a0dc9bb 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaRecorder.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraMediaRecorder.h @@ -11,6 +11,9 @@ namespace agora { namespace rtc { +/** + * @brief The IMediaRecorder interface. + */ class IMediaRecorder : public RefCountInterface { protected: virtual ~IMediaRecorder() {} @@ -32,7 +35,7 @@ class IMediaRecorder : public RefCountInterface { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int setMediaRecorderObserver(media::IMediaRecorderObserver* callback) = 0; /** @@ -69,7 +72,7 @@ class IMediaRecorder : public RefCountInterface { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. * - -2: Invalid parameter. Please ensure that: * - The specified recording file path is correct and writable. * - The specified recording file format is correct. diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraService.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraService.h index a886b598f..49a555a37 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraService.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/IAgoraService.h @@ -165,6 +165,13 @@ struct AgoraServiceConfiguration { */ bool domainLimit; + /** + * Provides the technical preview functionalities or special customizations by configuring the SDK with JSON options. + * Pointer to the set parameters in a JSON string. + * @technical preview + */ + const char* parameters; + AgoraServiceConfiguration() : enableAudioProcessor(true), enableAudioDevice(true), enableVideo(false), @@ -177,7 +184,8 @@ struct AgoraServiceConfiguration { useStringUid(false), serviceObserver(NULL), useExternalEglContext(false), - domainLimit(false){} + domainLimit(false), + parameters(NULL){} }; /** * The audio session configurations. @@ -577,21 +585,6 @@ class IAgoraService { virtual agora_refptr createDirectCustomAudioTrack( agora_refptr audioSource) = 0; - /** - * Creates a local audio track object with a PCM data sender and returns the pointer. - * - * Once created, this track can be used to send PCM audio data. - * - * @param audioSource The pointer to the PCM audio data sender: \ref agora::rtc::IAudioPcmDataSender "IAudioPcmDataSender". - * @param enableAec Whether enable audio echo cancellation for PCM audio data. - * @return - * - The pointer to \ref rtc::ILocalAudioTrack "ILocalAudioTrack": Success. - * - A null pointer: Failure. - * - `INVALID_STATE`, if `enableAudioProcessor` in \ref agora::base::AgoraServiceConfiguration "AgoraServiceConfiguration" is set as `false`. - */ - virtual agora_refptr createCustomAudioTrack( - agora_refptr audioSource, bool enableAec) = 0; - /** * Creates a local audio track object with a audio mixer source and returns the pointer. * @@ -671,12 +664,13 @@ class IAgoraService { * @param enableAec Whether enable audio echo cancellation for loopback recording. If loopback * recording device is a virtual sound card, it should be false, or it should be true. * @param overlap Whether overlap playout signal. + * @param standaloneAdm Whether use standalone ADM. * @return * - The pointer to \ref rtc::ILocalAudioTrack "ILocalAudioTrack": Success. * - A null pointer: Failure. */ virtual agora_refptr createRecordingDeviceAudioTrack( - agora_refptr audioSource, bool enableAec, bool overlap) = 0; + agora_refptr audioSource, bool enableAec, bool overlap, bool standaloneAdm) = 0; /** * Creates an audio device manager object and returns the pointer. diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraAudioTrack.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraAudioTrack.h index ef81421f6..fce4b4355 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraAudioTrack.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraAudioTrack.h @@ -243,6 +243,13 @@ class ILocalAudioTrackObserver { */ virtual void onLocalAudioTrackStateChanged(LOCAL_AUDIO_STREAM_STATE state, LOCAL_AUDIO_STREAM_REASON reasonCode) = 0; + + /** + * Occurs when the error of a local audio track occurs. + * + * @param errorCode The error code of the local audio track. + */ + virtual void onLocalAudioTrackError(int errorCode) = 0; }; /** @@ -921,6 +928,15 @@ class IRemoteAudioTrack : public IAudioTrack { */ virtual int adjustAudioDeceleration(int percentage, aosl_ref_t ares = AOSL_REF_INVALID) = 0; + /** enable audio fast acceleration during audio packets burst arrival. + + @param enable Whether to enable audio burst acceleration. + @return + - 0: Success. + - < 0: Failure. + */ + virtual int enableAudioBurstAccelerate(bool enable, aosl_ref_t ares = AOSL_REF_INVALID) = 0; + /** enable spatial audio @param enabled enable/disable spatial audio: diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraLocalUser.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraLocalUser.h index dd0cc0714..08e2b8896 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraLocalUser.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraLocalUser.h @@ -1214,6 +1214,18 @@ struct AudioVolumeInformation { */ class ILocalUserObserver { public: + enum USER_STREAMS_BITMASK { + USER_STREAMS_BITMASK_AUDIO = 1 << 0, + USER_STREAMS_BITMASK_VIDEO_LOW = 1 << 1, + USER_STREAMS_BITMASK_VIDEO_HIGH = 1 << 2, + USER_STREAMS_BITMASK_VIDEO_LAYER_1 = 1 << 3, + USER_STREAMS_BITMASK_VIDEO_LAYER_2 = 1 << 4, + USER_STREAMS_BITMASK_VIDEO_LAYER_3 = 1 << 5, + USER_STREAMS_BITMASK_VIDEO_LAYER_4 = 1 << 6, + USER_STREAMS_BITMASK_VIDEO_LAYER_5 = 1 << 7, + USER_STREAMS_BITMASK_VIDEO_LAYER_6 = 1 << 8, + }; + virtual ~ILocalUserObserver() {} virtual void onAudioTrackPublishStart(agora_refptr audioTrack) = 0; @@ -1586,6 +1598,14 @@ class ILocalUserObserver { */ virtual void onUserStateChanged(user_id_t userId, uint32_t state){} + /** + * Occurs when the remote user published streams are updated. + * @param uid The uid of the remote user. + * @param streams The remote user published streams in bitmask. + * See #USER_STREAMS_BITMASK. + */ + virtual void onUserStreamsChanged(user_id_t userId, uint32_t streams) {} + virtual void onVideoRenderingTracingResult(user_id_t user_id, MEDIA_TRACE_EVENT currentState, VideoRenderingTracingInfo tracingInfo) {} /** Occurs when receive rdt message. diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraMediaNode.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraMediaNode.h index 760390032..efcdc1f46 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraMediaNode.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraMediaNode.h @@ -585,6 +585,20 @@ class IVideoRenderer : public IVideoSinkBase { * - < 0: Failure. */ virtual int setRenderMode(void* view, media::base::RENDER_MODE_TYPE renderMode, aosl_ref_t ares = AOSL_REF_INVALID) = 0; + /** + * Sets the render rotation. + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int setRotation(agora::rtc::VIDEO_ORIENTATION rotation, aosl_ref_t ares = AOSL_REF_INVALID) = 0; + /** + * Sets the render rotation. + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int setRotation(void* view, agora::rtc::VIDEO_ORIENTATION rotation, aosl_ref_t ares = AOSL_REF_INVALID) = 0; /** * Sets whether to mirror the video. * @param mirror Whether to mirror the video: diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraRtcConnection.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraRtcConnection.h index d7f150a53..4d254974d 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraRtcConnection.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraRtcConnection.h @@ -155,6 +155,11 @@ struct RtcConnectionConfiguration { */ bool isDataChannelOnly; + /** + * Indicates the connection type. + */ + CHANNEL_TYPE channelType; + RtcConnectionConfiguration() : autoSubscribeAudio(true), autoSubscribeVideo(true), @@ -168,7 +173,8 @@ struct RtcConnectionConfiguration { audioRecvMediaPacket(false), videoRecvMediaPacket(false), isInteractiveAudience(false), - isDataChannelOnly(false) {} + isDataChannelOnly(false), + channelType(CHANNEL_TYPE_STANDARD) {} }; /** diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraVideoFrame.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraVideoFrame.h index f59ab5627..e7f0bfa7d 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraVideoFrame.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/NGIAgoraVideoFrame.h @@ -21,6 +21,30 @@ namespace agora { namespace rtc { +/** + * This structure defines the video frame of texture type on Android + * @note For technical preview, not supported for the moment. Use RawPixelBuffer instead. + * + */ +struct TextureInfo { + OPTIONAL_ENUM_CLASS TextureType { + kGlTextureOes, + kGlTexture2D, + }; + OPTIONAL_ENUM_CLASS EglContextType { + kEglContext10, + kEglContext14, + }; + + TextureType texture_type; + EglContextType context_type; + void* shared_context; + int texture_id; + int64_t fence_object; + int frame_buffer_id; + float transform_matrix[16]; +}; + /** * This structure defines the raw video frame data in memory * @@ -45,6 +69,11 @@ struct RawPixelBuffer { struct PaddedRawPixelBuffer { RawPixelBuffer::Format format; + uint8_t* data; + int size; + int stride; + int width; + int height; uint8_t* data_y; int stride_y; uint8_t* data_u; @@ -52,38 +81,8 @@ struct PaddedRawPixelBuffer { uint8_t* data_v; int stride_v; PaddedRawPixelBuffer() - : data_y(NULL), stride_y(0), data_u(NULL), stride_u(0), data_v(NULL), stride_v(0) {} -}; - - -/** - * This structure defines the video frame of texture type on Android - * @note For technical preview, not supported for the moment. Use RawPixelBuffer instead. - * - */ -struct TextureInfo { - OPTIONAL_ENUM_CLASS TextureType { - kGlTextureOes, - kGlTexture2D, - }; - OPTIONAL_ENUM_CLASS EglContextType { - kEglContext10, - kEglContext14, - }; - - TextureType texture_type; - EglContextType context_type; - void* shared_context; - int texture_id; - int64_t fence_object; - int frame_buffer_id; - float transform_matrix[16]; - - // for double buffer data - RawPixelBuffer::Format raw_data_format; - uint8_t* raw_data; - int64_t raw_data_size; - int raw_data_stride; + : data(NULL), size(0), stride(0), width(0), height(0), data_y(NULL), stride_y(0), + data_u(NULL), stride_u(0), data_v(NULL), stride_v(0) {} }; /** @@ -98,6 +97,7 @@ struct VideoFrameData { kPaddedRawPixels, // Raw pixels with paddings kTextureOES,// Android: GL_TEXTURE_OES kTexture2D, // Android: GL_TEXTURE_2D + kDoubleBuffer, //Android: double buffer }; Type type; union { diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ares.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ares.h index 9b5ba7b1f..b09b58488 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ares.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ares.h @@ -29,9 +29,32 @@ extern "C" { * arg: the parameter attached with the async result object; * Return value: * the async result object ref id just created, AOSL_REF_INVALID when failed. + * Remarks: + * ares is an async-result synchronization primitive: + * 1. one side completes it with aosl_ares_complete(); + * 2. another side waits or polls for completion; + * 3. it can be reused through aosl_ares_reset(). + * Unlike most long-lived refs, ares objects typically do not need + * destroy_wait behavior because they are meant to model completion state, + * not long business callbacks. **/ extern __aosl_api__ aosl_ref_t aosl_ares_create (void *arg); +/** + * Create an async result object with specified flags. + * Parameter: + * flags: creating flags bitmask, which specified some special attributes of the ares object; + * arg: the parameter attached with the async result object; + * Return value: + * the async result object ref id just created, AOSL_REF_INVALID when failed. + * Remarks: + * This is the flagged form of aosl_ares_create(). In practice, ares is + * often used as a short-lived completion primitive, so callers typically + * care more about the completion/reset lifecycle than about long-lived + * ref-style locking behavior. + **/ +extern __aosl_api__ aosl_ref_t aosl_ares_create_flags (int flags, void *arg); + /** * Complete the specified async result object. * Parameters: @@ -40,9 +63,27 @@ extern __aosl_api__ aosl_ref_t aosl_ares_create (void *arg); * Return value: * <0: error occured, and aosl_errno indicates which error; * >=0: successful; + * Remarks: + * Completing an ares wakes current/future waiters and records the result + * value that aosl_ares_wait() can later retrieve. **/ extern __aosl_api__ int aosl_ares_complete (aosl_ref_t ref, intptr_t result); +/** + * Complete the async result object specified with ref + magic. + * Parameters: + * ref: the async result object ref id; + * magic: the saved magic of the async result object; + * result: a result value which can be retrieved by wait function; + * Return value: + * <0: error occured, and aosl_errno indicates which error; + * >=0: successful; + * Remarks: + * Use the magic form when the completion signal may race with object reuse + * or replacement and the caller needs ABA-style protection. + **/ +extern __aosl_api__ int aosl_ares_magic_complete (aosl_ref_t ref, aosl_ref_magic_t magic, intptr_t result); + /** * Wait the specified async result object to complete. * Parameters: @@ -55,6 +96,12 @@ extern __aosl_api__ int aosl_ares_complete (aosl_ref_t ref, intptr_t result); * Return value: * <0: error occured, and aosl_errno indicates which error; * >=0: AOSL_POLL_ST_* macros value; + * Remarks: + * This API returns a poll-style state rather than a plain boolean: + * AOSL_POLL_ST_SIGNALED means completed normally, + * AOSL_POLL_ST_DESTROY means the ares object was destroyed, + * and AOSL_POLL_ST_NONE means still unsignaled in nonblocking cases. + * If timeo is 0, the call behaves like a nonblocking state probe. **/ extern __aosl_api__ int aosl_ares_wait (aosl_ref_t ref, intptr_t timeo, intptr_t *result); @@ -65,6 +112,8 @@ extern __aosl_api__ int aosl_ares_wait (aosl_ref_t ref, intptr_t timeo, intptr_t * Return value: * <0: error occured, and aosl_errno indicates which error; * >=0: successful; + * Remarks: + * Reset is what makes an ares reusable across multiple async rounds. **/ extern __aosl_api__ int aosl_ares_reset (aosl_ref_t ref); @@ -74,4 +123,4 @@ extern __aosl_api__ int aosl_ares_reset (aosl_ref_t ref); } #endif -#endif /* __AOSL_ARES_H__ */ \ No newline at end of file +#endif /* __AOSL_ARES_H__ */ diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_defs.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_defs.h index c0d1e8f4f..eaa8e8045 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_defs.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_defs.h @@ -25,6 +25,17 @@ #define aosl_stringify_1(x) #x #define aosl_stringify(x) aosl_stringify_1(x) +#ifndef __AOSL_SANITIZE_ADDRESS__ +#if defined (__SANITIZE_ADDRESS__) +/* gcc */ +#define __AOSL_SANITIZE_ADDRESS__ +#elif defined (__has_feature) +#if __has_feature(address_sanitizer) +/* clang */ +#define __AOSL_SANITIZE_ADDRESS__ +#endif +#endif +#endif #ifndef __MAKERCORE_ASSEMBLY__ @@ -33,10 +44,13 @@ extern "C" { #endif -#ifdef _MSC_VER +#if defined (_MSC_VER) && !defined (__GNUC__) #ifndef __inline__ #define __inline__ __inline #endif +#if defined (__cplusplus) && !defined (__typeof__) +#define __typeof__ decltype +#endif #endif @@ -181,6 +195,20 @@ __asm__ (".section __TEXT,__const\n\t" \ #error Unsupported Toolchain! #endif +/** + * Get the current call-site return address. + * Remarks: + * This is a low-level helper used by APIs such as AOSL_SO_GROUP() to + * identify the calling module. The exact value is compiler/toolchain + * dependent and should only be treated as an opaque code address. + **/ +#if defined (__GNUC__) +#define aosl_return_address() __builtin_return_address(0) +#elif defined (_WIN32) +#include +#define aosl_return_address() _ReturnAddress() +#endif + #ifdef __cplusplus } @@ -220,4 +248,4 @@ __asm__ (".section __TEXT,__const\n\t" \ #endif /* __MAKERCORE_ASSEMBLY__ */ -#endif /* __AOSL_DEFS_H__ */ \ No newline at end of file +#endif /* __AOSL_DEFS_H__ */ diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ref.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ref.h index 109d0a717..a6c546d7d 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ref.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/aosl_ref.h @@ -28,7 +28,7 @@ typedef struct _internal_ref_od_ *aosl_ref_t; #define AOSL_REF_INVALID ((aosl_ref_t)(intptr_t)NULL) -#define aosl_ref_invalid(ref) ((intptr_t)(ref) <= 0) +#define aosl_ref_invalid(ref) ((aosl_ref_t)(ref) == AOSL_REF_INVALID) /** @@ -52,9 +52,33 @@ typedef void (*aosl_ref_dtor_t) (void *arg); * 0 the destroy caller will not wait other threads; * Return value: * the ref object id, please use aosl_ref_invalid macro to check whether failed. + * Remarks: + * destroy_wait is one of the most important lifetime policy switches: + * 1. if non-zero, aosl_ref_destroy() waits for in-flight normal ref ops + * such as hold/read/write to finish before the destroy completes; + * 2. if 0, destroy only marks the object destroyed and does not wait for + * those in-flight users. + * AOSL ares objects are a typical case that usually do not need waiting. **/ extern __aosl_api__ aosl_ref_t aosl_ref_create (void *arg, aosl_ref_dtor_t dtor, int destroy_wait); +#define AOSL_REF_NEWID 0x00000001 + +/** + * The reference object creating with flags function prototype, which is used to create a ref object. + * Parameters: + * flags: creating flags bitmask, which specified some special attributes of the ref object; + * arg: the parameter attached with the reference object; + * dtor: the ref object destructor function, which will be invoked when + * the ref object is deleted; + * destroy_wait: + * none-0 the destroy caller will wait other threads to release the ref object; + * 0 the destroy caller will not wait other threads; + * Return value: + * the ref object id, please use aosl_ref_invalid macro to check whether failed. + **/ +extern __aosl_api__ aosl_ref_t aosl_ref_create_flags (int flags, void *arg, aosl_ref_dtor_t dtor, int destroy_wait); + /** * Returns the total ref objects count. **/ @@ -98,6 +122,10 @@ typedef void (*aosl_ref_func_t) (void *arg, uintptr_t argc, uintptr_t argv []); * Return value: * 0: success * <0: failure with aosl_errno set + * Remarks: + * hold protects the object lifetime by usage count, but does not take the + * internal read/write lock. For destroy_wait refs, an already-running hold + * callback still delays aosl_ref_destroy() until the callback returns. **/ extern __aosl_api__ int aosl_ref_hold (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, ...); extern __aosl_api__ int aosl_ref_hold_args (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, va_list args); @@ -113,6 +141,9 @@ extern __aosl_api__ int aosl_ref_hold_argv (aosl_ref_t ref, aosl_ref_func_t f, u * Return value: * 0: success * <0: failure with aosl_errno set + * Remarks: + * Multiple read callbacks may run concurrently. For destroy_wait refs, + * aosl_ref_destroy() waits until in-flight read callbacks return. **/ extern __aosl_api__ int aosl_ref_read (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, ...); extern __aosl_api__ int aosl_ref_read_args (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, va_list args); @@ -128,6 +159,10 @@ extern __aosl_api__ int aosl_ref_read_argv (aosl_ref_t ref, aosl_ref_func_t f, u * Return value: * 0: success * <0: failure with aosl_errno set + * Remarks: + * write is exclusive against other read/write callbacks. For destroy_wait + * refs, aosl_ref_destroy() waits until the in-flight write callback + * returns. **/ extern __aosl_api__ int aosl_ref_write (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, ...); extern __aosl_api__ int aosl_ref_write_args (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, va_list args); @@ -137,7 +172,7 @@ extern __aosl_api__ int aosl_ref_write_argv (aosl_ref_t ref, aosl_ref_func_t f, * Hold the ref object with the saved magic, and invoke the specified callback function. * Parameters: * ref: the ref object id; - * magic: the saved magic variable address; + * magic: the saved magic of the ref object; * f: the callback function; * argc: the args count * ...: variable args @@ -153,7 +188,7 @@ extern __aosl_api__ int aosl_ref_magic_hold_argv (aosl_ref_t ref, aosl_ref_magic * Read lock the ref object with the saved magic, and invoke the specified callback function. * Parameters: * ref: the ref object id; - * magic: the saved magic variable address; + * magic: the saved magic of the ref object; * f: the callback function; * argc: the args count * ...: variable args @@ -169,7 +204,7 @@ extern __aosl_api__ int aosl_ref_magic_read_argv (aosl_ref_t ref, aosl_ref_magic * Write lock the ref object with the saved magic, and invoke the specified callback function. * Parameters: * ref: the ref object id; - * magic: the saved magic variable address; + * magic: the saved magic of the ref object; * f: the callback function; * argc: the args count * ...: variable args @@ -191,6 +226,17 @@ extern __aosl_api__ int aosl_ref_magic_write_argv (aosl_ref_t ref, aosl_ref_magi * Return value: * 0: success * <0: failure with aosl_errno set + * Remarks: + * unsafe is intentionally different from hold/read/write: + * 1. it is allowed to run even after the ref has already been marked + * destroyed internally, as long as the internal object is still + * reachable; + * 2. destroy_wait destroyers do not wait for unsafe/maystall sections in + * the same way they wait normal hold/read/write users; + * 3. after the callback returns, unsafe reports -EPERM if the ref has + * already become destroyed. + * This mode is mainly for final cleanup paths and advanced lifecycle + * control, not for normal business access. **/ extern __aosl_api__ int aosl_ref_unsafe (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, ...); extern __aosl_api__ int aosl_ref_unsafe_args (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, va_list args); @@ -206,6 +252,11 @@ extern __aosl_api__ int aosl_ref_unsafe_argv (aosl_ref_t ref, aosl_ref_func_t f, * Return value: * 0: success * <0: failure with aosl_errno set + * Remarks: + * maystall currently shares the same exported implementation as unsafe. + * Use it when the callback may block for a long time and therefore should + * not force destroy_wait callers to wait for it like a normal read/write + * user. **/ extern __aosl_api__ int aosl_ref_maystall (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, ...); extern __aosl_api__ int aosl_ref_maystall_args (aosl_ref_t ref, aosl_ref_func_t f, uintptr_t argc, va_list args); @@ -322,6 +373,16 @@ extern __aosl_api__ int aosl_ref_set_scope (aosl_ref_t ref, aosl_ref_t scope_ref * Return value: * 0: success * <0: failed, and aosl_errno indicates what error occurs + * Remarks: + * Destroy has two logically separate effects: + * 1. mark the ref object as destroying/destroyed so that later normal + * hold/read/write attempts fail; + * 2. optionally uninstall and free the ref object when do_delete != 0. + * For destroy_wait refs, destroy waits for in-flight normal hold/read/ + * write users to leave before it reports completion. It does not turn an + * already-running direct hold/read/write callback into free_only; those + * callbacks run their normal path and only later callers observe the + * destroyed state. **/ extern __aosl_api__ int aosl_ref_destroy (aosl_ref_t ref, int do_delete); @@ -421,4 +482,4 @@ extern __aosl_api__ aosl_refobj_t aosl_ref_stack_top_obj (void); -#endif /* __AOSL_REF_H__ */ \ No newline at end of file +#endif /* __AOSL_REF_H__ */ diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ares_class.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ares_class.h index 0e52b13e9..831e07c50 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ares_class.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_ares_class.h @@ -22,13 +22,37 @@ #include #include +/** + * RAII wrapper for aosl_ares_*. + * Remarks: + * Copies share the same underlying ares object, but only the "owner" + * instance whose magic remains invalid destroys the underlying ref in its + * destructor. + **/ +class aosl_ares_class +#if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) + final +#endif +{ +private: + aosl_ref_t refid; + /** + * The magic is AOSL_REF_MAGIC_INVALID when define an ares class object, + * and set it to the corresponding ref magic when copy construct other + * instance. Only the instance having invalid magic will destroy the ares + * object in the destructor. + **/ + aosl_ref_magic_t magic; -class aosl_ares_class: public aosl_ref_class { public: - aosl_ares_class (): aosl_ref_class (aosl_ares_create (this)) + aosl_ares_class () { - if (aosl_ref_invalid (ref ())) + refid = aosl_ares_create_flags (AOSL_REF_NEWID, this); + if (aosl_ref_invalid (refid)) abort (); + + /* Invalid magic indicates needing destroy in the destructor */ + magic = AOSL_REF_MAGIC_INVALID; } /** @@ -36,56 +60,88 @@ class aosl_ares_class: public aosl_ref_class { * base class and other derivatives, destroy the ref * in the destructor and the destructor is public. **/ - virtual ~aosl_ares_class () + ~aosl_ares_class () { - aosl_ref_t refid = ref (); - if (!aosl_ref_invalid (refid)) - aosl_ref_destroy (refid, true); + /** + * Only the instance having invalid magic will destroy the ares object, + * and all other copies do nothing in the destructor. + **/ + if (magic == AOSL_REF_MAGIC_INVALID) { + if (!aosl_ref_invalid (refid)) + aosl_ref_destroy (refid, 1); + } + } + + int set_scope (aosl_ref_t scope_ref) + { + return aosl_ref_set_scope (refid, scope_ref); } /* complete the async result */ - int complete (intptr_t result = 0) + int complete (intptr_t result = 0) const { - return aosl_ares_complete (ref (), result); + return aosl_ares_magic_complete (refid, magic, result); } /* wait the async result to be completed */ int wait (intptr_t timeo, intptr_t *result = NULL) { - return aosl_ares_wait (ref (), timeo, result); + return aosl_ares_wait (refid, timeo, result); } /* reset the signaled state */ int reset (void) { - return aosl_ares_reset (ref ()); + return aosl_ares_reset (refid); + } + + aosl_ref_t ref () const + { + return refid; } operator aosl_ref_t () const { - return ref (); + return refid; } -private: - /* we do not allow invoke the destroy function of base class */ - int destroy (bool do_delete = true) + aosl_ares_class (const aosl_ares_class &src) { - abort (); - return 0; + refid = src.refid; + /* Set the magic in the copy constructor */ + if (aosl_ref_magic (refid, &magic) < 0) + magic = (aosl_ref_magic_t)-1; /* failed to get the magic, set it to -1 */ + } + + aosl_ares_class &operator = (const aosl_ares_class &src) + { + this->~aosl_ares_class (); + refid = src.refid; + /* Set the magic in the copy assignment operator */ + if (aosl_ref_magic (refid, &magic) < 0) + magic = (aosl_ref_magic_t)-1; /* failed to get the magic, set it to -1 */ + return *this; } #if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) -private: - aosl_ares_class (const aosl_ares_class &) = delete; - aosl_ares_class (aosl_ares_class &&) = delete; - aosl_ares_class &operator = (const aosl_ares_class &) = delete; - aosl_ares_class &operator = (aosl_ares_class &&) = delete; -#else -private: - aosl_ares_class (const aosl_ares_class &); - aosl_ares_class &operator = (const aosl_ares_class &); + aosl_ares_class (aosl_ares_class &&src) + { + refid = src.refid; + magic = src.magic; + src.refid = AOSL_REF_INVALID; + src.magic = AOSL_REF_MAGIC_INVALID; + } + aosl_ares_class &operator = (aosl_ares_class &&src) + { + this->~aosl_ares_class (); + refid = src.refid; + magic = src.magic; + src.refid = AOSL_REF_INVALID; + src.magic = AOSL_REF_MAGIC_INVALID; + return *this; + } #endif /* C++11 */ }; -#endif /* __AOSL_ARES_CLASS_H__ */ \ No newline at end of file +#endif /* __AOSL_ARES_CLASS_H__ */ diff --git a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_poll_class.h b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_poll_class.h index 39ac60ab5..84f831d19 100644 --- a/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_poll_class.h +++ b/Android/APIExample/agora-simple-filter/src/main/cpp/AgoraRtcKit/api/cpp/aosl_poll_class.h @@ -20,20 +20,25 @@ #include #include -#if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) -#include -#endif - #include #include - +/** + * Convenience class for polling multiple aosl_ares_class objects. + * Remarks: + * The class stores the watched refs, delegates the wait to aosl_poll(), + * and preserves the subset that became signaled for indexed access after + * poll() returns. + **/ class aosl_poll_class { private: std::map poll_refs; std::vector signaled_refs; public: + /** + * Add an async-result object to the polling set. + **/ void add (const aosl_ares_class &tail) { poll_refs [tail.ref ()] = &tail; @@ -62,7 +67,15 @@ class aosl_poll_class { add (*areses [i]); } - /* poll the constructed async results */ + /** + * Poll the constructed async-result set. + * Parameters: + * min: minimum number of signaled refs required to return; + * timeo: timeout in milliseconds. + * Return value: + * >=0 number of signaled refs reported by aosl_poll(); + * <0 failure. + **/ int poll (size_t min, intptr_t timeo) { aosl_ref_t local_refs [32]; @@ -96,19 +109,25 @@ class aosl_poll_class { return err; } - /* total async results count */ + /** + * Total watched async-result count. + **/ size_t total () { return poll_refs.size (); } - /* signaled async results count */ + /** + * Count of refs recorded as signaled by the last poll() call. + **/ size_t signaled () { return signaled_refs.size (); } - /* operator for accessing the signaled async results */ + /** + * Indexed access to the refs signaled in the last poll() call. + **/ const aosl_ares_class *operator [] (size_t idx) { if (idx < signaled_refs.size ()) @@ -131,4 +150,4 @@ class aosl_poll_class { }; -#endif /* __AOSL_POLL_CLASS_H__ */ \ No newline at end of file +#endif /* __AOSL_POLL_CLASS_H__ */ 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 16a53ea4e..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 @@ -32,11 +32,18 @@ #endif #if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) -#include #include #include #endif +/** + * C++ wrapper around aosl_ref_t with lambda helpers for hold/read/write, + * MPQ dispatch, prepare/resume, and destroy semantics. + * Remarks: + * The class keeps the same core lifetime model as aosl_ref_t: + * hold/read/write are normal ref users, while unsafe is the advanced path + * used when destroy must not wait in the same manner. + **/ class aosl_ref_class { public: class aosl_ref_t_oop { @@ -50,7 +57,7 @@ class aosl_ref_class { public: static aosl_ref_t_oop *create (void *arg = NULL, aosl_ref_dtor_t dtor = NULL, bool destroy_wait = true) { - return (aosl_ref_t_oop *)aosl_ref_create (arg, dtor, (int)destroy_wait); + return (aosl_ref_t_oop *)aosl_ref_create_flags (AOSL_REF_NEWID, arg, dtor, (int)destroy_wait); } static aosl_ref_t_oop *from_aosl_ref_t (aosl_ref_t ref) @@ -525,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__ */ @@ -695,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) @@ -716,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) @@ -737,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) @@ -758,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) @@ -821,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 @@ -859,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 */ @@ -1028,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 @@ -1066,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) @@ -1094,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; } @@ -1114,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; } @@ -1197,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) @@ -1216,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) @@ -1236,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 []) @@ -1278,6 +1405,9 @@ class aosl_ref_class { aosl_ref_class (aosl_ref_t_oop *obj) { + if (aosl_ref_invalid (obj)) + abort (); + refoop = obj; if (aosl_ref_magic (obj->ref (), &refmagic) < 0) refmagic = AOSL_REF_MAGIC_INVALID; @@ -1285,6 +1415,9 @@ class aosl_ref_class { aosl_ref_class (aosl_ref_t ref) { + if (aosl_ref_invalid (ref)) + abort (); + refoop = aosl_ref_t_oop::from_aosl_ref_t (ref); if (aosl_ref_magic (ref, &refmagic) < 0) refmagic = AOSL_REF_MAGIC_INVALID; @@ -1603,28 +1736,6 @@ class aosl_ref_class { return refoop->set_scope (scope_ref); } - int destroy (bool do_delete = true) - { - if (!aosl_ref_invalid (refoop->ref ())) { - /** - * if the ref is valid, then just call the destroy - * function and do not delete this object directly - * even the return value indicates failure. - **/ - return refoop->destroy (do_delete); - } - - if (do_delete) { - /** - * delete this object directly only when the ref - * is invalid and the do_delete argument is true. - **/ - ::delete this; - } - - return 0; - } - #if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) /* __ref_destroy_exec_lambda_t: void (int err) */ template unsafe (____ref_abort_f, 0); } private: @@ -1658,6 +1782,26 @@ class aosl_ref_class { ::delete __this; } + static void ____ref_abort_f (void *arg, uintptr_t argc, uintptr_t argv []) + { + abort (); + } + + int destroy (bool do_delete = true) + { + if (!aosl_ref_invalid (refoop->ref ())) { + /** + * if the ref is valid, then just call the destroy + * function and do not delete this object directly + * even the return value indicates failure. + **/ + return refoop->destroy (do_delete); + } + + return 0; + } + template friend class aosl_ref_unique_ptr; + #ifdef __AOSL_MPQ_H__ /* MPQ relative encapsulations */ public: @@ -1846,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__ */ @@ -2035,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) */ @@ -2070,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 @@ -2093,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 */ @@ -2172,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) */ @@ -2207,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 @@ -2230,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__ */ @@ -2263,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) @@ -2277,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) @@ -2300,7 +2623,6 @@ class aosl_ref_class { #endif /* C++11 */ }; - /** * The T_ref_cls argument of this template must be * aosl_ref_class or its derivatives. @@ -2312,10 +2634,18 @@ class aosl_ref_unique_ptr { public: aosl_ref_unique_ptr (): _ptr (NULL) {} - aosl_ref_unique_ptr (T_ref_cls *p): _ptr (p) {} + aosl_ref_unique_ptr (T_ref_cls *p): _ptr (p) + { +#if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) + static_assert (std::is_base_of::value, "T_ref_cls must derive from aosl_ref_class."); +#endif + } aosl_ref_unique_ptr &operator = (T_ref_cls *p) { +#if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) + static_assert (std::is_base_of::value, "T_ref_cls must derive from aosl_ref_class."); +#endif reset (); _ptr = p; return *this; @@ -2345,6 +2675,9 @@ class aosl_ref_unique_ptr { void reset (T_ref_cls *p = NULL) { +#if (__cplusplus >= 201103) || (defined (_MSC_VER) && _MSC_VER >= 1800) + static_assert (std::is_base_of::value, "T_ref_cls must derive from aosl_ref_class."); +#endif if (_ptr != p) { if (_ptr != NULL) { /* C++11 lambda encapsulations */ @@ -2465,4 +2798,4 @@ inline bool operator != (std::nullptr_t, const aosl_ref_unique_ptr &p typedef aosl_ref_unique_ptr aosl_ref_class_unique_ptr; -#endif /* __AOSL_REF_OBJ_CPP_H__ */ \ No newline at end of file +#endif /* __AOSL_REF_OBJ_CPP_H__ */ 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 977c28ca8..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) @@ -69,4 +69,4 @@ target_link_libraries( # Specifies the target library. # ${GLESv2} ${GLESv3} EGL - android) \ No newline at end of file + android) 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 acc302ea3..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 @@ -292,6 +292,20 @@ enum CHANNEL_PROFILE_TYPE { CHANNEL_PROFILE_COMMUNICATION_1v1 __deprecated = 4, }; +/** + * The channel types. + */ +enum CHANNEL_TYPE { + /** + * 0: Standard channel type. + */ + CHANNEL_TYPE_STANDARD = 0, + /** + * 1: Channel type with a large-scale scenario, for example, more than 200 broadcasters. + */ + CHANNEL_TYPE_LARGE_SCALE = 1, +}; + /** * The warning codes. */ @@ -403,6 +417,10 @@ enum WARN_CODE_TYPE { * 1032: Audio Device Module: The playback audio volume is too low. */ WARN_ADM_PLAYOUT_AUDIO_LOWLEVEL = 1032, + /** + * 1033: Audio device module: The audio capturing device is occupied. + */ + WARN_ADM_RECORD_IS_OCCUPIED = 1033, /** * 1040: Audio device module: An exception occurs with the audio drive. * Choose one of the following solutions: @@ -789,6 +807,71 @@ enum ERROR_CODE_TYPE { * camera permission is granted. */ ERR_VDM_CAMERA_NOT_AUTHORIZED = 1501, + + // Video Effect (Beauty) errors: 1700~1799 + /** + * 1700: The video effect asset is invalid. The asset bundle may be corrupted, + * the configuration file cannot be parsed, or required fields are missing. + */ + ERR_VIDEOEFFECT_ASSET_INVALID = 1700, + /** + * 1701: Failed to save the video effect configuration. The file write + * operation failed, possibly due to insufficient disk space or permission issues. + */ + ERR_VIDEOEFFECT_SAVE_FAILED = 1701, + /** + * 1702: The video effect engine is in an invalid state. The beauty processing + * engine is not initialized or the render engine instance does not exist. + */ + ERR_VIDEOEFFECT_ENGINE_INVALID = 1702, + /** + * 1704: The target video effect node is not active. Call + * addOrUpdateVideoEffect to activate the node first. + */ + ERR_VIDEOEFFECT_NODE_NOT_ACTIVE = 1704, + /** + * 1705: The video effect parameter is invalid. For example, the nodeId, + * actionId, option, or key is invalid or empty. + */ + ERR_VIDEOEFFECT_INVALID_PARAM = 1705, + /** + * 1706: The video effect is not supported on this device. + */ + ERR_VIDEOEFFECT_NOT_SUPPORTED = 1706, + /** + * 1707: The video effect bundle path is invalid. The path is empty or + * does not exist on the file system. + */ + ERR_VIDEOEFFECT_INVALID_BUNDLE_PATH = 1707, + /** + * 2007: Audio Device Module: An error occurs in starting the application loopback. + */ + ERR_ADM_APPLICATION_LOOPBACK = 2007, + /** + * 2008: Audio Device Module: The application loopback was stopped unexpectedly, typically due to the application exiting or the user disabling loopback. + */ + ERR_ADM_APPLICATION_LOOPBACK_STOPPED = 2008, + /** + * 2009: Audio Device Module: An error occurred while starting the system loopback. + * This may be due to system restrictions, device conflicts, or other internal errors. + */ + ERR_ADM_SYSTEM_LOOPBACK = 2009, + /** + * 2010: Audio Device Module: The system loopback was stopped unexpectedly, typically due to the user disabling loopback or a system error occurred. + */ + ERR_ADM_SYSTEM_LOOPBACK_STOPPED = 2010, + /** + * 2011: Audio Device Module: No permission to start the loopback. + */ + ERR_ADM_LOOPBACK_NO_PERMISSION = 2011, + /** + * 2012: Audio Device Module: Silent detected in loopback. + */ + ERR_ADM_LOOPBACK_SILENT_DETECTED = 2012, + /** + * 2013: Audio Device Module: Silent recovered in loopback. + */ + ERR_ADM_LOOPBACK_SILENT_RECOVERED = 2013, }; enum LICENSE_ERROR_TYPE { @@ -1128,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, /** @@ -2253,11 +2337,12 @@ struct SimulcastStreamConfig { /** * @brief Configure video streams of different quality levels. * - * @since v4.6.0 + * @technical preview */ struct SimulcastConfig { /** * @brief Index of video streams of different quality levels. + * @technical preview */ enum StreamLayerIndex { /** @@ -2295,7 +2380,7 @@ struct SimulcastConfig { }; /** * @brief Configures the parameters of a specific layer in multi-quality video streams. - * + * @technical preview * @details * Used to configure the resolution, frame rate, and enable status of a specific layer in * multi-quality video streams. @@ -2631,7 +2716,6 @@ struct WatermarkConfig { enum MultipathMode { /** * Duplicate mode, the same piece of data is redundantly transmitted over all available paths. - * @technical preview */ Duplicate= 0, /** @@ -5155,6 +5239,8 @@ enum CONNECTION_CHANGED_REASON_TYPE { CONNECTION_CHANGED_CLIENT_IP_ADDRESS_CHANGED_BY_USER = 18, /** * 19: The user joined the same channel from different devices with the same UID. + * @note Joining a channel with the same UID is an undefined behavior. Agora does not guarantee + * `reason` 19 to be triggered every time such a behavior occurs. */ CONNECTION_CHANGED_SAME_UID_LOGIN = 19, /** @@ -5341,6 +5427,11 @@ struct VideoCanvas { */ media::base::VIDEO_MODULE_POSITION position; + /** + * The clockwise rotation information. You can set it as 0, 90, 180 or 270. + */ + VIDEO_ORIENTATION rotation; + VideoCanvas() : uid(0), subviewUid(0), @@ -5353,7 +5444,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt) : uid(0), @@ -5367,7 +5459,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt, uid_t u) : uid(u), @@ -5381,7 +5474,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} VideoCanvas(view_t v, media::base::RENDER_MODE_TYPE m, VIDEO_MIRROR_MODE_TYPE mt, uid_t u, uid_t subu) @@ -5396,7 +5490,8 @@ struct VideoCanvas { mediaPlayerId(-ERR_NOT_READY), cropArea(0, 0, 0, 0), enableAlphaMask(false), - position(media::base::POSITION_POST_CAPTURER) {} + position(media::base::POSITION_POST_CAPTURER), + rotation(VIDEO_ORIENTATION_0) {} }; /** @@ -5485,7 +5580,7 @@ struct FaceShapeAreaOptions { FACE_SHAPE_AREA_NONE = -1, /** * (100): Head, used to achieve a smaller head effect. The value range is 0 to 100, and the default - * value is 50. The larger the value, the more noticeable the adjustment. + * value is 0. The larger the value, the more noticeable the adjustment. */ FACE_SHAPE_AREA_HEADSCALE = 100, /** @@ -5532,9 +5627,15 @@ struct FaceShapeAreaOptions { * opposite direction. */ FACE_SHAPE_AREA_CHIN = 108, + /** + * (109): Face Small, reduces the overall face size for a slimmer appearance. + * The value range is [0, 100]. The default value is 0. + * The larger the value, the more pronounced the overall face size reduction effect. + */ + FACE_SHAPE_AREA_FACESMALL = 109, /** * (200): Eyes, used to achieve a larger eye effect. The value range is 0 to 100, and the default - * value is 50. The larger the value, the more noticeable the adjustment. + * value is 0. The larger the value, the more noticeable the adjustment. */ FACE_SHAPE_AREA_EYESCALE = 200, /** @@ -5577,6 +5678,12 @@ struct FaceShapeAreaOptions { * @since v4.6.0 */ FACE_SHAPE_AREA_EYEOUTERCORNER = 206, + /** + * (207): Eye Angle, adjusts the tilt angle of the eyes (cat-eye effect). + * The value range is [-100, 100]. The default value is 0. + * The larger the value, the more the outer corners of the eyes are lifted upward. + */ + FACE_SHAPE_AREA_EYEANGLE = 207, /** * (300): Nose length, used to achieve a longer nose effect. The range is [-100, 100], with a * default value of 0. @@ -5589,7 +5696,7 @@ struct FaceShapeAreaOptions { */ FACE_SHAPE_AREA_NOSEWIDTH = 301, /** - * (302): Nose wing adjustment. The value range is 0 to 100, and the default value is 10. The larger + * (302): Nose wing adjustment. The value range is 0 to 100, and the default value is 0. The larger * the value, the more noticeable the adjustment. * @since v4.6.0 */ @@ -5601,19 +5708,19 @@ struct FaceShapeAreaOptions { */ FACE_SHAPE_AREA_NOSEROOT = 303, /** - * (304): Nose bridge adjustment. The value range is 0 to 100, and the default value is 50. The + * (304): Nose bridge adjustment. The value range is 0 to 100, and the default value is 0. The * larger the value, the more noticeable the adjustment. * @since v4.6.0 */ FACE_SHAPE_AREA_NOSEBRIDGE = 304, /** - * (305): Nose tip adjustment. The value range is 0 to 100, and the default value is 50. The larger + * (305): Nose tip adjustment. The value range is 0 to 100, and the default value is 0. The larger * the value, the more noticeable the adjustment. * @since v4.6.0 */ FACE_SHAPE_AREA_NOSETIP = 305, /** - * (306): Overall nose adjustment. The range is [-100, 100], with a default value of 50. The greater + * (306): Overall nose adjustment. The range is [-100, 100], with a default value of 0. The greater * the absolute value, the more noticeable the adjustment. Negative values indicate the opposite * direction. * @since v4.6.0 @@ -5621,7 +5728,7 @@ struct FaceShapeAreaOptions { FACE_SHAPE_AREA_NOSEGENERAL = 306, /** * (400): Mouth, used to achieve a larger mouth effect. The range is [-100, 100], with a default - * value of 20. The greater the absolute value, the more noticeable the adjustment. Negative values + * value of 0. The greater the absolute value, the more noticeable the adjustment. Negative values * indicate the opposite direction. * @since v4.6.0 */ @@ -6097,6 +6204,75 @@ struct AudioTrackConfig { AudioTrackConfig() : enableLocalPlayback(true),enableAudioProcessing(false) {} }; +/** The type of loopback audio source mode +*/ +enum LOOPBACK_AUDIO_TRACK_TYPE { + /** + * 0: loopback the whole system + */ + LOOPBACK_SYSTEM = 0, + /** + * 1: loopback the whole system exclude self + */ + LOOPBACK_SYSTEM_EXCLUDE_SELF = 1, + /** + * 2: loopback the specific application + */ + LOOPBACK_APPLICATION = 2, + /** + * 3: loopback the specific process + */ + LOOPBACK_PROCESS = 3, +}; + +/** Defines options for a custom loopback audio track. */ +struct LoopbackAudioTrackConfig { + /** + * Specifies the loopback source type. + * Possible values are defined by LOOPBACK_AUDIO_TRACK_TYPE (e.g., system audio, specific application, or process). + * Default: LOOPBACK_SYSTEM. + */ + LOOPBACK_AUDIO_TRACK_TYPE loopbackType; + + /** + * Initial playback volume for the loopback audio, valid in the range [0, 400]. + * 0: mute; 100: original volume; 400: maximum amplified volume. + * Default is 100. + */ + int volume; + + /** + * Playback device name to capture loopback audio from. + * + * Platform-specific notes: + * - **Windows:** Not supported for now. The `deviceName` parameter is ignored on Windows. + * - **macOS:** For `LOOPBACK_SYSTEM` or `LOOPBACK_SYSTEM_EXCLUDE_SELF`, if set, uses the named virtual device; if NULL, uses the default. + * For `LOOPBACK_APPLICATION` and `LOOPBACK_PROCESS`, ignored. + * + * If set, the name must exactly match the target playback device. + * Set to NULL to use the platform default selection. + */ + const char* deviceName; + + /** + * Name of the application to capture audio from. + * Only used if loopbackType is LOOPBACK_APPLICATION; otherwise ignored. + * Must be an exact, case-sensitive match of the application name. + * Set to NULL if not capturing a specific application. + */ + const char* appName; + + /** + * Process ID of the target application to capture audio from. + * Only used if loopbackType is LOOPBACK_PROCESS; otherwise ignored. + * Default is -1. + */ + unsigned int processId; + + LoopbackAudioTrackConfig() + : loopbackType(LOOPBACK_SYSTEM), volume(100), deviceName(NULL), appName(NULL), processId(-1) {} +}; + /** * Preset local voice reverberation options. * bitmap allocation: @@ -6484,6 +6660,7 @@ struct ScreenAudioParameters { struct ScreenCaptureParameters { /** + * @technical preview * Determines whether to capture system audio during screen sharing: * - `true`: Capture. * - `false`: (Default) Do not capture. @@ -6494,6 +6671,7 @@ struct ScreenCaptureParameters { */ bool captureAudio; /** + * @technical preview * The audio configuration for the shared screen stream. * @note This parameter only takes effect when `captureAudio` is `true`. * See `ScreenAudioParameters`. diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaBase.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaBase.h index 9a287d374..be639d56d 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaBase.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaBase.h @@ -572,6 +572,7 @@ enum VIDEO_PIXEL_FORMAT { /** * 17: The ID3D11TEXTURE2D format. Currently supported types are `DXGI_FORMAT_B8G8R8A8_UNORM`, * `DXGI_FORMAT_B8G8R8A8_TYPELESS` and `DXGI_FORMAT_NV12`. + * @note This enumeration applies to screen-captured video only. Camera-captured video is not supported. */ VIDEO_TEXTURE_ID3D11TEXTURE2D = 17, /** @@ -1170,7 +1171,7 @@ struct VideoFrame { }; /** - * The IVideoFrameObserver class. + * @brief The IVideoFrameObserver class. */ class IVideoFrameObserver { public: @@ -1386,7 +1387,10 @@ struct SnapshotConfig { const char* filePath; /** - * The position of the snapshot video frame in the video pipeline. See `VIDEO_MODULE_POSITION`. + * The position of the video observation. See `VIDEO_MODULE_POSITION`. + * Allowed values vary depending on the `uid` parameter passed in `takeSnapshot` or `takeSnapshotEx`: + * - `uid` = 0: Position 2, 4 and 8 are allowed. + * - `uid` != 0: Only position 2 is allowed. */ media::base::VIDEO_MODULE_POSITION position; SnapshotConfig() :filePath(NULL), position(media::base::POSITION_PRE_ENCODER) {} @@ -1688,13 +1692,14 @@ class IAudioFrameObserverBase { */ virtual bool onEarMonitoringAudioFrame(AudioFrame& audioFrame) = 0; /** - * Occurs when the before-mixing playback audio frame is received. - * @param channelId The channel name - * @param userId ID of the remote user. - * @param audioFrame The reference to the audio frame: AudioFrame. + * @brief Retrieves the audio frame before mixing of subscribed remote users. + * + * @param channelId The channel ID. + * @param userId The ID of subscribed remote users. + * @param audioFrame The raw audio data. See `AudioFrame`. + * * @return - * - true: The before-mixing playback audio frame is valid and is encoded and sent. - * - false: The before-mixing playback audio frame is invalid and is not encoded or sent. + * Without practical meaning. */ virtual bool onPlaybackAudioFrameBeforeMixing(const char* channelId, base::user_id_t userId, AudioFrame& audioFrame) { @@ -1949,7 +1954,7 @@ class IVideoEncodedFrameObserver { }; /** - * The IVideoFrameObserver class. + * @brief The IVideoFrameObserver class. */ class IVideoFrameObserver { public: @@ -2022,7 +2027,7 @@ class IVideoFrameObserver { VideoFrame& videoFrame) = 0; /** - * @brief Occurs each time the SDK receives a video frame before encoding. + * @brief Occurs each time the SDK receives a pre-encoded video frame. * * @details * After you successfully register the video frame observer, the SDK triggers this callback each @@ -2455,6 +2460,9 @@ struct MediaRecorderConfiguration { videoSourceType(rtc::VIDEO_SOURCE_CAMERA_PRIMARY) {} }; +/** + * @brief The IFaceInfoObserver class. + */ class IFaceInfoObserver { public: /** @@ -2528,6 +2536,9 @@ struct RecorderInfo { : fileName(name), durationMs(dur), fileSize(size) {} }; +/** + * @brief The IMediaRecorderObserver class. + */ class IMediaRecorderObserver { public: /** diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaPlayerTypes.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaPlayerTypes.h index ceb0642ea..db76901cc 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaPlayerTypes.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/AgoraMediaPlayerTypes.h @@ -500,7 +500,7 @@ struct PlayerUpdatedInfo { }; /** - * The custom data source provides a data stream input callback, and the player will continue to call back this interface, requesting the user to fill in the data that needs to be played. + * @brief The custom data source provides a data stream input callback, and the player will continue to call back this interface, requesting the user to fill in the data that needs to be played. */ class IMediaPlayerCustomDataProvider { public: 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/IAgoraMediaEngine.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaEngine.h index 42ab70d4c..797047c65 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaEngine.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaEngine.h @@ -422,6 +422,49 @@ class IMediaEngine { */ virtual int removeVideoFrameRenderer(IVideoFrameObserver *renderer) = 0; + /** + * Create a loopback audio track and get the audio track id. + * + * @note This method is only supported on macOS and Windows for now. + * + * @param config The config of loopback audio track + * See LoopbackAudioTrackConfig. + * + * @return + * - If the call is successful, SDK returns audio track id. + * - If the call fails, SDK returns 0xffffffff. + */ + virtual rtc::track_id_t createLoopbackAudioTrack(const rtc::LoopbackAudioTrackConfig& config) = 0; + + /** + * Destroy loopback audio track by trackId + * + * @note This method is only supported on macOS and Windows for now. + * + * @param trackId The loopback audio track id. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int destroyLoopbackAudioTrack(rtc::track_id_t trackId) = 0; + + /** + * Update the configuration of the loopback audio track + * + * @note This method is only supported on macOS and Windows for now. + * + * @param trackId The loopback audio track id. + * @param config The configuration of the loopback audio track. + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int updateLoopbackAudioTrackConfig(rtc::track_id_t trackId, const rtc::LoopbackAudioTrackConfig& config) = 0; + + /** + * Release the media engine + */ virtual void release() = 0; protected: diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaPlayerSource.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaPlayerSource.h index 563497881..7fd741095 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaPlayerSource.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaPlayerSource.h @@ -28,6 +28,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the unique source ID of the media player source. * @return + * @technical preview * - >=0: The source ID of this media player source. * - < 0: Failure. */ @@ -35,6 +36,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Opens a media file with a specified URL. + * @technical preview * @param url The path of the media file. Both the local path and online path are supported. * @param startPos The starting position (ms) for playback. Default value is 0. * @return @@ -45,6 +47,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Opens a media file with a media file source. + * @technical preview * @param source Media file source that you want to play, see `MediaSource` * @return * - 0: Success. @@ -54,6 +57,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Plays the media file. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -62,6 +66,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Pauses the playback. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -70,6 +75,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Stops the playback. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -78,6 +84,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Resumes the playback. + * @technical preview * @return * - 0: Success. * - < 0: Failure. @@ -86,6 +93,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Sets the playback position of the media file. + * @technical preview * @param newPos The new playback position (ms). * @return * - 0: Success. @@ -95,6 +103,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the duration of the media file. + * @technical preview * @param [out] duration A reference to the duration of the media file. * @return * - 0: Success. @@ -104,6 +113,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the current playback position of the media file. + * @technical preview * @param [out] pos A reference to the current playback position (ms). * @return * - 0: Success. @@ -113,6 +123,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the number of the media streams in the media source. + * @technical preview * @param [out] count The number of the media streams in the media source. * @return * - 0: Success. @@ -122,6 +133,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the detailed information of a media stream. + * @technical preview * @param index The index of the media stream. * @param [out] info The detailed information of the media stream. See \ref media::base::PlayerStreamInfo "PlayerStreamInfo" for details. * @return @@ -132,6 +144,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Sets whether to loop the media file for playback. + * @technical preview * @param loopCount The number of times of looping the media file. * - 0: Play the media file once. * - 1: Play the media file twice. @@ -144,6 +157,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Changes the playback speed. + * @technical preview * @param speed The playback speed ref [50-400]. * @return * - 0: Success. @@ -153,6 +167,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Selects an audio track of the media file for playback. + * @technical preview * @param index The index of the audio track in media file. * @return * - 0: Success. @@ -162,6 +177,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Selects multi audio track of the media file for playback or publish to channel. + * @technical preview * @param playoutTrackIndex The index of the audio track in media file for local playback. * @param publishTrackIndex The index of the audio track in the media file published to the remote. * @@ -179,6 +195,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Changes the player option before playing a file. + * @technical preview * @param key The key of the option paramemter. * @param value The value of option parameter. * @return @@ -189,6 +206,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Changes the player option before playing a file. + * @technical preview * @param key The key of the option paramemter. * @param value The value of option parameter. * @return @@ -199,6 +217,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Takes a screenshot when playing a video file. + * @technical preview * @param filename The filename of the screenshot file. * @return * - 0: Success. @@ -208,6 +227,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Selects internal subtitles for a video file. + * @technical preview * @param index The index of the internal subtitles. * @return * - 0: Success. @@ -217,6 +237,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Sets an external subtitle file for a video file. + * @technical preview * @param url The URL of the subtitle file. * @return * - 0: Success. @@ -226,6 +247,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the playback state. + * @technical preview * @return The current playback state. See {@link media::base::MEDIA_PLAYER_STATE MEDIA_PLAYER_STATE} for details. */ virtual media::base::MEDIA_PLAYER_STATE getState() = 0; @@ -233,6 +255,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Registers a media player source observer. * + * @technical preview * Once the media player source observer is registered, you can use the observer to monitor the state change of the media player. * @param observer The pointer to the IMediaPlayerSourceObserver object. * @return @@ -243,6 +266,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Releases the media player source observer. + * @technical preview * @param observer The pointer to the IMediaPlayerSourceObserver object. * @return * - 0: Success. @@ -252,7 +276,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Registers the audio frame observer. - * + * @technical preview * @param observer The pointer to the {@link media::IAudioPcmFrameSink observer} object. * @return * - 0: Success. @@ -262,6 +286,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Releases the audio frame observer. + * @technical preview * @param observer The pointer to the {@link media::IAudioPcmFrameSink observer} object. * @return * - 0: Success. @@ -271,6 +296,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Open the Agora CDN media source. + * @technical preview * @param src The src of the media file that you want to play. * @param startPos The playback position (ms). * @@ -284,7 +310,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the number of Agora CDN lines. - * + * @technical preview * @deprecated 4.6.0 * * @return @@ -296,7 +322,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Switch Agora CDN lines. - * + * @technical preview * @deprecated 4.6.0 * * @param index Specific CDN line index. @@ -308,7 +334,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Gets the line of the current CDN. - * + * @technical preview * @deprecated 4.6.0 * * @return @@ -319,7 +345,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Enable automatic CDN line switching. - * + * @technical preview * @deprecated 4.6.0 * * @param enable Whether enable. @@ -331,7 +357,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Update the CDN source token and timestamp. - * + * @technical preview * @deprecated 4.6.0 * * @param token token. @@ -344,7 +370,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Switch the CDN source when open a media through "openWithAgoraCDNSrc" API - * + * @technical preview * @deprecated 4.6.0 * * @param src Specific src. @@ -357,6 +383,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Switch the media source when open a media through "open" API + * @technical preview * @param src Specific src. * @param syncPts Live streaming must be set to false. * @return @@ -367,6 +394,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Preload a media source + * @technical preview * @param src Specific src. * @param startPos The starting position (ms) for playback. Default value is 0. * @return @@ -377,6 +405,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Unload a preloaded media source + * @technical preview * @param src Specific src. * @return * - 0: Success. @@ -386,6 +415,7 @@ class IMediaPlayerSource : public RefCountInterface { /** * Play a pre-loaded media source + * @technical preview * @param src Specific src. * @return * - 0: Success. diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaRecorder.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaRecorder.h index b10ee2b37..46a0dc9bb 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaRecorder.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMediaRecorder.h @@ -11,6 +11,9 @@ namespace agora { namespace rtc { +/** + * @brief The IMediaRecorder interface. + */ class IMediaRecorder : public RefCountInterface { protected: virtual ~IMediaRecorder() {} @@ -32,7 +35,7 @@ class IMediaRecorder : public RefCountInterface { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int setMediaRecorderObserver(media::IMediaRecorderObserver* callback) = 0; /** @@ -69,7 +72,7 @@ class IMediaRecorder : public RefCountInterface { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. * - -2: Invalid parameter. Please ensure that: * - The specified recording file path is correct and writable. * - The specified recording file format is correct. diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMusicContentCenter.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMusicContentCenter.h index 3dc0726df..08bf12cc2 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMusicContentCenter.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraMusicContentCenter.h @@ -319,6 +319,9 @@ class MusicCollection : public RefCountInterface { }; +/** + * @brief The IMusicContentCenterEventHandler class. + */ class IMusicContentCenterEventHandler { public: /** @@ -340,7 +343,7 @@ class IMusicContentCenterEventHandler { * @brief Callback for retrieving the music resource list. * * @details - * When you call the `getMusicCollectionWithMusicChartId` method to get the music resource list of a + * When you call the `getMusicCollectionByMusicChartId` method to get the music resource list of a * specific chart or call `searchMusic` to search for music resources, the SDK * triggers this callback to report the detailed information of the music resource list. * @@ -449,6 +452,9 @@ struct MusicContentCenterConfiguration { appId(appid),token(token),mccUid(id),eventHandler(handler),maxCacheSize(maxSize), mccDomain(apiurl){} }; +/** + * @brief The IMusicPlayer interface. + */ class IMusicPlayer : public IMediaPlayer { protected: virtual ~IMusicPlayer() {}; @@ -476,7 +482,7 @@ class IMusicPlayer : public IMediaPlayer { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int open(int64_t songCode, int64_t startPos = 0) = 0; @@ -501,12 +507,15 @@ class IMusicPlayer : public IMediaPlayer { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. * - -2: Invalid parameter. Please reset the parameter. */ virtual int setPlayMode(MusicPlayMode mode) = 0; }; +/** + * @brief The IMusicContentCenter interface. + */ class IMusicContentCenter { protected: @@ -526,7 +535,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int initialize(const MusicContentCenterConfiguration & configuration) = 0; @@ -541,12 +550,12 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int renewToken(const char* token) = 0; /** - * @brief Releases all resources used by the Music Content Center. + * @brief Releases all resources used by the MusicContentCenter. * * @details * This method must be called before the `release` method of `IRtcEngine`. @@ -555,13 +564,13 @@ class IMusicContentCenter virtual void release() = 0; /** - * @brief Registers the Music Content Center event handler. + * @brief Registers the MusicContentCenter event handler. * * @param eventHandler The event handler to register. See `IMusicContentCenterEventHandler`. * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int registerEventHandler(IMusicContentCenterEventHandler* eventHandler) = 0; @@ -570,7 +579,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int unregisterEventHandler() = 0; @@ -601,7 +610,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int destroyMusicPlayer(agora_refptr music_player) = 0; @@ -616,7 +625,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int getMusicCharts(agora::util::AString& requestId) = 0; @@ -643,7 +652,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int getMusicCollectionByMusicChartId(agora::util::AString& requestId, int32_t musicChartId, int32_t page, int32_t pageSize, const char* jsonOption = nullptr) = 0; @@ -667,7 +676,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int searchMusic(agora::util::AString& requestId, const char* keyWord, int32_t page, int32_t pageSize, const char* jsonOption = nullptr) = 0; @@ -702,7 +711,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int preload(int64_t songCode, const char* jsonOption) __deprecated = 0; @@ -713,7 +722,7 @@ class IMusicContentCenter * You can call this method to preload the music resource you want to play. After successfully * calling this method, the SDK triggers the `onPreLoadEvent` callback to report the preload event. * Before calling this method to preload a music resource, you need to call - * `getMusicCollectionWithMusicChartId` or `searchMusic` + * `getMusicCollectionByMusicChartId` or `searchMusic` * to get the music resource you want to play, and obtain the song code (`songCode`) from the * `onMusicCollectionResult` callback. * @@ -725,7 +734,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int preload(agora::util::AString& requestId, int64_t songCode) = 0; @@ -742,7 +751,7 @@ class IMusicContentCenter * * @return * - 0: Success. The music resource has been deleted. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int removeCache(int64_t songCode) = 0; @@ -765,7 +774,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int getCaches(MusicCacheInfo *cacheInfo, int32_t* cacheInfoSize) = 0; @@ -780,7 +789,7 @@ class IMusicContentCenter * * @return * - 0: Success. The music resource has been preloaded. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int isPreloaded(int64_t songCode) = 0; @@ -799,7 +808,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int getLyric(agora::util::AString& requestId, int64_t songCode, int32_t lyricType = 0) = 0; @@ -808,7 +817,7 @@ class IMusicContentCenter * * @details * Before calling this method, you need to obtain the song code of the corresponding music resource. - * You can get it by calling `getMusicCollectionWithMusicChartId` or `searchMusic`, + * You can get it by calling `getMusicCollectionByMusicChartId` or `searchMusic`, * and retrieve the song code from the `onMusicCollectionResult` callback triggered by those * methods. * After you call this method, the SDK triggers the `onSongSimpleInfoResult` callback to report the @@ -819,7 +828,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int getSongSimpleInfo(agora::util::AString& requestId, int64_t songCode) = 0; @@ -835,7 +844,7 @@ class IMusicContentCenter * resource. * * @param songCode The song code of the music resource, used to identify the resource. You can - * obtain it by calling `getMusicCollectionWithMusicChartId` or `searchMusic`, + * obtain it by calling `getMusicCollectionByMusicChartId` or `searchMusic`, * and retrieve the song code from the `onMusicCollectionResult` callback triggered by those * methods. * @param jsonOption Extended JSON field, default is NULL. Currently supports the following values: @@ -847,7 +856,7 @@ class IMusicContentCenter * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int getInternalSongCode(int64_t songCode, const char* jsonOption, int64_t& internalSongCode) = 0; 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 d431173db..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 @@ -1342,6 +1342,17 @@ struct ChannelMediaOptions { * custom audio track ID through the `createCustomAudioTrack` method. */ Optional publishCustomAudioTrackId; + + /** + * Whether to publish the loopback audio from a specific source: + * - true: Publish the loopback audio from a specific source. + * - false: (Default) Do not publish the loopback audio from the specific source. + */ + Optional publishLoopbackAudioTrack; + /** + * The loopback audio track id. + */ + Optional publishLoopbackAudioTrackId; /** * Whether to publish the video captured from a custom source: * - `true`: Publish the video captured from the custom source. @@ -1532,6 +1543,13 @@ struct ChannelMediaOptions { */ Optional preferMultipathType; + /** + * The channel type. See `CHANNEL_TYPE`. + * @note Valid only when calling channel joining methods (e.g. `joinChannel`, `joinChannelEx`); ignored by `updateChannelMediaOptions`. + * @since 4.6.3 + */ + Optional channelType; + ChannelMediaOptions() {} ~ChannelMediaOptions() {} @@ -1559,6 +1577,8 @@ struct ChannelMediaOptions { SET_FROM(publishLipSyncTrack); SET_FROM(publishCustomAudioTrack); SET_FROM(publishCustomAudioTrackId); + SET_FROM(publishLoopbackAudioTrack); + SET_FROM(publishLoopbackAudioTrackId); SET_FROM(publishCustomVideoTrack); SET_FROM(publishEncodedVideoTrack); SET_FROM(publishMediaPlayerAudioTrack); @@ -1584,6 +1604,7 @@ struct ChannelMediaOptions { SET_FROM(uplinkMultipathMode); SET_FROM(downlinkMultipathMode); SET_FROM(preferMultipathType); + SET_FROM(channelType); #undef SET_FROM } @@ -1614,6 +1635,8 @@ struct ChannelMediaOptions { ADD_COMPARE(publishLipSyncTrack); ADD_COMPARE(publishCustomAudioTrack); ADD_COMPARE(publishCustomAudioTrackId); + ADD_COMPARE(publishLoopbackAudioTrack); + ADD_COMPARE(publishLoopbackAudioTrackId); ADD_COMPARE(publishCustomVideoTrack); ADD_COMPARE(publishEncodedVideoTrack); ADD_COMPARE(publishMediaPlayerAudioTrack); @@ -1639,6 +1662,7 @@ struct ChannelMediaOptions { ADD_COMPARE(uplinkMultipathMode); ADD_COMPARE(downlinkMultipathMode); ADD_COMPARE(preferMultipathType); + ADD_COMPARE(channelType); END_COMPARE(); #undef BEGIN_COMPARE @@ -1672,6 +1696,8 @@ struct ChannelMediaOptions { REPLACE_BY(publishLipSyncTrack); REPLACE_BY(publishCustomAudioTrack); REPLACE_BY(publishCustomAudioTrackId); + REPLACE_BY(publishLoopbackAudioTrack); + REPLACE_BY(publishLoopbackAudioTrackId); REPLACE_BY(publishCustomVideoTrack); REPLACE_BY(publishEncodedVideoTrack); REPLACE_BY(publishMediaPlayerAudioTrack); @@ -1697,6 +1723,7 @@ struct ChannelMediaOptions { REPLACE_BY(uplinkMultipathMode); REPLACE_BY(downlinkMultipathMode); REPLACE_BY(preferMultipathType); + REPLACE_BY(channelType); #undef REPLACE_BY } return *this; @@ -2500,6 +2527,10 @@ class IRtcEngineEventHandler { /** * @brief Occurs when a specific remote user enables/disables the local video capturing function. + * + * @deprecated This method is deprecated. Use the following enumerators from `onRemoteVideoStateChanged`: + * - `REMOTE_VIDEO_STATE_STOPPED`(0) and `REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED`(5). + * - `REMOTE_VIDEO_STATE_DECODING`(2) and `REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED`(6). * * @details * The SDK triggers this callback when the remote user resumes or stops capturing the video stream @@ -2708,7 +2739,7 @@ class IRtcEngineEventHandler { /** * @brief Occurs when the state of virtual metronome changes. - * + * @deprecated v4.6.0 * @details * When the state of the virtual metronome changes, the SDK triggers this callback to report the * current state of the virtual metronome. This callback indicates the state of the local audio @@ -2992,7 +3023,7 @@ class IRtcEngineEventHandler { * occur. * * @note When the state is `LOCAL_AUDIO_STREAM_STATE_FAILED` (3), you can view the error information - * in the `error` parameter. + * in the `reason` parameter. * * @param state The state of the local audio. See `LOCAL_AUDIO_STREAM_STATE`. * @param reason Reasons for local audio state changes. See `LOCAL_AUDIO_STREAM_REASON`. @@ -3658,14 +3689,17 @@ class IRtcEngineEventHandler { } /** - * @brief Report the multipath transmission statistics - * - * @post This callback is triggered after you set `enableMultipath` to `true` to enable multipath transmission. + * @brief Callback for multipath transmission statistics. * * @since 4.6.0 * - * @param stats The multipath statistics. See the MultipathStats structure for details. - */ virtual void onMultipathStats(const MultipathStats& stats) { + * @details + * Call timing: This callback is triggered after you set `enableMultipath` to `true` to enable + * multipath transmission. + * + * @param stats Multipath transmission statistics. See `MultipathStats`. + */ + virtual void onMultipathStats(const MultipathStats& stats) { (void)stats; } @@ -3757,7 +3791,7 @@ class IVideoDeviceManager { /** * Specifies the video capture device with the device ID. * - * @param deviceIdUTF8 he device ID. You can get the device ID by calling `enumerateVideoDevices`. + * @param deviceIdUTF8 The device ID. You can get the device ID by calling `enumerateVideoDevices`. * The maximum length is #MAX_DEVICE_ID_LENGTH. * * @return @@ -3885,6 +3919,10 @@ class IVideoEffectObject : public RefCountInterface { * (4): Filter effect node. */ FILTER = 1U << 2, + /** + * (8): Sticker effect node. + */ + STICKER = 1U << 3, }; /** @@ -3960,6 +3998,21 @@ class IVideoEffectObject : public RefCountInterface { * - < 0: Failure. */ virtual int performVideoEffectAction(uint32_t nodeId, VIDEO_EFFECT_ACTION actionId) = 0; + + /** + * @brief Sets the string parameter for the video effect. + * + * @since v4.6.0 + * + * @param option The option category of the option. + * @param key The key name of the parameter. + * @param param The string value to set. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int setVideoEffectStringParam(const char* option, const char* key, const char* param) = 0; /** * @brief Sets the float parameter for video effects. @@ -4155,10 +4208,18 @@ struct RtcEngineContext { */ bool autoRegisterAgoraExtensions; + + /* + * Provides the technical preview functionalities or special customizations by configuring the SDK with JSON options. + * Pointer to the set parameters in a JSON string. + * @technical preview + */ + const char* parameters; + RtcEngineContext() : eventHandler(NULL), appId(NULL), context(NULL), channelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING), license(NULL), audioScenario(AUDIO_SCENARIO_DEFAULT), areaCode(AREA_CODE_GLOB), - logConfig(), useExternalEglContext(false), domainLimit(false), autoRegisterAgoraExtensions(true) {} + logConfig(), useExternalEglContext(false), domainLimit(false), autoRegisterAgoraExtensions(true), parameters(NULL) {} }; /** Definition of IMetadataObserver @@ -4243,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`. @@ -4732,8 +4794,7 @@ class IRtcEngine : public agora::base::IEngineBase { * - When preloading one channel, calling this method to pass in the new token. * - When preloading more than one channels: * - If you use a wildcard token for all preloaded channels, call `updatePreloadChannelToken` to - * update the token.Note: When generating a wildcard token, ensure the user ID is not set as 0. See - * `Secure authentication with tokens`. + * update the token. Note: When generating a wildcard token, ensure the user ID is not set as 0. * - If you use different tokens to preload different channels, call this method to pass in your * user ID, channel name and the new token. * @param channelId The channel name that you want to preload. This parameter signifies the channel @@ -4792,8 +4853,7 @@ class IRtcEngine : public agora::base::IEngineBase { * - When preloading one channel, calling this method to pass in the new token. * - When preloading more than one channels: * - If you use a wildcard token for all preloaded channels, call `updatePreloadChannelToken` to - * update the token.Note: When generating a wildcard token, ensure the user ID is not set as 0. See - * `Secure authentication with tokens`. + * update the token. Note: When generating a wildcard token, ensure the user ID is not set as 0. * - If you use different tokens to preload different channels, call this method to pass in your * user ID, channel name and the new token. * @param channelId The channel name that you want to preload. This parameter signifies the channel @@ -4858,7 +4918,7 @@ class IRtcEngine : public agora::base::IEngineBase { * @details * By default, the user subscribes to the audio and video streams of all the other users in the * channel, giving rise to usage and **billings**. To stop subscribing to a specified stream or all - * remote streams, call the corresponding `mute` methods. + * remote streams, call `muteRemoteAudio/VideoStream` or `muteAllRemoteAudio/VideoStreams`. * Call timing: Call this method after `initialize`. * Related callbacks: A successful call of this method triggers the following callbacks: * - The local client: The `onJoinChannelSuccess` and `onConnectionStateChanged` callbacks. @@ -4882,7 +4942,7 @@ class IRtcEngine : public agora::base::IEngineBase { * optional. You will automatically exit the channel 24 hours after successfully joining in. * - If you need to join different channels at the same time or switch between channels, Agora * recommends using a wildcard token so that you don't need to apply for a new token every time - * joining a channel. See `Secure authentication with tokens`. + * joining a channel. * @param channelId The channel name. This parameter signifies the channel in which users engage in * real-time audio and video interaction. Under the premise of the same App ID, users who fill in * the same channel ID enter the same channel for audio and video interaction. The string length @@ -4935,7 +4995,7 @@ class IRtcEngine : public agora::base::IEngineBase { * automatically subscribe to the audio and video streams of all remote users when joining a * channel. By default, the user subscribes to the audio and video streams of all the other users in * the channel, giving rise to usage and **billings**. To stop subscribing to other streams, set the - * `options` parameter or call the corresponding `mute` methods. + * `options` parameter or call `muteRemoteAudio/VideoStream` or `muteAllRemoteAudio/VideoStreams`. * Call timing: Call this method after `initialize`. * Related callbacks: A successful call of this method triggers the following callbacks: * - The local client: The `onJoinChannelSuccess` and `onConnectionStateChanged` callbacks. @@ -4959,7 +5019,7 @@ class IRtcEngine : public agora::base::IEngineBase { * optional. You will automatically exit the channel 24 hours after successfully joining in. * - If you need to join different channels at the same time or switch between channels, Agora * recommends using a wildcard token so that you don't need to apply for a new token every time - * joining a channel. See `Secure authentication with tokens`. + * joining a channel. * @param channelId The channel name. This parameter signifies the channel in which users engage in * real-time audio and video interaction. Under the premise of the same App ID, users who fill in * the same channel ID enter the same channel for audio and video interaction. The string length @@ -5146,10 +5206,10 @@ class IRtcEngine : public agora::base::IEngineBase { * as host. The user role ( `roles` ) determines the users' permissions at the SDK level, including * whether they can publish audio and video streams in a channel. * Call timing: You can call this method either before or after joining a channel. - * If you call this method to set the user role as the host before joining the channel and set the + * - If you call this method to set the user role as the host before joining the channel and set the * local video property through the `setupLocalVideo` method, the local video preview is * automatically enabled when the user joins the channel. - * If you call this method to set the user role after joining a channel, the SDK will automatically + * - If you call this method to set the user role after joining a channel, the SDK will automatically * call the `muteLocalAudioStream` and `muteLocalVideoStream` method to change the state for * publishing audio and video streams. * Related callbacks: If you call this method to switch the user role after joining the channel, the @@ -5194,10 +5254,10 @@ class IRtcEngine : public agora::base::IEngineBase { * to determine the level of service that users can enjoy within their permissions. For example, an * audience member can choose to receive remote streams with low latency or ultra-low latency. * Call timing: You can call this method either before or after joining a channel. - * If you call this method to set the user role as the host before joining the channel and set the + * - If you call this method to set the user role as the host before joining the channel and set the * local video property through the `setupLocalVideo` method, the local video preview is * automatically enabled when the user joins the channel. - * If you call this method to set the user role after joining a channel, the SDK will automatically + * - If you call this method to set the user role after joining a channel, the SDK will automatically * call the `muteLocalAudioStream` and `muteLocalVideoStream` method to change the state for * publishing audio and video streams. * Related callbacks: If you call this method to switch the user role after joining the channel, the @@ -6728,24 +6788,30 @@ class IRtcEngine : public agora::base::IEngineBase { virtual int enableAudioVolumeIndication(int interval, int smooth, bool reportVad) = 0; /** - * @brief Starts client-side audio recording with recording configuration. + * @brief Starts client-side audio recording and sets the recording sample rate. * * @details * The SDK supports recording on the client during a call. After calling this method, you can record - * the audio of users in the channel and obtain a recording file. The recording file supports the - * following formats only: - * - WAV: Higher audio fidelity, larger file size. For example, with a sample rate of 32000 Hz, a - * 10-minute recording is about 73 MB. - * - AAC: Lower audio fidelity, smaller file size. For example, with a sample rate of 32000 Hz and - * recording quality set to AUDIO_RECORDING_QUALITY_MEDIUM, a 10-minute recording is about 2 MB. - * Recording automatically stops when the user leaves the channel. - * Call timing: This method must be called after joining a channel. + * the audio of all users in the channel and obtain a recording file that includes all voices. The + * recording file supports the following formats only: + * - .wav: Large file size, higher audio fidelity. + * - .aac: Smaller file size, lower audio fidelity. * - * @param config Recording configuration. See `AudioRecordingConfiguration`. + * @note + * - Make sure the path you specify in this method exists and is writable. + * - This method must be called after `joinChannel(const char* token, const char* channelId, uid_t + * uid, const ChannelMediaOptions& options)`. If `leaveChannel(const LeaveChannelOptions& options)` + * is called while recording is in progress, the recording will automatically stop. + * + * @param filePath The absolute path where the recording file will be saved locally, including the + * file name and extension. For example: `C:\music\audio.aac`. + * Note: + * Make sure the specified path exists and is writable. + * @param quality Recording quality. See `AUDIO_RECORDING_QUALITY_TYPE`. * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int startAudioRecording(const char* filePath, AUDIO_RECORDING_QUALITY_TYPE quality) = 0; @@ -6781,30 +6847,32 @@ class IRtcEngine : public agora::base::IEngineBase { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int startAudioRecording(const char* filePath, int sampleRate, AUDIO_RECORDING_QUALITY_TYPE quality) = 0; - /** Starts an audio recording. - - The SDK allows recording during a call, which supports either one of the - following two formats: - - - .wav: Large file size with high sound fidelity - - .aac: Small file size with low sound fidelity - - Ensure that the directory to save the recording file exists and is writable. - This method is usually called after the joinChannel() method. - The recording automatically stops when the leaveChannel() method is - called. - - @param config Audio recording config. - @return - - 0: Success. - - < 0: Failure. - */ + /** + * @brief Starts client-side audio recording with recording configuration. + * + * @details + * The SDK supports recording on the client during a call. After calling this method, you can record + * the audio of users in the channel and obtain a recording file. The recording file supports the + * following formats only: + * - WAV: Higher audio fidelity, larger file size. For example, with a sample rate of 32000 Hz, a + * 10-minute recording is about 73 MB. + * - AAC: Lower audio fidelity, smaller file size. For example, with a sample rate of 32000 Hz and + * recording quality set to AUDIO_RECORDING_QUALITY_MEDIUM, a 10-minute recording is about 2 MB. + * Recording automatically stops when the user leaves the channel. + * Call timing: This method must be called after joining a channel. + * + * @param config Recording configuration. See `AudioRecordingConfiguration`. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ virtual int startAudioRecording(const AudioRecordingConfiguration& config) = 0; /** @@ -6830,7 +6898,7 @@ class IRtcEngine : public agora::base::IEngineBase { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int stopAudioRecording() = 0; @@ -6894,57 +6962,16 @@ class IRtcEngine : public agora::base::IEngineBase { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int destroyMediaRecorder(agora_refptr mediaRecorder) = 0; - /** Starts playing and mixing the music file. - - This method mixes the specified local audio file with the audio stream from - the microphone. You can choose whether the other user can hear the local - audio playback and specify the number of playback loops. This method also - supports online music playback. - - After calling this method successfully, the SDK triggers the - - \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (PLAY) - callback on the local client. - When the audio mixing file playback finishes after calling this method, the - SDK triggers the - \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (STOPPED) - callback on the local client. - - @note - - Call this method after joining a channel, otherwise issues may occur. - - If the local audio mixing file does not exist, or if the SDK does not - support the file format or cannot access the music file URL, the SDK returns - #WARN_AUDIO_MIXING_OPEN_ERROR (701). - - If you want to play an online music file, ensure that the time interval - between calling this method is more than 100 ms, or the - `AUDIO_MIXING_ERROR_TOO_FREQUENT_CALL(702)` error code occurs. - - @param filePath Pointer to the absolute path (including the suffixes of the - filename) of the local or online audio file to mix, for example, c:/music/au - dio.mp4. Supported audio formats: 3GP, ASF, ADTS, AVI, MP3, MP4, MPEG-4, - SAMI, and WAVE. - @param loopback Sets which user can hear the audio mixing: - - true: Only the local user can hear the audio mixing. - - false: Both users can hear the audio mixing. - - @param cycle Sets the number of playback loops: - - Positive integer: Number of playback loops. - - `-1`: Infinite playback loops. - - @return - - 0: Success. - - < 0: Failure. - */ /** * @brief Starts playing the music file. * + * @deprecated This method is deprecated. Use startAudioMixing(const char* filePath, bool loopback, int cycle, int startPos) instead. * @details - * For the audio file formats supported by this method, see `What formats of audio files does the - * Agora RTC SDK support`. If the local music file does not exist, the SDK does not support the file + * For the audio file formats supported by this method, see [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). If the local music file does not exist, the SDK does not support the file * format, or the the SDK cannot access the music file URL, the SDK reports * AUDIO_MIXING_REASON_CAN_NOT_OPEN. * Call timing: You can call this method either before or after joining a channel. @@ -6968,7 +6995,7 @@ class IRtcEngine : public agora::base::IEngineBase { * * @param filePath The file path. The SDK supports URLs and absolute path of local files. The * absolute path needs to be accurate to the file name and extension. Supported audio formats - * include MP3, AAC, M4A, MP4, WAV, and 3GP. See `Supported Audio Formats`. + * include MP3, AAC, M4A, MP4, WAV, and 3GP. See [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). * Attention: If you have preloaded an audio effect into memory by calling `preloadEffect`, ensure * that the value of this parameter is the same as that of `filePath` in `preloadEffect`. * @param loopback Whether to only play music files on the local client: @@ -6992,55 +7019,11 @@ class IRtcEngine : public agora::base::IEngineBase { */ virtual int startAudioMixing(const char* filePath, bool loopback, int cycle) = 0; - /** Starts playing and mixing the music file. - - This method mixes the specified local audio file with the audio stream from - the microphone. You can choose whether the other user can hear the local - audio playback and specify the number of playback loops. This method also - supports online music playback. - - After calling this method successfully, the SDK triggers the - - \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (PLAY) - callback on the local client. - When the audio mixing file playback finishes after calling this method, the - SDK triggers the - \ref IRtcEngineEventHandler::onAudioMixingStateChanged "onAudioMixingStateChanged" (STOPPED) - callback on the local client. - - @note - - Call this method after joining a channel, otherwise issues may occur. - - If the local audio mixing file does not exist, or if the SDK does not - support the file format or cannot access the music file URL, the SDK returns - #WARN_AUDIO_MIXING_OPEN_ERROR (701). - - If you want to play an online music file, ensure that the time interval - between calling this method is more than 100 ms, or the - `AUDIO_MIXING_ERROR_TOO_FREQUENT_CALL(702)` error code occurs. - - @param filePath Pointer to the absolute path (including the suffixes of the - filename) of the local or online audio file to mix, for example, c:/music/au - dio.mp4. Supported audio formats: 3GP, ASF, ADTS, AVI, MP3, MP4, MPEG-4, - SAMI, and WAVE. - @param loopback Sets which user can hear the audio mixing: - - true: Only the local user can hear the audio mixing. - - false: Both users can hear the audio mixing. - - @param cycle Sets the number of playback loops: - - Positive integer: Number of playback loops. - - `-1`: Infinite playback loops. - - @param startPos The playback position (ms) of the music file. - - @return - - 0: Success. - - < 0: Failure. - */ /** * @brief Starts playing the music file. * * @details - * For the audio file formats supported by this method, see `What formats of audio files does the - * Agora RTC SDK support`. If the local music file does not exist, the SDK does not support the file + * For the audio file formats supported by this method, see [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). If the local music file does not exist, the SDK does not support the file * format, or the the SDK cannot access the music file URL, the SDK reports * AUDIO_MIXING_REASON_CAN_NOT_OPEN. * Call timing: You can call this method either before or after joining a channel. @@ -7145,8 +7128,7 @@ class IRtcEngine : public agora::base::IEngineBase { * you can call this method to set the playback language. * * @note - * - For the supported formats of audio files, see - * `https://docs.agora.io/en/help/general-product-inquiry/audio_format#extended-audio-file-formats`. + * - For the supported formats of audio files, see [Which audio file formats does the Agora Video SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format#extended-audio-file-formats). * - You need to call this method after calling `startAudioMixing(const char* filePath, bool * loopback, int cycle, int startPos)` and receiving the * `onAudioMixingStateChanged` `(AUDIO_MIXING_STATE_PLAYING)` callback. @@ -7413,8 +7395,7 @@ class IRtcEngine : public agora::base::IEngineBase { * * @details * Ensure the size of all preloaded files does not exceed the limit. - * For the audio file formats supported by this method, see `What formats of audio files does the - * Agora RTC SDK support`. + * For the audio file formats supported by this method, see [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). * Call timing: Agora recommends that you call this method before joining a channel. * * @note @@ -7466,7 +7447,7 @@ class IRtcEngine : public agora::base::IEngineBase { * this parameter is the same as that of `soundId` in `preloadEffect`. * @param filePath The file path. The SDK supports URLs and absolute path of local files. The * absolute path needs to be accurate to the file name and extension. Supported audio formats - * include MP3, AAC, M4A, MP4, WAV, and 3GP. See `Supported Audio Formats`. + * include MP3, AAC, M4A, MP4, WAV, and 3GP. See [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). * Attention: If you have preloaded an audio effect into memory by calling `preloadEffect`, ensure * that the value of this parameter is the same as that of `filePath` in `preloadEffect`. * @param loopCount The number of times the audio effect loops. @@ -8112,7 +8093,7 @@ class IRtcEngine : public agora::base::IEngineBase { * The SDK provides an easier-to-use method, `setAudioEffectPreset`, to directly implement preset * reverb effects for such as pop, R&B, and KTV. * - * @note You can call this method either before or after joining a channel. + * Call timing: You can call this method either before or after joining a channel. * * @param reverbKey The reverberation key. Agora provides five reverberation keys, see * `AUDIO_REVERB_TYPE`. @@ -8184,7 +8165,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the log file. - * + * @deprecated Use `context` in `initialize` to set up the log file path. * @details * Specifies an SDK output log file. The log file records all log data for the SDK’s operation. * Call timing: This method needs to be called immediately after `initialize`, otherwise the output @@ -8202,7 +8183,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the log output level of the SDK. - * + * @deprecated Use `logConfig` in `initialize` to set the log filter. * @details * This method sets the output log level of the SDK. You can use one or a combination of the log * filter levels. The log level follows the sequence of `LOG_FILTER_OFF`, `LOG_FILTER_CRITICAL`, @@ -8221,6 +8202,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the output log level of the SDK. + * @deprecated Use `context` in `initialize` to set the log level. * * @details * Choose a level to see the logs preceding that level. @@ -8235,7 +8217,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the log file size. - * + * @deprecated Use `logConfig` in `initialize` to set the log file size. * @details * By default, the SDK generates five SDK log files and five API call log files with the following * rules: @@ -8358,7 +8340,7 @@ class IRtcEngine : public agora::base::IEngineBase { * * @param sourceType The type of the video source. See `VIDEO_SOURCE_TYPE`. * @param targetFps The capture frame rate (fps) of the local video. Sopported values are: 1, 7, 10, - * 15, 24, 30, 60.CAUTION: Set this parameter to a value lower than the actual video frame rate; + * 15, 24, 30, 60. CAUTION: Set this parameter to a value lower than the actual video frame rate; * otherwise, the settings do not take effect. * * @return @@ -8389,9 +8371,12 @@ class IRtcEngine : public agora::base::IEngineBase { // The following APIs are either deprecated and going to deleted. /** - * Updates the display mode of the local video view. + * @brief Updates the display mode of the local video view. * - * After initializing the local video view, you can call this method to update its rendering mode. + * @deprecated This method is deprecated since v4.0.0. Use `setLocalRenderMode(RENDER_MODE_TYPE + * renderMode, VIDEO_MIRROR_MODE_TYPE mirrorMode)` instead. + * + * @details After initializing the local video view, you can call this method to update its rendering mode. * It affects only the video view that the local user sees, not the published local video stream. * * @note @@ -8409,6 +8394,8 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the local video mirror mode. + * + * @deprecated This method is deprecated. Use `setupLocalVideo` or `setLocalRenderMode` instead. * * @param mirrorMode The local video mirror mode. See `VIDEO_MIRROR_MODE_TYPE`. * @@ -8421,6 +8408,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Enables or disables dual-stream mode on the sender side. * + * @deprecated This method is deprecated since v4.2.0. Use `setDualStreamMode` instead. * @details * Dual streams are a pairing of a high-quality video stream and a low-quality video stream: * - High-quality video stream: High bitrate, high resolution. @@ -8448,6 +8436,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the dual-stream mode on the sender side and the low-quality video stream. * + * @deprecated This method is deprecated since v4.2.0. Use `setDualStreamMode` instead. * @details * You can call this method to enable or disable the dual-stream mode on the publisher side. Dual * streams are a pairing of a high-quality video stream and a low-quality video stream: @@ -8513,7 +8502,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Sets the simulcast video stream configuration. * - * @since v4.6.0 + * @technical preview * * @details * You can call this method to set video streams with different resolutions for the same video @@ -9835,18 +9824,19 @@ class IRtcEngine : public agora::base::IEngineBase { * `LOCAL_VIDEO_STREAM_STATE_CAPTURING` (1). * * @note - * Due to the high performance requirements of this feature, you need to use it on the following + * - This method is for iOS and macOS only. + * - Due to the high performance requirements of this feature, you need to use it on the following * types of devices or devices with higher performance: - * - iPad: - * - 12.9-inch iPad Pro (5th generation) - * - 11-inch iPad Pro (3rd generation) - * - iPad (9th generation) - * - iPad mini (6th generation) - * - iPad Air (5th generation) - * - 2020 M1 MacBook Pro 13-inch + iPhone 11 (using iPhone as external camera for the MacBook) - * Agora recommends that you call `isCameraCenterStageSupported` to check whether the current device + * - iPad: + * - 12.9-inch iPad Pro (5th generation) + * - 11-inch iPad Pro (3rd generation) + * - iPad (9th generation) + * - iPad mini (6th generation) + * - iPad Air (5th generation) + * - 2020 M1 MacBook Pro 13-inch + iPhone 11 (using iPhone as external camera for the MacBook) + * Agora recommends that you call `isCameraCenterStageSupported` to check whether the current device * supports portrait center stage before enabling this feature. - * This method is for iOS and macOS only. + * * * @param enabled Whether to enable the portrait center stage: * - `true`: Enable portrait center stage. @@ -9873,13 +9863,13 @@ class IRtcEngine : public agora::base::IEngineBase { * @note This method applies to macOS and Windows only. * * @param thumbSize The target size of the screen or window thumbnail (the width and height are in - * pixels). See `SIZE`. The SDK scales the original image to make the length of the longest side of + * pixels). See [SIZE](https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-size). The SDK scales the original image to make the length of the longest side of * the image the same as that of the target size without distorting the original image. For example, * if the original image is 400 × 300 and thumbSize is 100 × 100, the actual size of the `thumbnail` * is 100 × 75. If the target size is larger than the original size, the thumbnail is the original * image and the SDK does not scale it. * @param iconSize The target size of the icon corresponding to the application program (the width - * and height are in pixels). See `SIZE`. The SDK scales the original image to make the length of + * and height are in pixels). See [SIZE](https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-size). The SDK scales the original image to make the length of * the longest side of the image the same as that of the target size without distorting the original * image. For example, if the original image is 400 × 300 and iconSize is 100 × 100, the actual size * of the ` icon` is 100 × 75. If the target size is larger than the original size, the icon is the @@ -9933,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 @@ -9979,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. * @@ -10033,6 +10025,25 @@ class IRtcEngine : public agora::base::IEngineBase { virtual int getAudioDeviceInfo(DeviceInfo& deviceInfo) = 0; #endif // __ANDROID__ + /** + * Sets the rotation of remote render. + * + * @since v4.6.4 + * + * @note + * For example, the sending end is a screen sharing scene. + * When the screen of the sending end changes horizontally or vertically, + * the rendered screen of the receiving end also wants to change synchronously with that of the peer end. + * In this case, you can set the angle of the receiving end. + * + * @param uid The user ID of the remote user. + * @param rotation The clockwise rotation angle. See #VIDEO_ORIENTATION. + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int setRemoteRenderRotation(uid_t uid, VIDEO_ORIENTATION rotation) = 0; + #if defined(_WIN32) || (defined(__APPLE__) && TARGET_OS_MAC && !TARGET_OS_IPHONE) || (defined(__linux__) && !defined(__ANDROID__) && !defined(__OHOS__)) /** @@ -10075,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 @@ -10087,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 @@ -10188,6 +10201,7 @@ class IRtcEngine : public agora::base::IEngineBase { * `publishScreenCaptureVideo` to `true` to start screen sharing. * * @note + * - This method is for Android and iOS only. * - On the iOS platform, screen sharing is only available on iOS 12.0 and later. * - If you are using the custom audio source instead of the SDK to capture audio, Agora recommends * you add the keep-alive processing logic to your application to avoid screen sharing stopping when @@ -10211,7 +10225,6 @@ class IRtcEngine : public agora::base::IEngineBase { * - To avoid system audio capture failure when sharing screen, Agora recommends that you set the * audio application scenario to `AUDIO_SCENARIO_GAME_STREAMING` by using the `setAudioScenario` * method before joining the channel. - * - This method is for Android and iOS only. * - The billing for the screen sharing stream is based on the `dimensions` in * `ScreenVideoParameters`: * - When you do not pass in a value, Agora bills you at 1280 × 720. @@ -10311,8 +10324,6 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Configures `MediaProjection` outside of the SDK to capture screen video streams. * - * @technical preview - * * @details * After successfully calling this method, the external `MediaProjection` you set will replace the * `MediaProjection` requested by the SDK to capture the screen video stream. @@ -10330,7 +10341,7 @@ class IRtcEngine : public agora::base::IEngineBase { * captureParams)`. * * @note - * Before calling this method, you must first apply for `MediaProjection` permission. + * Before calling this method, you must first apply for [`MediaProjection`](https://developer.android.com/reference/android/media/projection/MediaProjection) permission. * This method is for Android only. * * @param mediaProjection An `MediaProjection` object used to capture screen video streams. @@ -10438,8 +10449,8 @@ class IRtcEngine : public agora::base::IEngineBase { * @brief Starts pushing media streams to a CDN without transcoding. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Push streams using RESTful + * APIs](https://docs.agora.io/en/media-push/develop/restful-api). * You can call this method to push an audio or video stream to the specified CDN address. This * method can push media streams to only one CDN address at a time, so if you need to push streams * to multiple addresses, call this method multiple times. @@ -10470,8 +10481,8 @@ class IRtcEngine : public agora::base::IEngineBase { * @brief Starts Media Push and sets the transcoding configuration. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Pushing streams using RESTful + * APIs](https://docs.agora.io/en/media-push/develop/restful-api). * You can call this method to push a live audio-and-video stream to the specified CDN address and * set the transcoding configuration. This method can push media streams to only one CDN address at * a time, so if you need to push streams to multiple addresses, call this method multiple times. @@ -10505,8 +10516,8 @@ class IRtcEngine : public agora::base::IEngineBase { * @brief Updates the transcoding configuration. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Pushing streams using RESTful + * APIs](https://docs.agora.io/en/media-push/develop/restful-api). * After you start pushing media streams to CDN with transcoding, you can dynamically update the * transcoding configuration according to the scenario. The SDK triggers the `onTranscodingUpdated` * callback after the transcoding configuration is updated. @@ -10599,8 +10610,8 @@ class IRtcEngine : public agora::base::IEngineBase { * @brief Stops pushing media streams to a CDN. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Use RESTful + * API](https://docs.agora.io/en/media-push/develop/restful-api). * You can call this method to stop the live stream on the specified CDN address. This method can * stop pushing media streams to only one CDN address at a time, so if you need to stop pushing * streams to multiple addresses, call this method multiple times. @@ -10798,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 @@ -10929,7 +10943,7 @@ class IRtcEngine : public agora::base::IEngineBase { * stream will be destroyed when leaving the channel, and the data stream needs to be recreated if * needed. * If you need a more comprehensive solution for low-latency, high-concurrency, and scalable - * real-time messaging and status synchronization, it is recommended to use `Signaling`. + * real-time messaging and status synchronization, it is recommended to use [Signaling](https://docs.agora.io/en/signaling/overview/product-overview). * * @param streamId An output parameter; the ID of the data stream created. * @param reliable Sets whether the recipients are guaranteed to receive the data stream within five @@ -10966,7 +10980,7 @@ class IRtcEngine : public agora::base::IEngineBase { * stream will be destroyed when leaving the channel, and the data stream needs to be recreated if * needed. * If you need a more comprehensive solution for low-latency, high-concurrency, and scalable - * real-time messaging and status synchronization, it is recommended to use `Signaling`. + * real-time messaging and status synchronization, it is recommended to use [Signaling](https://docs.agora.io/en/signaling/overview/product-overview). * * @param streamId An output parameter; the ID of the data stream created. * @param config The configurations for the data stream. See `DataStreamConfig`. @@ -10995,7 +11009,7 @@ class IRtcEngine : public agora::base::IEngineBase { * * @note * If you need a more comprehensive solution for low-latency, high-concurrency, and scalable - * real-time messaging and status synchronization, it is recommended to use `Signaling`. + * real-time messaging and status synchronization, it is recommended to use [Signaling](https://docs.agora.io/en/signaling/overview/product-overview). * - This method needs to be called after `createDataStream(int* streamId, const DataStreamConfig& * config)` and joining the channel. * - This method applies to broadcasters only. @@ -11072,8 +11086,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Adds a watermark image to the local video. * - * @deprecated Use addVideoWatermarkEx(const WatermarkConfig& config, const RtcConnection& - * connection) instead. + * @deprecated Use addVideoWatermark(const WatermarkConfig& configs) instead. * * @details * This method adds a PNG watermark image to the local video in the live streaming. Once the @@ -11184,10 +11197,9 @@ class IRtcEngine : public agora::base::IEngineBase { virtual int resumeAudio() __deprecated = 0; /** - * @brief Enables interoperability with the Agora Web SDK (applicable only in the live streaming + * @brief Enables interoperability with the Agora Web SDK (applicable only in the live streaming scenarios. * - * @deprecated The Agora NG SDK enables the interoperablity with the Web SDK. - * scenarios). + * @deprecated By default, the Agora RTC SDK enables the interoperablity with the Web SDK. * * @details * You can call this method to enable or disable interoperability with the Agora Web SDK. If a @@ -11213,7 +11225,7 @@ class IRtcEngine : public agora::base::IEngineBase { * Agora supports reporting and analyzing customized messages. This function is in the beta stage * with a free trial. The ability provided in its beta test version is reporting a maximum of 10 * message pieces within 6 seconds, with each message piece not exceeding 256 bytes and each string - * not exceeding 100 bytes. To try out this function, contact `support@agora.io` and discuss the + * not exceeding 100 bytes. To try out this function, contact sales and discuss the * format of customized messages with us. * */ @@ -11377,7 +11389,7 @@ class IRtcEngine : public agora::base::IEngineBase { * then calling this method to join a channel can shorten the time it takes to enter the channel. * Once a user joins the channel, the user subscribes to the audio and video streams of all the * other users in the channel by default, giving rise to usage and billings. To stop subscribing to - * a specified stream or all remote streams, call the corresponding `mute` methods. + * a specified stream or all remote streams, call `muteRemoteAudio/VideoStream` or `muteAllRemoteAudio/VideoStreams`. * Call timing: Call this method after `initialize`. * Related callbacks: After the user successfully joins the channel, the SDK triggers the following * callbacks: @@ -11405,7 +11417,7 @@ class IRtcEngine : public agora::base::IEngineBase { * optional. You will automatically exit the channel 24 hours after successfully joining in. * - If you need to join different channels at the same time or switch between channels, Agora * recommends using a wildcard token so that you don't need to apply for a new token every time - * joining a channel. See `Secure authentication with tokens`. + * joining a channel. * @param channelId The channel name. This parameter signifies the channel in which users engage in * real-time audio and video interaction. Under the premise of the same App ID, users who fill in * the same channel ID enter the same channel for audio and video interaction. The string length @@ -11465,7 +11477,7 @@ class IRtcEngine : public agora::base::IEngineBase { * used to set media options, such as whether to publish audio and video streams within a channel. * By default, the user subscribes to the audio and video streams of all the other users in the * channel, giving rise to usage and **billings**. To stop subscribing to other streams, set the - * `options` parameter or call the corresponding `mute` methods. + * `options` parameter or call `muteRemoteAudio/VideoStream` or `muteAllRemoteAudio/VideoStreams`. * Call timing: Call this method after `initialize`. * Related callbacks: After the user successfully joins the channel, the SDK triggers the following * callbacks: @@ -11493,7 +11505,7 @@ class IRtcEngine : public agora::base::IEngineBase { * optional. You will automatically exit the channel 24 hours after successfully joining in. * - If you need to join different channels at the same time or switch between channels, Agora * recommends using a wildcard token so that you don't need to apply for a new token every time - * joining a channel. See `Secure authentication with tokens`. + * joining a channel. * @param channelId The channel name. This parameter signifies the channel in which users engage in * real-time audio and video interaction. Under the premise of the same App ID, users who fill in * the same channel ID enter the same channel for audio and video interaction. The string length @@ -11551,8 +11563,7 @@ class IRtcEngine : public agora::base::IEngineBase { * then calling this method to join a channel can shorten the time it takes to enter the channel. * Once a user joins the channel, the user subscribes to the audio and video streams of all the * other users in the channel by default, giving rise to usage and **billings**. If you want to stop - * subscribing to the media stream of other users, you can set the `options` parameter or call the - * corresponding `mute` method. + * subscribing to the media stream of other users, you can set the `options` parameter or call `muteRemoteAudio/VideoStream` or `muteAllRemoteAudio/VideoStreams`. * Call timing: Call this method after `initialize`. * Related callbacks: After the user successfully joins the channel, the SDK triggers the following * callbacks: @@ -11580,7 +11591,7 @@ class IRtcEngine : public agora::base::IEngineBase { * optional. You will automatically exit the channel 24 hours after successfully joining in. * - If you need to join different channels at the same time or switch between channels, Agora * recommends using a wildcard token so that you don't need to apply for a new token every time - * joining a channel. See `Secure authentication with tokens`. + * joining a channel. * @param channelId The channel name. This parameter signifies the channel in which users engage in * real-time audio and video interaction. Under the premise of the same App ID, users who fill in * the same channel ID enter the same channel for audio and video interaction. The string length @@ -11878,7 +11889,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Enables the virtual metronome. - * + * @deprecated v4.6.0 * @details * - After enabling the virtual metronome, the SDK plays the specified audio effect file from the * beginning, and controls the playback duration of each file according to `beatsPerMinute` you set @@ -11898,10 +11909,10 @@ class IRtcEngine : public agora::base::IEngineBase { * * @param sound1 The absolute path or URL address (including the filename extensions) of the file * for the downbeat. For example, `C:\music\audio.mp4`. For the audio file formats supported by this - * method, see `What formats of audio files does the Agora RTC SDK support`. + * method, see [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). * @param sound2 The absolute path or URL address (including the filename extensions) of the file * for the upbeats. For example, `C:\music\audio.mp4`. For the audio file formats supported by this - * method, see `What formats of audio files does the Agora RTC SDK support`. + * method, see [What audio file formats does the Agora RTC SDK support](https://docs.agora.io/en/help/general-product-inquiry/audio_format). * @param config The metronome configuration. See `AgoraRhythmPlayerConfig`. * * @return @@ -11913,7 +11924,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Disables the virtual metronome. - * + * @deprecated v4.6.0 * @details * After calling `startRhythmPlayer`, you can call this method to disable the virtual metronome. * @@ -11927,7 +11938,7 @@ class IRtcEngine : public agora::base::IEngineBase { /** * @brief Configures the virtual metronome. - * + * @deprecated v4.6.0 * @details * - After calling `startRhythmPlayer`, you can call this method to reconfigure the virtual * metronome. @@ -12125,13 +12136,13 @@ class IRtcEngine : public agora::base::IEngineBase { * Call timing: This method must be called before joining a channel. * * @note This method takes effect only after deploying the Agora Hybrid Cloud solution. You can - * `contact sales` to learn more and deploy the Agora Hybrid Cloud. + * contact sales to learn more and deploy the Agora Hybrid Cloud. * * @param config Local Access Point configuration. See `LocalAccessPointConfiguration`. * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int setLocalAccessPoint(const LocalAccessPointConfiguration& config) = 0; @@ -12146,6 +12157,7 @@ class IRtcEngine : public agora::base::IEngineBase { * uid, const ChannelMediaOptions& options)`, `enableAudio` and `enableLocalAudio`. * * @param options The advanced options for audio. See `AdvancedAudioOptions`. + * @param sourceType The type of the audio source. * * @return * - 0: Success. @@ -12171,7 +12183,7 @@ class IRtcEngine : public agora::base::IEngineBase { * * @return * - 0: Success. - * - < 0: Failure. See `Error Codes` for details and resolution suggestions. + * - < 0: Failure. */ virtual int setAVSyncSource(const char* channelId, uid_t uid) = 0; diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngineEx.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngineEx.h index f17f3f5b3..8611f7366 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngineEx.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraRtcEngineEx.h @@ -32,6 +32,9 @@ struct RtcConnection { : channelId(channel_id), localUid(local_uid) {} }; +/** + * @brief The IRtcEngineEventHandlerEx class. + */ class IRtcEngineEventHandlerEx : public IRtcEngineEventHandler { public: using IRtcEngineEventHandler::eventHandlerType; @@ -1135,7 +1138,7 @@ class IRtcEngineEx : public IRtcEngine { * optional. You will automatically exit the channel 24 hours after successfully joining in. * - If you need to join different channels at the same time or switch between channels, Agora * recommends using a wildcard token so that you don't need to apply for a new token every time - * joining a channel. See `Secure authentication with tokens`. + * joining a channel. * @param connection The connection information. See `RtcConnection`. * @param options The channel media options. See `ChannelMediaOptions`. * @param eventHandler The callback class of `IRtcEngineEx`. See `IRtcEngineEventHandler`. You can @@ -1320,7 +1323,6 @@ class IRtcEngineEx : public IRtcEngine { * @details * Sets the encoder configuration for the local video. Each configuration profile corresponds to a * set of video parameters, including the resolution, frame rate, and bitrate. - * Call timing: Call this method after `joinChannelEx`. * * @note The `config` specified in this method is the maximum value under ideal network conditions. * If the video engine cannot render the video using the specified `config` due to unreliable @@ -1353,8 +1355,7 @@ class IRtcEngineEx : public IRtcEngine { * Once the remote user leaves the channel, the SDK unbinds the remote user. * * @note - * - Call this method after `joinChannelEx`. - * - To update the rendering or mirror mode of the remote video view during a call, use the + * To update the rendering or mirror mode of the remote video view during a call, use the * `setRemoteRenderModeEx` method. * * @param canvas The remote video view settings. See `VideoCanvas`. @@ -1722,6 +1723,27 @@ class IRtcEngineEx : public IRtcEngine { */ virtual int setRemoteRenderModeEx(uid_t uid, media::base::RENDER_MODE_TYPE renderMode, VIDEO_MIRROR_MODE_TYPE mirrorMode, const RtcConnection& connection) = 0; + + /** + * Sets the rotation of remote render. + * + * @since v4.6.4 + * + * @note + * For example, the sending end is a screen sharing scene. + * When the screen of the sending end changes horizontally or vertically, + * the rendered screen of the receiving end also wants to change synchronously with that of the peer end. + * In this case, you can set the angle of the receiving end. + * + * @param uid The user ID of the remote user. + * @param rotation The clockwise rotation angle. See #VIDEO_ORIENTATION. + * @param connection The RTC connection. See #RtcConnection. + * @return + * - 0: Success. + * - < 0: Failure. + */ + virtual int setRemoteRenderRotationEx(uid_t uid, VIDEO_ORIENTATION rotation, const RtcConnection& connection) = 0; + /** * @brief Enables loopback audio capturing. * @@ -1891,7 +1913,7 @@ class IRtcEngineEx : public IRtcEngine { * stream will be destroyed when leaving the channel, and the data stream needs to be recreated if * needed. * If you need a more comprehensive solution for low-latency, high-concurrency, and scalable - * real-time messaging and status synchronization, it is recommended to use `Signaling`. + * real-time messaging and status synchronization, it is recommended to use [Signaling](https://docs.agora.io/en/signaling/overview/product-overview). * * @param streamId An output parameter; the ID of the data stream created. * @param config The configurations for the data stream. See `DataStreamConfig`. @@ -1920,7 +1942,7 @@ class IRtcEngineEx : public IRtcEngine { * * @note * - If you need a more comprehensive solution for low-latency, high-concurrency, and scalable - * real-time messaging and status synchronization, it is recommended to use `Signaling`. + * real-time messaging and status synchronization, it is recommended to use [Signaling](https://docs.agora.io/en/signaling/overview/product-overview). * - Call this method after `joinChannelEx`. * - Ensure that you call `createDataStreamEx(int* streamId, const DataStreamConfig& config, const * RtcConnection& connection)` to create a data channel before calling this @@ -2067,7 +2089,7 @@ class IRtcEngineEx : public IRtcEngine { * Agora supports reporting and analyzing customized messages. This function is in the beta stage * with a free trial. The ability provided in its beta test version is reporting a maximum of 10 * message pieces within 6 seconds, with each message piece not exceeding 256 bytes and each string - * not exceeding 100 bytes. To try out this function, contact `support@agora.io` and discuss the + * not exceeding 100 bytes. To try out this function, contact sales and discuss the * format of customized messages with us. * */ @@ -2113,8 +2135,8 @@ class IRtcEngineEx : public IRtcEngine { * @brief Starts pushing media streams to a CDN without transcoding. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Pushing streams using RESTful + * APIs](https://docs.agora.io/en/media-push/develop/restful-api). * You can call this method to push an audio or video stream to the specified CDN address. This * method can push media streams to only one CDN address at a time, so if you need to push streams * to multiple addresses, call this method multiple times. @@ -2146,8 +2168,8 @@ class IRtcEngineEx : public IRtcEngine { * @brief Starts Media Push and sets the transcoding configuration. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Pushing streams using RESTful + * APIs](https://docs.agora.io/en/media-push/develop/restful-api). * You can call this method to push a live audio-and-video stream to the specified CDN address and * set the transcoding configuration. This method can push media streams to only one CDN address at * a time, so if you need to push streams to multiple addresses, call this method multiple times. @@ -2200,8 +2222,8 @@ class IRtcEngineEx : public IRtcEngine { * @brief Stops pushing media streams to a CDN. * * @details - * Agora recommends that you use the server-side Media Push function. For details, see `Use RESTful - * API`. + * Agora recommends that you use the server-side Media Push function. For details, see [Pushing streams using RESTful + * APIs](https://docs.agora.io/en/media-push/develop/restful-api). * You can call this method to stop the live stream on the specified CDN address. This method can * stop pushing media streams to only one CDN address at a time, so if you need to stop pushing * streams to multiple addresses, call this method multiple times. @@ -2317,7 +2339,8 @@ class IRtcEngineEx : public IRtcEngine { */ virtual int resumeAllChannelMediaRelayEx(const RtcConnection& connection) = 0; - /** Gets the user information by passing in the user account. + /** + * @brief Gets the user information by passing in the user account. * It is same as agora::rtc::IRtcEngine::getUserInfoByUserAccount. * * @param userAccount The user account of the user. Ensure that you set this parameter. @@ -2332,7 +2355,8 @@ class IRtcEngineEx : public IRtcEngine { */ virtual int getUserInfoByUserAccountEx(const char* userAccount, rtc::UserInfo* userInfo, const RtcConnection& connection) = 0; - /** Gets the user information by passing in the user ID. + /** + * @brief Gets the user information by passing in the user ID. * It is same as agora::rtc::IRtcEngine::getUserInfoByUid. * * @param uid The user ID of the remote user. Ensure that you set this parameter. @@ -2350,7 +2374,7 @@ class IRtcEngineEx : public IRtcEngine { /** * @brief Enables or disables dual-stream mode on the sender side. * - * @deprecated v4.2.0. This method is deprecated. Use setDualStreamModeEx instead + * @deprecated v4.2.0. This method is deprecated. Use setDualStreamModeEx instead. * * @details * You can call this method to enable or disable the dual-stream mode on the publisher side. Dual @@ -2419,7 +2443,7 @@ class IRtcEngineEx : public IRtcEngine { /** * @brief Sets the simulcast video stream configuration. * - * @since v4.6.0 + * @technical preview * * @details * This method can be called in scenarios involving multiple channels. You can call the diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraSpatialAudio.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraSpatialAudio.h index b796d3210..8ff20e7f6 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraSpatialAudio.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/IAgoraSpatialAudio.h @@ -101,7 +101,8 @@ struct LocalSpatialAudioConfig { LocalSpatialAudioConfig() : rtcEngine(NULL) {} }; -/** The IBaseSpatialAudioEngine class provides the common methods of ICloudSpatialAudioEngine and ILocalSpatialAudioEngine. +/** + * @brief The base spatial audio engine interface. */ class ILocalSpatialAudioEngine: public RefCountInterface { protected: @@ -109,13 +110,13 @@ class ILocalSpatialAudioEngine: public RefCountInterface { public: /** - * @brief Destroys `IBaseSpatialAudioEngine`. + * @brief Destroys `ILocalSpatialAudioEngine`. * * @details - * This method releases all resources under `IBaseSpatialAudioEngine`. When the user does not need + * This method releases all resources under `ILocalSpatialAudioEngine`. When the user does not need * to use the spatial audio effect, you can call this method to release resources for other * operations. - * After calling this method, you can no longer use any of the APIs under `IBaseSpatialAudioEngine`. + * After calling this method, you can no longer use any of the APIs under `ILocalSpatialAudioEngine`. * * @note Call this method before the `release` method under `IRtcEngine`. * @@ -336,9 +337,8 @@ class ILocalSpatialAudioEngine: public RefCountInterface { * - Call this method after the `joinChannel(const char* token, const char* channelId, const char* * info, uid_t uid)` or `joinChannel(const char* token, const char* channelId, uid_t uid, const * ChannelMediaOptions& options)` method. - * - When using the spatial audio effect, if you need to set whether to stop subscribing to the - * audio stream of a specified user, Agora recommends calling this method instead of the - * `muteLocalAudioStream` method in `IRtcEngine`. + * - When using the spatial audio effect, if you need to set whether to stop publishing the local audio stream, Agora recommends calling this method instead of the + * one in `IRtcEngine`. * - A successful call of this method triggers the `onUserMuteAudio` and `onRemoteAudioStateChanged` * callbacks on the remote client. * @@ -364,7 +364,7 @@ class ILocalSpatialAudioEngine: public RefCountInterface { * ChannelMediaOptions& options)` method. * - When using the spatial audio effect, if you need to set whether to stop subscribing to the * audio streams of all remote users, Agora recommends calling this method instead of the - * `muteAllRemoteAudioStreams` method in `IRtcEngine`. + * one in `IRtcEngine`. * - After calling this method, you need to call `updateSelfPosition` and `updateRemotePosition` to * update the spatial location of the local user and the remote user; otherwise, the settings in * this method do not take effect. diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/c/c_player.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/c/c_player.h index 376787ab6..cb4f5418a 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/c/c_player.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/c/c_player.h @@ -368,6 +368,38 @@ typedef enum RtePlayerMetadataType { kRtePlayerMetadataTypeSei } RtePlayerMetadataType; +/** + * @brief Audio volume information for a stream. + * + * @since v4.5.3 + * + * @details This object contains the audio volume information for a stream, including the channel + * it belongs to, the stream id (which corresponds to the RTC uid), and the current volume level. + */ +typedef struct RteStreamAudioVolumeInfo { + /** The RTC channel name. */ + char* channel_name; + /** The stream identifier. */ + char* stream_id; + /** The audio volume. The value range is [0, 255]. */ + int32_t volume; +} RteStreamAudioVolumeInfo; + +/** + * @brief Stream data of the Player. + * + * @since v4.5.3 + * + * @details This object contains the stream data of the Player, including the volume information of + * each audio stream before audio mixing. + */ +typedef struct RtePlayerStreamData { + /** Array of per-stream audio volume entries before audio mixing. */ + RteStreamAudioVolumeInfo* audio_volumes; + /** Number of elements in audio_volumes. */ + int32_t audio_volumes_cnt; +} RtePlayerStreamData; + typedef enum RteAudioDualMonoMode { RteAudioDualMonoStereo, RteAudioDualMonoLeft, @@ -465,13 +497,43 @@ struct RtePlayerObserver { void (*on_audio_volume_indication)(RtePlayerObserver *observer, int32_t volume); + + /** + * @brief Stream data callback. + * + * @since v4.5.3 + * + * @details This callback reports the stream data of the Player. The current RtePlayerStreamData + * contains the volume information of each audio stream before audio mixing. + * + * @example If you use the Agora cloud transcoding service for audio mixing and use the Player to + * play the mixed audio RTE URL, you can use this callback to get the volume information of each + * audio stream before mixing. + * + * @param observer The player observer. + * @param stream_data The received stream data. + * @return void + */ + void (*on_stream_data_received)(RtePlayerObserver *observer, + RtePlayerStreamData *stream_data); }; AGORA_RTE_API_C void RtePlayerInfoInit(RtePlayerInfo *info, RteError *err); AGORA_RTE_API_C void RtePlayerInfoCopy(RtePlayerInfo *dest, const RtePlayerInfo *src, RteError *err); AGORA_RTE_API_C void RtePlayerInfoDeinit(RtePlayerInfo *info, RteError *err); +AGORA_RTE_API_C void RteStreamAudioVolumeInfoInit(RteStreamAudioVolumeInfo *info, RteError *err); +AGORA_RTE_API_C void RteStreamAudioVolumeInfoDeinit(RteStreamAudioVolumeInfo *info, RteError *err); +AGORA_RTE_API_C void RteStreamAudioVolumeInfoCopy(RteStreamAudioVolumeInfo *dest, const RteStreamAudioVolumeInfo *src, RteError *err); +AGORA_RTE_API_C void RteStreamAudioVolumeInfoSetChannelName(RteStreamAudioVolumeInfo *info, const char *channel_name, RteError *err); +AGORA_RTE_API_C void RteStreamAudioVolumeInfoSetStreamId(RteStreamAudioVolumeInfo *info, const char *stream_id, RteError *err); + +AGORA_RTE_API_C void RtePlayerStreamDataInit(RtePlayerStreamData *data, RteError *err); +AGORA_RTE_API_C void RtePlayerStreamDataDeinit(RtePlayerStreamData *data, RteError *err); +AGORA_RTE_API_C void RtePlayerStreamDataCopy(RtePlayerStreamData *dest, const RtePlayerStreamData *src, RteError *err); + AGORA_RTE_API_C void RtePlayerStatsInit(RtePlayerStats *stats, RteError *err); +AGORA_RTE_API_C void RtePlayerStatsCopy(RtePlayerStats *dest, const RtePlayerStats *src, RteError *err); AGORA_RTE_API_C void RtePlayerStatsDeinit(RtePlayerStats *stats, RteError *err); AGORA_RTE_API_C void RteMediaTrackInfoInit(RteMediaTrackInfo *info, diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_player.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_player.h index 5a82845ea..920856e5d 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_player.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_player.h @@ -7,6 +7,7 @@ #pragma once #include #include +#include #include "rte_base/c/c_rte.h" #include "rte_base/c/c_player.h" @@ -277,6 +278,164 @@ class PlayerInfo { ::RtePlayerInfo c_player_info; }; + + +/** + * @brief Audio volume information for a stream. + * + * @since v4.5.3 + * + * @details This object contains the audio volume information for a stream, including the channel + * it belongs to, the stream id (which corresponds to the RTC uid), and the current volume level. + */ +class StreamAudioVolumeInfo { + public: + StreamAudioVolumeInfo(){ RteStreamAudioVolumeInfoInit(&c_stream_audio_volume_info, nullptr); } + ~StreamAudioVolumeInfo(){ RteStreamAudioVolumeInfoDeinit(&c_stream_audio_volume_info, nullptr); } + + explicit StreamAudioVolumeInfo(const RteStreamAudioVolumeInfo* other) { + if (other != nullptr) { + RteStreamAudioVolumeInfoInit(&c_stream_audio_volume_info, nullptr); + RteStreamAudioVolumeInfoCopy(&c_stream_audio_volume_info, other, nullptr); + } + } + + StreamAudioVolumeInfo(const StreamAudioVolumeInfo& other) { + RteStreamAudioVolumeInfoInit(&c_stream_audio_volume_info, nullptr); + RteStreamAudioVolumeInfoCopy(&c_stream_audio_volume_info, &other.c_stream_audio_volume_info, nullptr); + } + + StreamAudioVolumeInfo(StreamAudioVolumeInfo&& other) { + c_stream_audio_volume_info = other.c_stream_audio_volume_info; + other.c_stream_audio_volume_info = {}; + } + + + StreamAudioVolumeInfo& operator=(const StreamAudioVolumeInfo& other) { + RteStreamAudioVolumeInfoCopy(&c_stream_audio_volume_info, &other.c_stream_audio_volume_info, nullptr); + return *this; + } + + StreamAudioVolumeInfo& operator=(StreamAudioVolumeInfo&& other) { + c_stream_audio_volume_info = other.c_stream_audio_volume_info; + other.c_stream_audio_volume_info = {}; + return *this; + } + + StreamAudioVolumeInfo& operator=(const RteStreamAudioVolumeInfo* other) { + RteStreamAudioVolumeInfoCopy(&c_stream_audio_volume_info, other, nullptr); + return *this; + } + + /** + * @brief Gets the RTC channel name for this stream. + * + * @since v4.5.3 + * + * @return std::string The channel name. + */ + std::string ChannelName() const { + return c_stream_audio_volume_info.channel_name != nullptr + ? std::string(c_stream_audio_volume_info.channel_name) + : std::string(); + } + + /** + * @brief Gets the stream identifier. + * + * @since v4.5.3 + * + * @return std::string The stream id. + */ + std::string StreamId() const { + return c_stream_audio_volume_info.stream_id != nullptr + ? std::string(c_stream_audio_volume_info.stream_id) + : std::string(); + } + + /** + * @brief Gets the audio volume level for this stream. + * + * @since v4.5.3 + * + * @return int32_t The volume. The value range is [0, 255]. + */ + int32_t Volume() const { return c_stream_audio_volume_info.volume; } + + const RteStreamAudioVolumeInfo* get_underlying_impl() const { return &c_stream_audio_volume_info; } + + protected: + + RteStreamAudioVolumeInfo c_stream_audio_volume_info = {}; +}; + +/** + * @brief Stream data of the Player. + * + * @since v4.5.3 + * + * @details This object contains the stream data of the Player, including the volume information of + * each audio stream before audio mixing. + */ +class PlayerStreamData { + public: + PlayerStreamData(){ RtePlayerStreamDataInit(&c_stream_data_, nullptr); } + ~PlayerStreamData(){ RtePlayerStreamDataDeinit(&c_stream_data_, nullptr); } + + explicit PlayerStreamData(const RtePlayerStreamData* other) { + if (other != nullptr) { + RtePlayerStreamDataInit(&c_stream_data_, nullptr); + RtePlayerStreamDataCopy(&c_stream_data_, other, nullptr); + } + } + + PlayerStreamData(const PlayerStreamData& other) { + RtePlayerStreamDataInit(&c_stream_data_, nullptr); + RtePlayerStreamDataCopy(&c_stream_data_, &other.c_stream_data_, nullptr); + } + + PlayerStreamData& operator=(const PlayerStreamData& other) { + RtePlayerStreamDataCopy(&c_stream_data_, &other.c_stream_data_, nullptr); + return *this; + } + + PlayerStreamData& operator=(PlayerStreamData&& other) { + c_stream_data_ = other.c_stream_data_; + other.c_stream_data_ = {}; + return *this; + } + + PlayerStreamData& operator=(const RtePlayerStreamData* other) { + RtePlayerStreamDataCopy(&c_stream_data_, other, nullptr); + return *this; + } + + /** + * @brief Gets the list of per-stream audio volume entries in this stream data. + * + * @since v4.5.3 + * + * @return std::vector The audio volume list. May be empty if no data is available. + */ + std::vector AudioVolumes() const { + std::vector audio_volumes; + + if (c_stream_data_.audio_volumes_cnt > 0 && c_stream_data_.audio_volumes != nullptr) { + audio_volumes.reserve(c_stream_data_.audio_volumes_cnt); + for (int32_t i = 0; i < c_stream_data_.audio_volumes_cnt; ++i) { + audio_volumes.emplace_back(&c_stream_data_.audio_volumes[i]); + } + } + + return audio_volumes; + } + + RtePlayerStreamData* get_underlying_impl() { return &c_stream_data_; } + + protected: + RtePlayerStreamData c_stream_data_ = {}; +}; + static void onStateChanged(::RtePlayerObserver *observer, RtePlayerState old_state, RtePlayerState new_state, RteError *err); @@ -295,6 +454,8 @@ static void onPlayerInfoUpdated(::RtePlayerObserver *observer, const RtePlayerIn static void onAudioVolumeIndication(::RtePlayerObserver *observer, int32_t volume); +static void onStreamDataReceived(::RtePlayerObserver *observer, RtePlayerStreamData *stream_data); + /** * @brief The PlayerObserver class is used to observe the event of Player object. @@ -313,6 +474,7 @@ class PlayerObserver { c_player_observer->on_metadata = rte::onMetadata; c_player_observer->on_player_info_updated = rte::onPlayerInfoUpdated; c_player_observer->on_audio_volume_indication = rte::onAudioVolumeIndication; + c_player_observer->on_stream_data_received = rte::onStreamDataReceived; } virtual ~PlayerObserver(){ RtePlayerObserverDestroy(c_player_observer, nullptr); } @@ -439,6 +601,23 @@ class PlayerObserver { */ virtual void onAudioVolumeIndication(int32_t volume) {}; + /** + * @brief Stream data callback. + * + * @since v4.5.3 + * + * @details This callback reports the stream data of the Player. The current PlayerStreamData + * object contains the volume information of each audio stream before audio mixing. + * + * @example If you use the Agora cloud transcoding service for audio mixing and use Player to + * play the mixed audio RTE URL, you can use this callback to get the volume information of each + * audio stream before mixing. + * + * @param stream_data The received stream data. + * @return void + */ + virtual void onStreamDataReceived(const PlayerStreamData *stream_data) {}; + private: friend class Player; @@ -499,6 +678,14 @@ void onAudioVolumeIndication(::RtePlayerObserver *observer, int32_t volume){ } } +void onStreamDataReceived(::RtePlayerObserver *observer, RtePlayerStreamData *stream_data) { + auto *player_observer = static_cast(observer->base_observer.me_in_target_lang); + if (player_observer != nullptr) { + PlayerStreamData cpp_data(stream_data); + player_observer->onStreamDataReceived(&cpp_data); + } +} + /** * @brief This class provides methods for configuring the player. * @@ -966,8 +1153,7 @@ class PlayerConfig { * If you have higher requirements for video resolution switching, you can `contact technical * support` to enable * the ABR (Adaptive Bitrate) feature. Once enabled, you can customize the resolution for each video - * quality layer, and the audience can switch between all video quality layers as needed. For - * detailed implementation, see `Audience-side URL streaming`. + * quality layer, and the audience can switch between all video quality layers as needed. * Applicable scenarios: In unstable network environments, the audience can choose an appropriate * video quality level based on actual network conditions to ensure a smooth viewing experience. * Call timing: This method must be called before `SetConfigs(PlayerConfig* config, Error* err)`. diff --git a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_rte.h b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_rte.h index 059b9a45a..5f1e18daf 100644 --- a/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_rte.h +++ b/Android/APIExample/agora-stream-encrypt/src/main/cpp/include/agora/rte_base/rte_cpp_rte.h @@ -261,7 +261,7 @@ class Config { }; /** - * The Rte class, which is the base interface of the Agora Real Time Engagement SDK. + * @brief The Rte class, which is the base interface of the Agora Real Time Engagement SDK. * @since v4.4.0 */ class Rte { diff --git a/Android/APIExample/app/.gitignore b/Android/APIExample/app/.gitignore index 6de48b91b..3b466dd9a 100644 --- a/Android/APIExample/app/.gitignore +++ b/Android/APIExample/app/.gitignore @@ -20,11 +20,9 @@ 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_sensetime/* !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/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/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/common/BaseVbFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseVbFragment.java index cd0f6289b..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. @@ -77,7 +77,7 @@ protected RtcEngine initRtcEngine(IRtcEngineEventHandler engineEventHandler) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = engineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* 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 0439938a4..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, @@ -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; } @@ -575,7 +575,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long if (!shapeBeauty.isChecked()) { return; } - AgoraBeautySDK.getBeautyConfig().setBeautyShapeStyle(spinnerShapeBeautifyStyle.getSelectedItem().toString()); + AgoraBeautySDK.getBeautyConfig().setBeautyShapeStyle(getMaterialKey(spinnerShapeBeautifyStyle, R.array.agora_beauty_style_keys)); sbShapeBeautifyStyleIntensity.setProgress(AgoraBeautySDK.getBeautyConfig().getBeautyShapeStrength()); updateBasicBeautyOption(); checkEnable(); @@ -629,7 +629,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long if (!makeUp.isChecked()) { return; } - AgoraBeautySDK.getBeautyConfig().setBeautyMakeupStyle(spinnerFaceMakeupStyle.getSelectedItem().toString()); + AgoraBeautySDK.getBeautyConfig().setBeautyMakeupStyle(getMaterialKey(spinnerFaceMakeupStyle, R.array.agora_makeup_style_keys)); sbFaceMakeupStyleIntensity.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBeautyMakeupStrength() * 10)); updateMakeupOptionsByStyle(); checkEnable(); @@ -638,7 +638,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long if (!filter.isChecked()) { return; } - AgoraBeautySDK.getBeautyConfig().setBeautyFilter(spinnerFilterStyle.getSelectedItem().toString()); + AgoraBeautySDK.getBeautyConfig().setBeautyFilter(getMaterialKey(spinnerFilterStyle, R.array.agora_filter_style_keys)); sbFilterStyleIntensity.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getFilterStrength() * 10)); checkEnable(); return; @@ -762,6 +762,12 @@ public void onItemSelected(AdapterView parent, View view, int position, long } } + private String getMaterialKey(Spinner spinner, int keyArrayRes) { + String[] keys = getResources().getStringArray(keyArrayRes); + int position = spinner.getSelectedItemPosition(); + return position >= 0 && position < keys.length ? keys[position] : null; + } + @Override public void onNothingSelected(AdapterView parent) { @@ -825,7 +831,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { return; } if (isChecked) { - AgoraBeautySDK.getBeautyConfig().setBeautyShapeStyle(spinnerShapeBeautifyStyle.getSelectedItem().toString()); + AgoraBeautySDK.getBeautyConfig().setBeautyShapeStyle(getMaterialKey(spinnerShapeBeautifyStyle, R.array.agora_beauty_style_keys)); sbShapeBeautifyStyleIntensity.setProgress(AgoraBeautySDK.getBeautyConfig().getBeautyShapeStrength()); updateBasicBeautyOption(); checkEnable(); @@ -840,7 +846,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { return; } if (isChecked) { - AgoraBeautySDK.getBeautyConfig().setBeautyMakeupStyle(spinnerFaceMakeupStyle.getSelectedItem().toString()); + AgoraBeautySDK.getBeautyConfig().setBeautyMakeupStyle(getMaterialKey(spinnerFaceMakeupStyle, R.array.agora_makeup_style_keys)); sbFaceMakeupStyleIntensity.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getBeautyMakeupStrength() * 10)); updateMakeupOptionsByStyle(); checkEnable(); @@ -874,7 +880,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { return; } if (isChecked) { - AgoraBeautySDK.getBeautyConfig().setBeautyFilter(spinnerFilterStyle.getSelectedItem().toString()); + AgoraBeautySDK.getBeautyConfig().setBeautyFilter(getMaterialKey(spinnerFilterStyle, R.array.agora_filter_style_keys)); sbFilterStyleIntensity.setProgress((int) (AgoraBeautySDK.getBeautyConfig().getFilterStrength() * 10)); checkEnable(); } else { @@ -1047,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 11a39da3e..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, @@ -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. @@ -117,7 +117,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 0b28ca513..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. @@ -112,7 +112,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 8a078f402..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, @@ -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. @@ -148,7 +148,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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/FaceCapture.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/FaceCapture.java deleted file mode 100644 index ea3079d42..000000000 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/FaceCapture.java +++ /dev/null @@ -1,459 +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.MediaSourceType.PRIMARY_CAMERA_SOURCE; -import static io.agora.rtc2.video.VideoCanvas.RENDER_MODE_HIDDEN; -import static io.agora.rtc2.video.VideoEncoderConfiguration.STANDARD_BITRATE; - -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.EditText; -import android.widget.FrameLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Locale; - -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.utils.CommonUtil; -import io.agora.api.example.utils.TokenUtils; -import io.agora.base.VideoFrame; -import io.agora.rtc2.ChannelMediaOptions; -import io.agora.rtc2.Constants; -import io.agora.rtc2.ExtensionContext; -import io.agora.rtc2.IMediaExtensionObserver; -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.IFaceInfoObserver; -import io.agora.rtc2.video.IVideoFrameObserver; -import io.agora.rtc2.video.VideoCanvas; -import io.agora.rtc2.video.VideoEncoderConfiguration; - -/** - * The type Process raw data. - */ -@Example( - index = 23, - group = ADVANCED, - name = R.string.item_face_capture, - actionId = R.id.action_mainFragment_to_face_capture, - tipsId = R.string.face_capture_tip -) -public class FaceCapture extends BaseFragment implements View.OnClickListener { - private static final String TAG = FaceCapture.class.getSimpleName(); - - private static final String AUTHENTICATION = "i1RXlMcDyB5F6Yn9aPijy1MouDxdsI4ajOj+NrHsc99FntK1drIlJNWdodNzBjAeH8cSUSuRkX6LwoE0WDgEAnivcXbsYv8atcZYZFl4FoXONgI6uIOmi+r5SGjsgZe+yI6wdh5eW1pLLh6m/W02M35EX1wtBvP3l79q3KeGsDQ="; - - private FrameLayout fl_local; - private Button join; - private EditText et_channel, et_capture_info; - private RtcEngine engine; - private int myUid; - private boolean joined = false; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - try { - RtcEngineConfig config = new RtcEngineConfig(); - /* - * The context of Android Activity - */ - config.mContext = context.getApplicationContext(); - /* - * The App ID issued to you by Agora. See How to get the App ID - */ - config.mAppId = getAgoraAppId(); - /* The channel profile. - CHANNEL_PROFILE_COMMUNICATION(0): Communication. Agora recommends using the live streaming profile - for a better audio and video experience. - CHANNEL_PROFILE_LIVE_BROADCASTING(1): (Default) Live streaming. - CHANNEL_PROFILE_GAME(2): Gaming. - Deprecated: Use CHANNEL_PROFILE_LIVE_BROADCASTING instead. - CHANNEL_PROFILE_CLOUD_GAMING(3): Interaction. The scenario is optimized for latency. Use this - profile if the use case requires frequent interactions between users. - Deprecated: Use CHANNEL_PROFILE_LIVE_BROADCASTING instead.*/ - config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - /* - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); - config.mAreaCode = ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getAreaCode(); - /* For Android, the agora_face_capture_extension will not load default. You must add it manually. */ - config.addExtension("agora_face_capture_extension"); - config.addExtension("agora_lip_sync_extension"); - /* Config extension observer to receive the events. */ - config.mExtensionObserver = iMediaExtensionObserver; - engine = RtcEngine.create(config); - /* - * This parameter is for reporting the usages of APIExample to agora background. - * Generally, it is not necessary for you to set this parameter. - */ - engine.setParameters("{" - + "\"rtc.report_app_scenario\":" - + "{" - + "\"appScenario\":" + 100 + "," - + "\"serviceType\":" + 11 + "," - + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"" - + "}" - + "}"); - /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/ - LocalAccessPointConfiguration localAccessPointConfiguration = ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig(); - if (localAccessPointConfiguration != null) { - // This api can only be used in the private media server scenario, otherwise some problems may occur. - engine.setLocalAccessPoint(localAccessPointConfiguration); - } - - engine.registerVideoFrameObserver(iVideoFrameObserver); - engine.registerFaceInfoObserver(iFaceInfoObserver); - engine.enableExtension("agora_video_filters_face_capture", "face_capture", true, PRIMARY_CAMERA_SOURCE); - engine.setExtensionProperty("agora_video_filters_face_capture", - "face_capture", - "authentication_information", - "{\"company_id\":\"agoraTest\"," - + "\"license\":\"" - + AUTHENTICATION - + "\"}" - ); - - engine.enableExtension("agora_filters_lip_sync", "lip_sync", true, Constants.MediaSourceType.SPEECH_DRIVEN_VIDEO_SOURCE); - engine.setExtensionProperty("agora_filters_lip_sync","lip_sync", "parameters", - "{\"company_id\":\"agoraTest\"" - + ",\"license\":\"" + AUTHENTICATION + "\"" - + ",\"open_agc\":true" - + "}", - Constants.MediaSourceType.SPEECH_DRIVEN_VIDEO_SOURCE); - - } catch (Exception e) { - e.printStackTrace(); - requireActivity().onBackPressed(); - } - } - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_face_capture, 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); - fl_local = view.findViewById(R.id.fl_local); - et_capture_info = view.findViewById(R.id.et_capture_info); - } - - - @Override - public void onDestroy() { - /*leaveChannel and Destroy the RtcEngine instance*/ - if (engine != null) { - engine.leaveChannel(); - engine.stopPreview(); - } - engine = null; - super.onDestroy(); - handler.post(RtcEngine::destroy); - } - - @Override - public void onClick(View v) { - if (v.getId() == R.id.btn_join) { - if (!joined) { - CommonUtil.hideInputBoard(requireActivity(), et_channel); - // call when join button hit - String channelId = et_channel.getText().toString(); - joinChannel(channelId); - } else { - joined = false; - /*After joining a channel, the user must call the leaveChannel method to end the - * call before joining another channel. This method returns 0 if the user leaves the - * channel and releases all resources related to the call. This method call is - * asynchronous, and the user has not exited the channel when the method call returns. - * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. - * A successful leaveChannel method call triggers the following callbacks: - * 1:The local client: onLeaveChannel. - * 2:The remote client: onUserOffline, if the user leaving the channel is in the - * Communication channel, or is a BROADCASTER in the Live Broadcast profile. - * @returns 0: Success. - * < 0: Failure. - * PS: - * 1:If you call the destroy method immediately after calling the leaveChannel - * method, the leaveChannel process interrupts, and the SDK does not trigger - * the onLeaveChannel callback. - * 2:If you call the leaveChannel method during CDN live streaming, the SDK - * triggers the removeInjectStreamUrl method.*/ - engine.leaveChannel(); - engine.stopPreview(); - join.setText(getString(R.string.join)); - } - } - } - - private void joinChannel(String channelId) { - // Check if the context is valid - Context context = getContext(); - if (context == null) { - return; - } - - // Create render view by RtcEngine - SurfaceView surfaceView = new SurfaceView(context); - // Add to the local container - fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - // Setup local video to render your local camera preview - engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); - - /*In the demo, the default is to enter as the anchor.*/ - engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); - // Setup video encoding configs - engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( - ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), - VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication) requireActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), - STANDARD_BITRATE, - VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication) requireActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) - )); - /*Set up to play remote sound with receiver*/ - engine.setDefaultAudioRoutetoSpeakerphone(true); - - engine.enableVideo(); - - engine.startPreview(); - - /* - * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see - * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token - * A token generated at the server. This applies to scenarios with high-security requirements. For details, see - * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ - TokenUtils.gen(requireContext(), channelId, 0, token -> { - /* Allows a user to join a channel. - if you do not specify the uid, we will generate the uid for you*/ - - ChannelMediaOptions option = new ChannelMediaOptions(); - option.autoSubscribeAudio = true; - option.autoSubscribeVideo = true; - option.publishCameraTrack = true; - int res = engine.joinChannel(token, channelId, 0, option); - if (res != 0) { - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html - showAlert(RtcEngine.getErrorDescription(Math.abs(res))); - return; - } - // Prevent repeated entry - join.setEnabled(false); - }); - } - - private final IVideoFrameObserver iVideoFrameObserver = new IVideoFrameObserver() { - @Override - public boolean onRenderVideoFrame(String channelId, int uid, VideoFrame videoFrame) { - return false; - } - - @Override - public boolean onCaptureVideoFrame(int sourceType, VideoFrame videoFrame) { - Log.i(TAG, String.format(Locale.US, "VideoFrameObserver >> onCaptureVideoFrame : metadata=%s", - videoFrame.getMetaInfo().toString())); - // runOnUIThread(() -> et_capture_info.setText(videoFrame.getMetaInfo().toString())); - return true; - } - - @Override - public boolean onPreEncodeVideoFrame(int sourceType, VideoFrame videoFrame) { - return false; - } - - @Override - public boolean onMediaPlayerVideoFrame(VideoFrame videoFrame, int mediaPlayerId) { - return false; - } - - @Override - public int getVideoFrameProcessMode() { - return PROCESS_MODE_READ_ONLY; - } - - @Override - public int getVideoFormatPreference() { - return VIDEO_PIXEL_DEFAULT; - } - - @Override - public boolean getRotationApplied() { - return false; - } - - @Override - public boolean getMirrorApplied() { - return false; - } - - @Override - public int getObservedFramePosition() { - return POSITION_POST_CAPTURER; - } - }; - - private final IMediaExtensionObserver iMediaExtensionObserver = new IMediaExtensionObserver() { - @Override - public void onEventWithContext(ExtensionContext extContext, String key, String value) { - String provider = extContext.providerName; - String extension = extContext.extensionName; - Log.i(TAG, String.format(Locale.US, "ExtensionObserver >> onEvent : provider=%s, extension=%s, key=%s, value=%s", - provider, extension, key, value)); - if ("agora_video_filters_face_capture".equals(provider) - && "face_capture".equals(extension) - && "authentication_state".equals(key)) { - if ("0".equals(value)) { - showShortToast("Face capture authentication successful."); - } else if ("-1".equals(value)) { - showShortToast("Face capture authentication failed!"); - } else if ("-2".equals(value)) { - showShortToast("Face capture authentication information not set!"); - showAlert(getString(R.string.face_capture_authentication), false); - } - } else if ("agora_filters_lip_sync".equals(provider) - && "lip_sync".equals(extension) - && "status_code".equals(key)) { - if ("0".equals(value)) { - showShortToast("Speech driven authentication successful."); - } else { - showShortToast("Speech driven authentication failed. code=" + value); - } - } - } - - @Override - public void onStartedWithContext(ExtensionContext extContext) { - String provider = extContext.providerName; - String extension = extContext.extensionName; - Log.i(TAG, String.format(Locale.US, "ExtensionObserver >> onStarted : provider=%s, extension=%s", - provider, extension)); - } - - @Override - public void onStoppedWithContext(ExtensionContext extContext) { - String provider = extContext.providerName; - String extension = extContext.extensionName; - Log.i(TAG, String.format(Locale.US, "ExtensionObserver >> onStopped : provider=%s, extension=%s", - provider, extension)); - } - - @Override - public void onErrorWithContext(ExtensionContext extContext, int error, String message) { - String provider = extContext.providerName; - String extension = extContext.extensionName; - Log.i(TAG, String.format(Locale.US, "ExtensionObserver >> onError : provider=%s, extension=%s, error=%d, message=%s", - provider, extension, error, message)); - } - }; - - private final IFaceInfoObserver iFaceInfoObserver = new IFaceInfoObserver() { - @Override - public boolean onFaceInfo(String outFaceInfo) { - runOnUIThread(() -> et_capture_info.setText(outFaceInfo)); - return false; - } - }; - - /** - * IRtcEngineEventHandler is an abstract class providing default implementation. - * The SDK uses this class to report to the app on SDK runtime events. - */ - private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { - /** - * Error code description can be found at: - * en: ... - * cn: ... - */ - @Override - public void onError(int err) { - Log.w(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); - } - - - /**Occurs when a user leaves the channel. - * @param stats With this callback, the application retrieves the channel information, - * such as the call duration and statistics.*/ - @Override - public void onLeaveChannel(RtcStats stats) { - super.onLeaveChannel(stats); - Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); - showLongToast(String.format(Locale.US, "local user %d leaveChannel!", myUid)); - } - - /**Occurs when the local user joins a specified channel. - * The channel name assignment is based on channelName specified in the joinChannel method. - * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. - * @param channel Channel name - * @param uid User ID - * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ - @Override - public void onJoinChannelSuccess(String channel, int uid, int elapsed) { - Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); - showLongToast(String.format(Locale.US, "onJoinChannelSuccess channel %s uid %d", channel, uid)); - myUid = uid; - joined = true; - handler.post(new Runnable() { - @Override - public void run() { - join.setEnabled(true); - join.setText(getString(R.string.leave)); - } - }); - } - - /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. - * @param uid ID of the user whose audio state changes. - * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole - * until this callback is triggered.*/ - @Override - public void onUserJoined(int uid, int elapsed) { - super.onUserJoined(uid, elapsed); - Log.i(TAG, "onUserJoined->" + uid); - showLongToast(String.format(Locale.US, "user %d joined!", uid)); - } - - /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. - * @param uid ID of the user whose audio state changes. - * @param reason Reason why the user goes offline: - * USER_OFFLINE_QUIT(0): The user left the current channel. - * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data - * packet was received within a certain period of time. If a user quits the - * call and the message is not passed to the SDK (due to an unreliable channel), - * the SDK assumes the user dropped offline. - * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from - * the host to the audience.*/ - @Override - public void onUserOffline(int uid, int reason) { - Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); - showLongToast(String.format(Locale.US, "user %d offline! reason:%d", uid, reason)); - } - }; - -} 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 ae144c10b..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. @@ -117,7 +117,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 db516640c..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, @@ -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. @@ -121,7 +121,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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/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/LiveStreaming.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java index e011b9e1c..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 @@ -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; /** @@ -303,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.*/ @@ -321,7 +323,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { profile if the use case requires frequent interactions between users. Deprecated: Use CHANNEL_PROFILE_LIVE_BROADCASTING instead.*/ rtcEngineConfig.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING; - rtcEngineConfig.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + rtcEngineConfig.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; rtcEngineConfig.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(rtcEngineConfig); /* @@ -344,7 +346,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { } engine.setVideoEncoderConfiguration(videoEncoderConfiguration); - engine.enableDualStreamMode(true); + engine.setDualStreamMode(Constants.SimulcastStreamMode.ENABLE_SIMULCAST_STREAM); agoraFocalLengthInfos = engine.queryCameraFocalLengthCapability(); ArrayList strings = new ArrayList<>(); @@ -565,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*/ @@ -587,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; } @@ -606,7 +608,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)); } @@ -689,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 c94e5a5fc..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, @@ -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. @@ -117,7 +117,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 5ad6461e5..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. @@ -131,7 +131,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 f144b11f5..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, @@ -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. @@ -134,7 +134,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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) { @@ -705,4 +705,4 @@ public void onPlayerPlaybackStats(PlayerPlaybackStats stats) { public void onAudioVolumeIndication(int i) { } -} \ No newline at end of file +} 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 cea6908f8..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, @@ -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. @@ -128,7 +128,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 bbdc7e91b..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, @@ -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. @@ -151,7 +151,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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 674cd0db9..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. @@ -133,7 +133,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 a3a7ab4d6..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(); @@ -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. @@ -127,7 +127,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 91b1e8258..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, @@ -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. @@ -179,7 +179,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -234,7 +234,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())); } } @@ -332,15 +332,15 @@ private void joinChannel(String channelId) { /*In the demo, the default is to enter as the anchor.*/ 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())) + Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString())) ); + engine.setAudioScenario(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 + * 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; @@ -362,6 +362,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. @@ -370,7 +381,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) { @@ -508,10 +519,6 @@ public void onAudioMixingStateChanged(int state, int errorCode) { showLongToast(String.format("onAudioMixingStateChanged %d error code:%d", state, errorCode)); } - @Override - public void onAudioMixingFinished() { - super.onAudioMixingFinished(); - } }; @Override 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 99636974f..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, @@ -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. @@ -98,7 +98,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime evepnts. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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..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, @@ -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 367bea13f..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, @@ -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. @@ -106,7 +106,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 fc1d882a6..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, @@ -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. @@ -152,7 +152,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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; } @@ -359,12 +359,12 @@ private void pushVideoFrameByI420(byte[] yuv, int width, int height) { /* * Pushes the external video frame to the app. */ - boolean success = engine.pushExternalVideoFrame(videoFrame); + int ret = engine.pushExternalVideoFrameById(videoFrame, 0); i420Buffer.release(); - if (!success) { - Log.w(TAG, "pushExternalVideoFrame error"); + if (ret != Constants.ERR_OK) { + Log.w(TAG, "pushExternalVideoFrameById error: " + ret); } } @@ -392,10 +392,10 @@ private void pushVideoFrameByNV21(byte[] nv21, int width, int height) { /* * Pushes the external video frame to the app. */ - boolean success = engine.pushExternalVideoFrame(videoFrame); + int ret = engine.pushExternalVideoFrameById(videoFrame, 0); - if (!success) { - Log.w(TAG, "pushExternalVideoFrame error"); + if (ret != Constants.ERR_OK) { + Log.w(TAG, "pushExternalVideoFrameById error: " + ret); } } @@ -422,10 +422,10 @@ private void pushVideoFrameByNV12(ByteBuffer nv12, int width, int height) { /* * Pushes the external video frame to the app. */ - boolean success = engine.pushExternalVideoFrame(videoFrame); + int ret = engine.pushExternalVideoFrameById(videoFrame, 0); - if (!success) { - Log.w(TAG, "pushExternalVideoFrame error"); + if (ret != Constants.ERR_OK) { + Log.w(TAG, "pushExternalVideoFrameById error: " + ret); } } @@ -464,10 +464,10 @@ private void pushVideoFrameByTexture(int textureId, VideoFrame.TextureBuffer.Typ /* * Pushes the external video frame to the app. */ - boolean success = engine.pushExternalVideoFrame(videoFrame); + int ret = engine.pushExternalVideoFrameById(videoFrame, 0); - if (!success) { - Log.w(TAG, "pushExternalVideoFrame error"); + if (ret != Constants.ERR_OK) { + Log.w(TAG, "pushExternalVideoFrameById error: " + ret); } } 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 a0fdad735..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. @@ -131,7 +131,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 21d53beff..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 @@ -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(); @@ -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. @@ -118,7 +118,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 c0a1bf15b..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, @@ -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. @@ -172,7 +172,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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) { @@ -516,6 +516,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 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 d68db990f..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. @@ -118,7 +118,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 071d4887f..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, @@ -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. */ @@ -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. @@ -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; @@ -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 394b7c3a6..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. @@ -137,7 +137,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 afb1b45a1..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. @@ -126,7 +126,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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/ThirdPartyBeauty.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ThirdPartyBeauty.java index 95de231a3..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 @@ -23,12 +23,13 @@ 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. */ @Example( - index = 39, + index = 40, group = ADVANCED, name = R.string.item_third_party_beauty, actionId = R.id.action_mainFragment_to_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/TransparentRendering.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/TransparentRendering.java index 55382e971..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, @@ -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. @@ -114,7 +114,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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; } @@ -295,7 +295,10 @@ private void startPlaying() { // kAlphaStitchRight = 4 //}; frame.setAlphaStitchMode(3); - engine.pushExternalVideoFrame(frame); + int ret = engine.pushExternalVideoFrameById(frame, 0); + if (ret != Constants.ERR_OK) { + Log.w(TAG, "pushExternalVideoFrameById error: " + ret); + } } }); @@ -325,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/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 8c2d4f05a..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, @@ -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 7cdb91c5c..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, @@ -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. @@ -119,7 +119,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 cb15af51e..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. @@ -265,7 +265,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -395,16 +395,17 @@ private void joinChannel(String channelId) { /*In the demo, the default is to enter as the anchor.*/ engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); // audio config - engine.setAudioProfile( - Constants.AudioProfile.getValue(Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString())), - Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString())) - ); + int selectedAudioProfile = Constants.AudioProfile.getValue( + Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString()) + ); + engine.setAudioProfile(selectedAudioProfile); + engine.setAudioScenario(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 + * 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 +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; @@ -434,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) { @@ -578,7 +579,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; } @@ -655,6 +656,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/advanced/beauty/AgoraBeautySDK.kt b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/AgoraBeautySDK.kt index 4a1443f53..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 @@ -12,8 +12,10 @@ import io.agora.rtc2.RtcEngine */ object AgoraBeautySDK { private const val TAG = "AgoraBeautySDK" + private const val FUNCTIONAL_MATERIAL_DIRECTORY = "beauty_material_functional" private var rtcEngine: RtcEngine? = null private var videoEffectObject: IVideoEffectObject? = null + private var beautyMaterialCopied = false // Beauty config @JvmStatic @@ -24,13 +26,32 @@ object AgoraBeautySDK { fun initBeautySDK(context: Context, rtcEngine: RtcEngine): Boolean { this.rtcEngine = rtcEngine rtcEngine.enableExtension("agora_video_filters_clear_vision", "clear_vision", true) - val storagePath = context.getExternalFilesDir("")?.absolutePath ?: return false - val modelsPath = "$storagePath/beauty_agora/beauty_material" - copyAssets(context, "beauty_agora/beauty_material", modelsPath) + val storagePath = context.getExternalFilesDir("")?.absolutePath + if (storagePath == null) { + rtcEngine.enableExtension("agora_video_filters_clear_vision", "clear_vision", false) + this.rtcEngine = null + return false + } + val materialDirectory = java.io.File(storagePath, "beauty_agora") + if (!beautyMaterialCopied) { + materialDirectory.deleteRecursively() + copyAssets(context, "beauty_agora", materialDirectory.path) + if (!java.io.File(materialDirectory, "$FUNCTIONAL_MATERIAL_DIRECTORY/config.json").isFile) { + rtcEngine.enableExtension("agora_video_filters_clear_vision", "clear_vision", false) + this.rtcEngine = null + return false + } + beautyMaterialCopied = true + } videoEffectObject = rtcEngine.createVideoEffectObject( - "$modelsPath/beauty_material_v2.0.0", + "${materialDirectory.path}/$FUNCTIONAL_MATERIAL_DIRECTORY", Constants.MediaSourceType.PRIMARY_CAMERA_SOURCE ) + if (videoEffectObject == null) { + rtcEngine.enableExtension("agora_video_filters_clear_vision", "clear_vision", false) + this.rtcEngine = null + return false + } // Fix lipstick ghosting issue rtcEngine.setParameters("{\"rtc.video.yuvconverter_enable_hardware_buffer\":true}") Log.d(TAG, "initBeautySDK called") 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/api/example/examples/advanced/beauty/utils/FileUtils.kt b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/utils/FileUtils.kt index 0da7f3323..c97d3c7e5 100644 --- a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/utils/FileUtils.kt +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/beauty/utils/FileUtils.kt @@ -110,4 +110,4 @@ object FileUtils { input?.close() } } -} \ No newline at end of file +} 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 e094b4de5..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. @@ -122,7 +122,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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 0788ad3bf..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. @@ -137,7 +137,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -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/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/AudioRouterPlayerExo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/audio/AudioRouterPlayerExo.java index 53619a417..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. @@ -84,7 +84,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getAreaCode(); mRtcEngine = RtcEngine.create(config); /* @@ -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 6215a3807..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. @@ -82,7 +82,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getAreaCode(); mRtcEngine = RtcEngine.create(config); /* @@ -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 11e4d1fbb..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. @@ -83,7 +83,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) requireActivity().getApplication()).getGlobalSettings().getAreaCode(); mRtcEngine = RtcEngine.create(config); /* @@ -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..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, @@ -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 0f0c56c41..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 @@ -156,7 +156,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); } } @@ -174,15 +174,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); @@ -298,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. @@ -316,7 +324,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString())); + config.mAudioScenario = getAudioScenarioValue(audioScenarioInput.getSelectedItem().toString()); config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = (RtcEngineEx) RtcEngine.create(config); /* @@ -549,7 +557,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(); @@ -558,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. @@ -569,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; @@ -581,6 +589,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"; @@ -666,10 +685,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. @@ -678,7 +711,7 @@ public void onAudioRouteChanged(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 1cb48d021..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. @@ -116,7 +116,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { * The SDK uses this class to report to the app on SDK runtime events. */ config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); /* @@ -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 214fc412d..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 @@ -100,7 +100,7 @@ private boolean createRtcEngine(String appId) { // RtcEngineEventHandler is an abstract class providing default implementation. // he SDK uses this class to report to the app on SDK runtime events. config.mEventHandler = iRtcEngineEventHandler; - config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT); + config.mAudioScenario = Constants.AUDIO_SCENARIO_DEFAULT; config.mAreaCode = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode(); engine = RtcEngine.create(config); // This parameter is for reporting the usages of APIExample to agora background. @@ -154,7 +154,7 @@ public void onClick(View v) { showLongToast(getString(R.string.app_id_empty)); return; } - if (createRtcEngine(appId)) { + if (engine != null || createRtcEngine(appId)) { joinChannel(channelId, token); } @@ -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,20 +236,38 @@ 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) { 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)); + runOnUIThread(() -> { + joined = false; + join.setEnabled(true); + join.setText(getString(R.string.join)); + for (ViewGroup value : remoteViews.values()) { + value.removeAllViews(); + } + remoteViews.clear(); + }); - 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)); } } 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 b88c54aa7..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 @@ -615,9 +615,11 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, IVideoFrameObserver { return@setFilter -1 } val ret = texBufferHelper.invoke { - synchronized(EglBase.lock) { - return@invoke fuRenderKit.renderWithInput(input).texture?.texId ?: -1 + var result = -1 + EglBaseLock.lock { + result = fuRenderKit.renderWithInput(input).texture?.texId ?: -1 } + result } return@setFilter ret ?: -1 } @@ -728,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) @@ -851,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) @@ -933,4 +935,4 @@ class FaceUnityBeautyAPIImpl : FaceUnityBeautyAPI, 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/faceunity/utils/egl/GLFrameBuffer.java b/Android/APIExample/app/src/main/java/io/agora/beautyapi/faceunity/utils/egl/GLFrameBuffer.java index 42d8f3480..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/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..11462ac7c --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/SenseTimeBeautyAPIImpl.kt @@ -0,0 +1,934 @@ +/* + * 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()) + apiReporter.release() + + 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 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() + nv21ByteBuffer = ByteBuffer.allocateDirect(nv21Size) + return null + } + val nv21ByteArray = ByteArray(nv21Size) + val i420Buffer = buffer as? I420Buffer ?: buffer.toI420() + + 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() + } + } + } + + // 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 + +} 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..a520b1cc1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/beautyapi/sensetime/utils/APIReporter.kt @@ -0,0 +1,170 @@ +package io.agora.beautyapi.sensetime.utils + +import io.agora.rtc2.RtcEngine +import org.json.JSONObject +import java.lang.ref.WeakReference +import java.util.concurrent.Executors +import java.util.concurrent.RejectedExecutionException + +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) { + submitReport { + 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) { + 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) + 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() + } + + fun release() { + executorService.shutdown() + } + + // ---------------------- private ---------------------- + + private fun configParameters() { + submitReport { + 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) { + 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) + 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 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() + } catch (e: Exception) { + LogUtils.e(tag, "convert to json fail: $e dictionary: $dictionary") + null + } + } +} 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_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/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/layout/fragment_face_capture.xml b/Android/APIExample/app/src/main/res/layout/fragment_face_capture.xml deleted file mode 100644 index de7b4c4ee..000000000 --- a/Android/APIExample/app/src/main/res/layout/fragment_face_capture.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 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 e2e3cab68..f364c7821 100755 --- a/Android/APIExample/app/src/main/res/navigation/nav_graph.xml +++ b/Android/APIExample/app/src/main/res/navigation/nav_graph.xml @@ -137,9 +137,6 @@ - @@ -176,11 +173,21 @@ android:label="@string/item_third_party_beauty" tools:layout="@layout/fragment_third_party_beauty"> + + + + - - + + + + 自动选择 + 硬编 + 软编 + + + + 关闭 + 类型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..9ccee7ace --- /dev/null +++ b/Android/APIExample/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,443 @@ + + + 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集成后再编译运行 + + 私有云 + IP地址 + 请输入IP地址 + 日志上报 + 日志服务域名/IP + 请输入日志服务域名/IP + 日志服务端口 + 请输入日志服务端口 + 日志服务路径 + 请输入日志服务路径 + 使用Https + 加入Ex频道 + 离开Ex频道 + 离开选项 + 确认 + 提示 + 取消 + 停止音频录制 + 停止所有音效 + 停止混音 + 麦克风录制已停止! + B帧 + 编码类型 + 在默认浏览器打开 + 刷新 + 保存 + 重置 + AppID为空! + 商汤美颜 + 播放器错误: %s + 播放器播放结束。 + 是否重新打开url? + 首帧出图优化 + 开启后无法关闭,并且需要主辅两端都开启时才生效 + 开始录制 + 停止录制 + 透明背景 + 转发音频流 + 用户ID + View背景色(只在居中渲染模式下有效果) + 渲染模式 + 平铺 + 居中 + 拉伸 + 默认 + 耳机 + 听筒 + 耳机(无麦克风) + 扬声器 + 外部扬声器(尚不支持) + 蓝牙耳机 + USB 设备(Type-C) + 第三方播放器 + 当前设备不支持! + 垫片 + 自定义 + 请先参考README配置stream encrypt! + 播放音轨 + 推送音轨 + 音轨%d + 请打开通知权限,防止后台录音中断 + 请打开通知权限,防止后台共享屏幕中断 + 视频元数据 + 发送 + 音频元数据 + 前置 + 后置 + 其他 + 默认摄像头 + 广角摄像头 + 超宽摄像头 + 长焦摄像头 + 进入画中画模式 + 透明渲染 + 极速直播Url拉流 + 本地帧率,范围:1-60 + 远端帧率,范围:1-60 + 场景 + 截图时机 + 请先加入频道 + 外部EglContext + 外部EglContext设置 + 频道号为空 + 目标频道号为空 + 是否转码 + 此示例演示了如何使用SDK进行语音通话的功能。 + 静音 + 声音模糊 + 空气传播模拟 + 衰减系数 + 角色 + 主播 + 观众 + Multipath + Multipath Mode + Multipath Type + 设置层级配置(最多3层) + 基础美颜 + 自然美颜 + 主播美颜 + 学妹妆 + 学姐妆 + 日常 + 月光 + 奶油 + diff --git a/Android/APIExample/app/src/main/res/values/arrays.xml b/Android/APIExample/app/src/main/res/values/arrays.xml index 740b8015a..a57f1b098 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 @@ -204,7 +207,6 @@ VD_960x720 VD_1280x720 VD_1920x1080 - VD_1920x1080 VD_2540x1440 VD_3840x2160 @@ -259,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 @@ -338,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 @@ -396,20 +403,37 @@ - 模板女 - 模板男 - 模板自然 + @string/agora_beauty_style_basic + @string/agora_beauty_style_ordinary + @string/agora_beauty_style_anchor + + + + Beauty-Basic + Beauty-Ordinary + Beauty-Anchor - 学妹妆 - 学姐妆 + @string/agora_makeup_style_young + @string/agora_makeup_style_mature + + + + Makeup-Young + Makeup-Mature - 原生 - 冷白 - 嫩白 + @string/agora_filter_style_daily + @string/agora_filter_style_moonlight + @string/agora_filter_style_cream + + + + Filter-Daily + Filter-Moonlight + Filter-Cream @@ -440,4 +464,4 @@ VIDEO_STREAM_LAYER_3 VIDEO_STREAM_LAYER_4 - \ No newline at end of file + diff --git a/Android/APIExample/app/src/main/res/values/strings.xml b/Android/APIExample/app/src/main/res/values/strings.xml index ad756046a..6d2007813 100644 --- a/Android/APIExample/app/src/main/res/values/strings.xml +++ b/Android/APIExample/app/src/main/res/values/strings.xml @@ -85,54 +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 - Scree 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 - Face Capture + Media Recorder + Local Video Transcoding Simulcast Multipath @@ -205,7 +204,6 @@ This example demonstrates how to integrate third-party beauty sdk. his example demonstrates how to integrate agora beauty 2.0. This example demonstrates how to using ktv api to implement a scene demo. - This example demonstrates how to use face capture function. This example demonstrates how to use transparent rendering. This example demonstrates how to use ultra live stream with url. This example demonstrates how to use Simulcast @@ -245,7 +243,7 @@ Recording Countdown Playing Countdown Recording on Microphone - Video Enhancement + Video Process Rhythm Player Beats per Measure Beats per Minute @@ -349,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 @@ -403,11 +399,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 @@ -419,7 +417,6 @@ Player Stream Publish Stream AudioStream%d - Please contact Agora customer service to obtain the face capture certificate and configure it to the AUTHENTICATION constant. Please turn on notification permission to prevent background recording from being interrupted. Please turn on notification permission to prevent background screen sharing from being interrupted. @@ -434,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 @@ -458,4 +455,12 @@ Multipath Mode Multipath Type Set Layers Config(Maximum 3) + Basic + Ordinary + Anchor + Young + Mature + Daily + Moonlight + Cream diff --git a/Android/APIExample/app/vendors.gradle b/Android/APIExample/app/vendors.gradle new file mode 100644 index 000000000..5a657f7c0 --- /dev/null +++ b/Android/APIExample/app/vendors.gradle @@ -0,0 +1,35 @@ +apply plugin: 'de.undercouch.download' + +task downloadVendorLibs { + def destFile = new File(buildDir, 'vendor_sensetime_9.3.1.zip') + download { + src 'https://fullapp.oss-cn-beijing.aliyuncs.com/thirdparty-beauty/android/vendor_sensetime_9.3.1.zip' + dest destFile + overwrite false + } + def fileTree = zipTree(destFile) + def HardwareBuffer = fileTree.getFiles().find { + it.name.contains("HardwareBuffer") + } + def SenseArSourceManager = fileTree.getFiles().find { + it.name.contains("SenseArSourceManager") + } + def STMobileJNI = fileTree.getFiles().find { + it.name.contains("STMobileJNI") + } + println "HardwareBuffer path= $HardwareBuffer.absolutePath" + println "SenseArSourceManager path= $SenseArSourceManager.absolutePath" + println "STMobileJNI path= $STMobileJNI.parentFile.absolutePath" + copy { + from HardwareBuffer + into 'libs' + } + copy { + from SenseArSourceManager + into 'libs' + } + copy { + from STMobileJNI + into 'libs' + } +} \ No newline at end of file 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/APIExample/cloud_build.sh b/Android/APIExample/cloud_build.sh index dfaac5334..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" @@ -84,18 +79,18 @@ fi sed -i -e "s#applicationId \"io.agora.api.example\"#applicationId \"io.agora.entfull\"#g" app/build.gradle rm -f app/build.gradle-e cd app/src/main || exit 1 +curl --fail --location -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -o AgoraBeautyMaterial.zip "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/AgoraBeautyMaterial.zip" || exit 1 +rm -rf assets/beauty_agora +mkdir -p assets/beauty_agora +unzip -q AgoraBeautyMaterial.zip -d assets/beauty_agora || exit 1 +rm -f AgoraBeautyMaterial.zip +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 -curl -L -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -O "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/vender_agora_resources_apiexample.4.6.2.zip" -unzip -o vender_agora_resources_apiexample.4.6.2.zip -rm -f vender_agora_resources_apiexample.4.6.2.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 cd - || exit 1 ./gradlew clean || exit 1 diff --git a/Android/APIExample/gradle.properties b/Android/APIExample/gradle.properties index 9786e73c8..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.4 +rtc_sdk_version = 4.7.0 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/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 a0a79aba3..bd40b5f28 100644 --- a/HOOKS-GUIDE.md +++ b/HOOKS-GUIDE.md @@ -1,75 +1,75 @@ # 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 +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). -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. diff --git a/README.kr.md b/README.kr.md index b28ff6867..a89196f4c 100644 --- a/README.kr.md +++ b/README.kr.md @@ -8,14 +8,15 @@ | 플랫폼 | 언어 | 프로젝트 위치 | 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-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) | 각 플랫폼의 내용을 참조하여 프로젝트에 대해 자세히 알아볼 수 있습니다. @@ -25,7 +26,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를 확인할 수 있습니다. @@ -33,4 +34,4 @@ ## 라이선스 -샘플 프로젝트는 MIT 라이선스를 따릅니다. \ No newline at end of file +샘플 프로젝트는 MIT 라이선스를 따릅니다. diff --git a/README.md b/README.md index 6d7940afd..c173f771b 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,15 @@ 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-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) | You can refer to each individual platform to learn more about the projects. @@ -25,7 +26,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 631e19de5..609ea2a5d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,14 +8,15 @@ | 平台 | 语言 | 项目位置 | 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-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) | 你可以进入不同平台的项目进行试用或参考源代码。 @@ -25,7 +26,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/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 89fd342a0..000000000 --- a/cicd/build-template/build-ios.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 && ./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 }} 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 a32c91747..000000000 --- a/cicd/scripts/ios_build.sh +++ /dev/null @@ -1,81 +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 - -#修改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 -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' -fi - -xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . - -mkdir app -mv *.ipa app && mv *.xcarchive app diff --git a/cicd/scripts/keycenter.py b/cicd/scripts/keycenter.py deleted file mode 100644 index 3779b1a3e..000000000 --- a/cicd/scripts/keycenter.py +++ /dev/null @@ -1,51 +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'<#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/README.md b/docs/ai/README.md new file mode 100644 index 000000000..a1d043c4d --- /dev/null +++ b/docs/ai/README.md @@ -0,0 +1,65 @@ +# 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. +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 `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 +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..e340e456b --- /dev/null +++ b/docs/ai/project-matrix.md @@ -0,0 +1,53 @@ +# 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` | + +## 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 + 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/.gitignore b/iOS/.gitignore index d39e42089..4e01c5a78 100644 --- a/iOS/.gitignore +++ b/iOS/.gitignore @@ -6,6 +6,7 @@ *.xcworkspacedata *.zip +APIExample/APIExample/Resources/AgoraBeautyMaterial.bundle/ agora_sdk *.xcarchive */libs @@ -15,7 +16,6 @@ Packaging.log *.app FULib/ SenseLib/ -ByteEffectLib/ # Xcode # 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/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample-Audio/.agent/skills/upsert-case/SKILL.md deleted file mode 100644 index 3e59cbd14..000000000 --- a/iOS/APIExample-Audio/.agent/skills/upsert-case/SKILL.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -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] -license: MIT -metadata: - author: APIExample Team - version: 1.0.0 - platform: iOS ---- - -# upsert-case — APIExample-Audio - -## 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+ - -Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case does not already exist. - -> **Audio-only constraint**: this project uses `AgoraAudio_iOS` SDK. The video module is not available. -> Do NOT add any video API calls. See the NEVER list below. - -## Files to Touch - -| 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) | - ---- - -## Step 1 — Create the Example Folder - -``` -APIExample-Audio/Examples/[Basic|Advanced]// -``` - -## Step 2 — Create the Swift File - -Create `.swift` with Entry and Main classes: - -```swift -import UIKit -import AgoraRtcKit - -class Entry: UIViewController { - @IBOutlet weak var channelTextField: UITextField! - - @IBAction func onJoinPressed(_ sender: UIButton) { - guard let channelName = channelTextField.text, !channelName.isEmpty else { return } - let storyboard = UIStoryboard(name: "", bundle: nil) - guard let mainVC = storyboard.instantiateViewController( - withIdentifier: "") as? Main else { return } - mainVC.configs = ["channelName": channelName] - navigationController?.pushViewController(mainVC, animated: true) - } -} - -class Main: BaseViewController { - var agoraKit: AgoraRtcEngineKit? - - override func viewDidLoad() { - super.viewDidLoad() - guard let channelName = configs["channelName"] as? String 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) - } - } - - override func willMove(toParent parent: UIViewController?) { - super.willMove(toParent: parent) - if parent == nil { - agoraKit?.leaveChannel() - AgoraRtcEngineKit.destroy() - } - } -} - -extension Main: AgoraRtcEngineDelegate { - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, - withUid uid: UInt, elapsed: Int) { - LogUtils.log(message: "Joined: \(channel) uid: \(uid)", level: .info) - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "Error: \(errorCode.rawValue)", level: .error) - } -} -``` - -## Step 3 — Create the Storyboard - -Create `APIExample-Audio/Base.lproj/.storyboard` with two scenes: - -| Scene | Storyboard ID | Class | -|-------|--------------|-------| -| Entry | `EntryViewController` | `Entry` | -| Main | `` | `Main` | - -UI should contain only audio controls — no video rendering views. - -## Step 4 — Register the MenuItem - -Add to the `menus` array in `APIExample-Audio/ViewController.swift`: - -```swift -MenuItem(name: "".localized, - storyboard: "", - controller: "") -``` - -## Step 5 — Update the Case Index - -Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - -```markdown -| | `Examples/[Basic|Advanced]//.swift` | `keyApi1()`, `keyApi2()` | One-line description | -``` - ---- - -## Verification Checklist - -- [ ] Folder created under correct category (Basic / Advanced) -- [ ] Both Entry and Main classes exist in the Swift file -- [ ] Main inherits `BaseViewController` -- [ ] Storyboard has correct scene IDs -- [ ] No video rendering views in the storyboard -- [ ] MenuItem added to `ViewController.swift` -- [ ] `leaveChannel()` + `AgoraRtcEngineKit.destroy()` called in `willMove(toParent:)` when `parent == nil` -- [ ] UI updates inside delegate callbacks dispatched to `DispatchQueue.main` -- [ ] Microphone permission requested before `joinChannel()` -- [ ] Case Index row added/updated in `ARCHITECTURE.md` -- [ ] Project builds without errors - ---- - -## NEVER - -- NEVER call `enableVideo()`, `setupLocalVideo()`, `setupRemoteVideo()`, or `startPreview()` — the SDK has no video module -- NEVER add `AgoraRtcVideoCanvas` or `VideoView` to any storyboard or code in this project -- 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 share an `AgoraRtcEngineKit` instance between cases -- NEVER skip updating the Case Index in `ARCHITECTURE.md` diff --git a/iOS/APIExample-Audio/.agent/skills/query-cases/SKILL.md b/iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md similarity index 96% rename from iOS/APIExample-Audio/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample-Audio/.agents/skills/query-cases/SKILL.md index 6b6676b67..2552f9ac2 100644 --- a/iOS/APIExample-Audio/.agent/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/.agent/skills/review-case/SKILL.md b/iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md similarity index 71% rename from iOS/APIExample-Audio/.agent/skills/review-case/SKILL.md rename to iOS/APIExample-Audio/.agents/skills/review-case/SKILL.md index c1cb428f8..599fe635f 100644 --- a/iOS/APIExample-Audio/.agent/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 @@ -14,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) @@ -35,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. --- @@ -109,6 +114,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 new file mode 100644 index 000000000..267aa9a79 --- /dev/null +++ b/iOS/APIExample-Audio/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,242 @@ +--- +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, Xcode target membership, and Case Index update. +license: MIT +metadata: + author: APIExample Team + version: 1.0.0 + platform: iOS +--- + +# upsert-case — APIExample-Audio + +## When to Use + +- **Add**: the feature has no existing case in `Examples/Basic/` or `Examples/Advanced/` +- **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. + +> **Audio-only constraint**: this project uses `AgoraAudio_iOS` SDK. The video module is not available. +> Do NOT add any video API calls. See the NEVER list below. + +## Files to Touch + +| Scenario | Files | +|----------|-------| +| 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 + +``` +APIExample-Audio/Examples/[Basic|Advanced]// +``` + +## Step 2 — Create the Swift File + +Create `.swift` with Entry and Main classes: + +```swift +import UIKit +import AgoraRtcKit + +class Entry: UIViewController { + @IBOutlet weak var channelTextField: UITextField! + + @IBAction func onJoinPressed(_ sender: UIButton) { + guard let channelName = channelTextField.text, !channelName.isEmpty else { return } + let storyboard = UIStoryboard(name: "", bundle: nil) + guard let mainVC = storyboard.instantiateViewController( + withIdentifier: "") as? Main else { return } + mainVC.configs = ["channelName": channelName] + navigationController?.pushViewController(mainVC, animated: true) + } +} + +class Main: BaseViewController { + private(set) var agoraKit: AgoraRtcEngineKit? + private var tokenRequestID = 0 + + override func viewDidLoad() { + super.viewDidLoad() + 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) + // 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 { onDestroy() } + } +} + +extension Main: AgoraRtcEngineDelegate { + 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) uid: \(uid)", level: .info) + } + } + + 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: "Error: \(errorCode.rawValue)", level: .error) + } + } +} +``` + +## Step 3 — Create the Storyboard + +Create `APIExample-Audio/Base.lproj/.storyboard` with two scenes: + +| Scene | Storyboard ID | Class | +|-------|--------------|-------| +| Entry | `EntryViewController` | `Entry` | +| Main | `` | `Main` | + +UI should contain only audio controls — no video rendering views. + +## Step 4 — Register the MenuItem + +Add to the `menus` array in `APIExample-Audio/ViewController.swift`: + +```swift +MenuItem(name: "".localized, + storyboard: "", + controller: "") +``` + +## 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`: + +```markdown +| | `Examples/[Basic|Advanced]//.swift` | `keyApi1()`, `keyApi2()` | One-line description | +``` + +--- + +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) +- [ ] Both Entry and Main classes exist in the Swift file +- [ ] Main inherits `BaseViewController` +- [ ] 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()` +- [ ] 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 + +--- + +## NEVER + +- NEVER call `enableVideo()`, `setupLocalVideo()`, `setupRemoteVideo()`, or `startPreview()` — the SDK has no video module +- NEVER add `AgoraRtcVideoCanvas` or `VideoView` to any storyboard or code in this project +- 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/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-Audio/APIExample-Audio.xcodeproj/project.pbxproj b/iOS/APIExample-Audio/APIExample-Audio.xcodeproj/project.pbxproj index 5cf3fcbf2..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 */; }; @@ -44,7 +45,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 +59,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 +67,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 */; }; @@ -127,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 = ""; }; @@ -414,7 +413,6 @@ 671BD6B227E1DB2D0076D5E1 /* CustomAudioRender */ = { isa = PBXGroup; children = ( - 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */, 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */, 671BD6B727E1DB2D0076D5E1 /* CustomAudioRender.swift */, ); @@ -470,6 +468,7 @@ children = ( 03D13BDD2448758B00B599B3 /* Info.plist */, 03D13BCF2448758900B599B3 /* AppDelegate.swift */, + BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */, 03D13BD82448758B00B599B3 /* Assets.xcassets */, 03D13BDA2448758B00B599B3 /* LaunchScreen.storyboard */, 033A9F2C252D737900BC26E1 /* Localizable.strings */, @@ -480,7 +479,6 @@ E70EE9E128F6A6FF00EE9E7F /* JoinChannelAudio(Token) */ = { isa = PBXGroup; children = ( - E70EE9E228F6A6FF00EE9E7F /* JoinChannelAudioToken.strings */, E70EE9E428F6A6FF00EE9E7F /* JoinChannelAudioToken.swift */, E70EE9E528F6A6FF00EE9E7F /* JoinChannelAudioToken.storyboard */, ); @@ -500,7 +498,6 @@ E74878A028ADE21200CA2F58 /* RhythmPlayer */ = { isa = PBXGroup; children = ( - E74878A128ADE21200CA2F58 /* RhythmPlayer.strings */, E74878A328ADE21200CA2F58 /* RhythmPlayer.swift */, E74878A428ADE21200CA2F58 /* RhythmPlayer.storyboard */, ); @@ -591,8 +588,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 +605,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 */, @@ -703,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 */, @@ -805,18 +800,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 +817,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 +979,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1075,7 +1049,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/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/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/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/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/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 7a6635018..e8247a5fd 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/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/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/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/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/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/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings index 7431a98aa..5b8a82621 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/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/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/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/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/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/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/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/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings index 5a897d8e1..ed1deb75a 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" = "AI 音色调节"; + +/* 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)/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(Token)/zh-Hans.lproj/JoinChannelAudioToken.strings b/iOS/APIExample-Audio/APIExample-Audio/Examples/Basic/JoinChannelAudio(Token)/zh-Hans.lproj/JoinChannelAudioToken.strings index c1548454f..170839208 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" = "音频使用场景"; +/* 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/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/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..124a5ea10 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" = "音频使用场景"; -/* 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/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 c893af889..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 @@ -28,37 +27,25 @@ 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: "Rhythm Player".localized, storyboard: "RhythmPlayer", controller: "RhythmPlayer"), - 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"), ]), ] - 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/APIExample-Audio/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-Audio/APIExample-Audio/zh-Hans.lproj/Localizable.strings index fb0219a0c..3d407bc50 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,153 @@ 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旁路推流"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Pre-call 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" = "选择角色"; +"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/ARCHITECTURE.md b/iOS/APIExample-Audio/ARCHITECTURE.md index 823be6545..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 | @@ -12,7 +28,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 | @@ -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 @@ -50,14 +67,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/ # "Rhythm Player" - │ ├── PrecallTest/ # "Precall Test" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 + │ ├── PrecallTest/ # "Pre-call Test" │ └── SpatialAudio/ # "Spatial Audio" │ ├── Resources/ # Audio sample files @@ -81,9 +98,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,15 +136,22 @@ 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 -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-Audio/Podfile b/iOS/APIExample-Audio/Podfile index a256d1e0d..bd9c7d606 100644 --- a/iOS/APIExample-Audio/Podfile +++ b/iOS/APIExample-Audio/Podfile @@ -7,10 +7,10 @@ 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 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..9ac15aa51 100644 --- a/iOS/APIExample-Audio/README.md +++ b/iOS/APIExample-Audio/README.md @@ -9,11 +9,12 @@ 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 -- 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 @@ -23,18 +24,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 @@ -73,13 +70,13 @@ 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) - 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-Audio/README.zh.md b/iOS/APIExample-Audio/README.zh.md new file mode 100644 index 000000000..b757a1891 --- /dev/null +++ b/iOS/APIExample-Audio/README.zh.md @@ -0,0 +1,65 @@ +# 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/api-reference/faq/integration/local_network_privacy) + +## 环境准备 + +- iOS 12.0 或更高版本 +- Xcode 13.0 或更高版本,且该版本支持将最低部署目标设为 iOS 12([Xcode 支持范围](https://developer.apple.com/support/xcode/)) +- 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://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) +- 完整 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-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-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/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample-OC/.agent/skills/upsert-case/SKILL.md deleted file mode 100644 index 92a9c1d7c..000000000 --- a/iOS/APIExample-OC/.agent/skills/upsert-case/SKILL.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -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] -license: MIT -metadata: - author: APIExample Team - version: 1.0.0 - platform: iOS ---- - -# upsert-case — APIExample-OC - -## 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+ - -Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case does not already exist. - -## Files to Touch - -| 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) | - ---- - -## Step 1 — Create the Example Folder - -``` -APIExample-OC/Examples/[Basic|Advanced]// -``` - -## Step 2 — Create the Header File - -Create `.h`: - -```objc -#import "BaseViewController.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface Entry : UIViewController -@end - -@interface Main : BaseViewController -@end - -NS_ASSUME_NONNULL_END -``` - -## Step 3 — Create the Implementation File - -Create `.m`: - -```objc -#import ".h" -#import -#import "KeyCenter.h" -#import "NetworkManager.h" - -@interface Entry () -@property (weak, nonatomic) IBOutlet UITextField *channelTextField; -@end - -@implementation Entry -- (IBAction)onJoinPressed:(UIButton *)sender { - NSString *channelName = self.channelTextField.text; - if (channelName.length == 0) return; - UIStoryboard *sb = [UIStoryboard storyboardWithName:@"" bundle:nil]; - Main *mainVC = [sb instantiateViewControllerWithIdentifier:@""]; - mainVC.configs = @{@"channelName": channelName}; - [self.navigationController pushViewController:mainVC animated:YES]; -} -@end - -@interface Main () -@property (nonatomic, strong) AgoraRtcEngineKit *agoraKit; -@end - -@implementation Main -- (void)viewDidLoad { - [super viewDidLoad]; - NSString *channelName = self.configs[@"channelName"]; - 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]; - }]; -} - -- (void)viewDidDisappear:(BOOL)animated { - [super viewDidDisappear:animated]; - if (self.isMovingFromParentViewController) { - [self.agoraKit leaveChannel:nil]; - [AgoraRtcEngineKit destroy]; - } -} -@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); -} -- (void)rtcEngine:(AgoraRtcEngineKit *)engine didOccurError:(AgoraErrorCode)errorCode { - NSLog(@"Error: %ld", (long)errorCode); -} -@end -``` - -## Step 4 — Create the Storyboard - -Create `APIExample-OC/.storyboard` with two scenes: - -| Scene | Storyboard ID | Class | -|-------|--------------|-------| -| Entry | `EntryViewController` | `Entry` | -| Main | `` | `Main` | - -## Step 5 — Register the MenuItem - -Add to `+[MenuSection menus]` in `ViewController.m`: - -```objc -[[MenuItem alloc] initWithName:NSLocalizedString(@"", nil) - storyboard:@"" - controller:@""] -``` - -## Step 6 — Update the Case Index - -Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - -```markdown -| | `Examples/[Basic|Advanced]//.m` | `keyApi1:`, `keyApi2:` | One-line description | -``` - ---- - -## Verification Checklist - -- [ ] Folder created under correct category (Basic / Advanced) -- [ ] Both `.h` and `.m` files created with Entry and Main classes -- [ ] Main inherits `BaseViewController` and conforms to `AgoraRtcEngineDelegate` -- [ ] Storyboard has correct scene IDs -- [ ] MenuItem added to `ViewController.m` -- [ ] `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` -- [ ] Camera/microphone permissions requested before `joinChannelByToken:` -- [ ] Case Index row added/updated in `ARCHITECTURE.md` -- [ ] Project builds without errors - ---- - -## NEVER - -- NEVER create `AgoraRtcEngineKit` in the Entry VC -- 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 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/.agent/skills/query-cases/SKILL.md b/iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md similarity index 96% rename from iOS/APIExample-OC/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample-OC/.agents/skills/query-cases/SKILL.md index 8817e67f1..7253b3c0e 100644 --- a/iOS/APIExample-OC/.agent/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/.agent/skills/review-case/SKILL.md b/iOS/APIExample-OC/.agents/skills/review-case/SKILL.md similarity index 65% rename from iOS/APIExample-OC/.agent/skills/review-case/SKILL.md rename to iOS/APIExample-OC/.agents/skills/review-case/SKILL.md index 2c9d57145..e1b25b3f3 100644 --- a/iOS/APIExample-OC/.agent/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 @@ -13,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 @@ -20,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 @@ -76,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. --- @@ -136,6 +137,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 ``` @@ -154,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 new file mode 100644 index 000000000..62d483593 --- /dev/null +++ b/iOS/APIExample-OC/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,264 @@ +--- +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, Xcode target + membership, and Case Index update. +license: MIT +metadata: + author: APIExample Team + version: 1.0.0 + platform: iOS +--- + +# upsert-case — APIExample-OC + +## When to Use + +- **Add**: the feature has no existing case in `Examples/Basic/` or `Examples/Advanced/` +- **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. + +## Files to Touch + +| Scenario | Files | +|----------|-------| +| 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 + +``` +APIExample-OC/Examples/[Basic|Advanced]// +``` + +## Step 2 — Create the Header File + +Create `.h`: + +```objc +#import "BaseViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface Entry : UIViewController +@end + +@interface Main : BaseViewController +@end + +NS_ASSUME_NONNULL_END +``` + +## Step 3 — Create the Implementation File + +Create `.m`: + +```objc +#import ".h" +#import +#import "KeyCenter.h" +#import "APIExample_OC-swift.h" + +@interface Entry () +@property (weak, nonatomic) IBOutlet UITextField *channelTextField; +@end + +@implementation Entry +- (IBAction)onJoinPressed:(UIButton *)sender { + NSString *channelName = self.channelTextField.text; + if (channelName.length == 0) return; + UIStoryboard *sb = [UIStoryboard storyboardWithName:@"" bundle:nil]; + Main *mainVC = [sb instantiateViewControllerWithIdentifier:@""]; + mainVC.configs = @{@"channelName": channelName}; + [self.navigationController pushViewController:mainVC animated:YES]; +} +@end + +@interface Main () +@property (nonatomic, strong, nullable) AgoraRtcEngineKit *agoraKit; +@property (nonatomic) NSUInteger tokenRequestID; +@end + +@implementation Main +- (void)viewDidLoad { + [super viewDidLoad]; + [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 media, then call requestJoin:requestPermission: with the actual permission flow. +} + +- (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 { + __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 { + __weak typeof(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.agoraKit != engine) return; + NSLog(@"Error: %ld", (long)errorCode); + }); +} +@end +``` + +## Step 4 — Create the Storyboard + +Create `APIExample-OC/.storyboard` with two scenes: + +| Scene | Storyboard ID | Class | +|-------|--------------|-------| +| Entry | `EntryViewController` | `Entry` | +| Main | `` | `Main` | + +## Step 5 — Register the MenuItem + +Add to `+[MenuSection menus]` in `ViewController.m`: + +```objc +[[MenuItem alloc] initWithName:NSLocalizedString(@"", nil) + storyboard:@"" + controller:@""] +``` + +## 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`: + +```markdown +| | `Examples/[Basic|Advanced]//.m` | `keyApi1:`, `keyApi2:` | One-line description | +``` + +--- + +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) +- [ ] Both `.h` and `.m` files created with Entry and Main classes +- [ ] 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` +- [ ] 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 + +--- + +## NEVER + +- NEVER create `AgoraRtcEngineKit` in the Entry VC +- 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/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-OC/APIExample-OC.xcodeproj/project.pbxproj b/iOS/APIExample-OC/APIExample-OC.xcodeproj/project.pbxproj index 1469fa683..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 */; }; @@ -187,6 +188,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 = ""; }; @@ -196,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 = ""; }; @@ -460,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 */, @@ -1311,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 */, @@ -1395,6 +1403,7 @@ children = ( AB84740D2DF9CD7900B4DB73 /* Multipath.storyboard */, AB84741C2DF9CD9300B4DB73 /* en */, + ABB90ACA2DF728F8007FDDEB /* zh-Hans */, ); name = Multipath.storyboard; sourceTree = ""; @@ -1404,6 +1413,7 @@ children = ( AB8474132DF9CD7900B4DB73 /* Simulcast.storyboard */, AB84741E2DF9CD9B00B4DB73 /* en */, + ABB90ACC2DF7293C007FDDEB /* zh-Hans */, ); name = Simulcast.storyboard; sourceTree = ""; @@ -1620,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; @@ -1628,7 +1637,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 = ""; @@ -1662,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; @@ -1670,7 +1678,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 +1711,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 +1740,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/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/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 7a6635018..e8247a5fd 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/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/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings b/iOS/APIExample-OC/APIExample-OC/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings index 78350d5c9..e38a593fe 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..83caeba72 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..c168a39a6 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/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/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..57d22bdc6 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 @@ +/* Element = "button"; configuration.title = "Button"; ObjectID = "99i-oC-tHx"; */ +"99i-oC-tHx.configuration.title" = "加密方式"; -/* Class = "UIButton"; configuration.title = "Button"; ObjectID = "99i-oC-tHx"; */ -"99i-oC-tHx.configuration.title" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "99i-oC-tHx"; */ +"99i-oC-tHx.normalTitle" = "加密方式"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "99i-oC-tHx"; */ -"99i-oC-tHx.normalTitle" = "Button"; +/* 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 = "textField"; placeholder = "Enter encryption secret"; ObjectID = "gkN-x2-moD"; */ +"gkN-x2-moD.placeholder" = "加密密码"; -/* Class = "UITextField"; placeholder = "Enter encryption secret"; ObjectID = "gkN-x2-moD"; */ -"gkN-x2-moD.placeholder" = "Enter encryption secret"; +/* Element = "label"; text = "Encryption Mode"; ObjectID = "hXZ-v4-LRg"; */ +"hXZ-v4-LRg.text" = "加密方式"; -/* Class = "UILabel"; text = "Encryption Mode"; ObjectID = "hXZ-v4-LRg"; */ -"hXZ-v4-LRg.text" = "Encryption Mode"; +/* 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"; - -/* 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/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/APIExample-OC/ViewController.m b/iOS/APIExample-OC/APIExample-OC/ViewController.m index c7b3c5ff8..143ca3e3c 100644 --- a/iOS/APIExample-OC/APIExample-OC/ViewController.m +++ b/iOS/APIExample-OC/APIExample-OC/ViewController.m @@ -52,34 +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:@"Rhythm Player".localized storyboard:@"RhythmPlayer" 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/APIExample-OC/zh-Hans.lproj/Main.strings b/iOS/APIExample-OC/APIExample-OC/zh-Hans.lproj/Main.strings index 439c03858..b186739d6 100644 --- a/iOS/APIExample-OC/APIExample-OC/zh-Hans.lproj/Main.strings +++ b/iOS/APIExample-OC/APIExample-OC/zh-Hans.lproj/Main.strings @@ -1,3 +1,3 @@ /* Class = "UILabel"; text = "Title"; ObjectID = "0cK-Mg-28i"; */ -"0cK-Mg-28i.text" = "Title"; +"0cK-Mg-28i.text" = "标题"; diff --git a/iOS/APIExample-OC/ARCHITECTURE.md b/iOS/APIExample-OC/ARCHITECTURE.md index e7091a64d..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 | @@ -26,7 +37,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 | @@ -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 @@ -72,15 +84,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" @@ -88,12 +100,12 @@ APIExample-OC/ │ ├── MediaPlayer/ # "Media Player" │ ├── ScreenShare/ # "Screen Share" │ ├── VideoProcess/ # "Video Process" - │ ├── RhythmPlayer/ # "Rhythm Player" - │ ├── CreateDataStream/ # "Create Data Stream" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 + │ ├── 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" @@ -121,12 +133,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,15 +161,25 @@ viewDidLoad → [AgoraRtcEngineKit sharedEngineWithAppId:delegate:] ↓ [AgoraRtcEngineDelegate callbacks — may be on background thread] ↓ -viewDidDisappear / dealloc +willMoveToParentViewController: when parent == nil → [engine leaveChannel:] → [AgoraRtcEngineKit destroy] ``` +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/Podfile b/iOS/APIExample-OC/Podfile index d6eb3505e..43e0bc065 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 @@ -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..40296c295 100644 --- a/iOS/APIExample-OC/README.md +++ b/iOS/APIExample-OC/README.md @@ -9,11 +9,12 @@ 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 -- 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 @@ -23,14 +24,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 ``` @@ -73,13 +70,13 @@ 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) - 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/README.zh.md b/iOS/APIExample-OC/README.zh.md new file mode 100644 index 000000000..5f5c788f6 --- /dev/null +++ b/iOS/APIExample-OC/README.zh.md @@ -0,0 +1,65 @@ +# 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/api-reference/faq/integration/local_network_privacy) + +## 环境准备 + +- iOS 12.0 或更高版本 +- Xcode 13.0 或更高版本,且该版本支持将最低部署目标设为 iOS 12([Xcode 支持范围](https://developer.apple.com/support/xcode/)) +- 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://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) +- 完整 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/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-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-OC/zh-Hans.lproj/Localizable.strings b/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings index ab17a8e77..3a20fbbaa 100644 --- a/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample-OC/zh-Hans.lproj/Localizable.strings @@ -6,166 +6,185 @@ 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旁路推流"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source" = "视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Local Composite Graph" = "本地合图屏幕共享"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Pre-call 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" = "选择角色"; +"Send 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" = "画中画"; +"please input Token!" = "请输入Token"; +"please input AppId!" = "请输入APPID"; +"stopMicrophoneRecording" = "停止麦克风采集"; +"KTV Copyright Music" = "KTV版权音乐"; +"Multi Camera" = "多路摄像头"; +"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/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample-SwiftUI/.agent/skills/upsert-case/SKILL.md deleted file mode 100644 index 821cff8eb..000000000 --- a/iOS/APIExample-SwiftUI/.agent/skills/upsert-case/SKILL.md +++ /dev/null @@ -1,163 +0,0 @@ ---- -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] -license: MIT -metadata: - author: APIExample Team - version: 1.0.0 - platform: iOS ---- - -# upsert-case — APIExample-SwiftUI - -## 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+ - -Before adding, search the Case Index in `ARCHITECTURE.md` to confirm the case does not already exist. - -## Files to Touch - -| 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) | - ---- - -## Step 1 — Create the Example Folder - -``` -APIExample-SwiftUI/Examples/[Basic|Advanced]// -``` - -## Step 2 — Create the RTC Class - -Create `RTC.swift` — owns the engine lifecycle: - -```swift -import AgoraRtcKit -import SwiftUI - -class RTC: NSObject, ObservableObject { - var agoraKit: AgoraRtcEngineKit! - private var isJoined = false - - func setupRTC(configs: [String: Any]) { - let config = AgoraRtcEngineConfig() - config.appId = KeyCenter.AppId - agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) - - 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) - } - } - - func onDestroy() { - if isJoined { agoraKit.leaveChannel(nil) } - AgoraRtcEngineKit.destroy() - } -} - -extension RTC: AgoraRtcEngineDelegate { - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, - withUid uid: UInt, elapsed: Int) { - isJoined = true - LogUtils.log(message: "Joined: \(channel)", level: .info) - } - - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(message: "Error: \(errorCode)", level: .error) - } -} -``` - -## Step 3 — Create the SwiftUI Views - -Create `.swift` with Entry and Main views: - -```swift -import SwiftUI - -struct Entry: View { - @State private var channelName = "" - @State private var isActive = false - @State private var configs: [String: Any] = [:] - - var body: some View { - VStack { - TextField("Enter channel name".localized, text: $channelName) - .textFieldStyle(.roundedBorder).padding() - Button("Join".localized) { - configs = ["channelName": channelName] - isActive = true - }.disabled(channelName.isEmpty) - NavigationLink(destination: (configs: configs), - isActive: $isActive) { EmptyView() } - } - } -} - -struct : View { - @State var configs: [String: Any] = [:] - @ObservedObject private var rtc = RTC() - - var body: some View { - VStack { /* UI here */ } - .onAppear { rtc.setupRTC(configs: configs) } - .onDisappear { rtc.onDestroy() } - } -} -``` - -## Step 4 — Register the MenuItem - -Add to the `menus` array in `APIExample-SwiftUI/ContentView.swift`: - -```swift -MenuItem(name: "".localized, view: AnyView(Entry())) -``` - -## Step 5 — Update the Case Index - -Add a row to the `## Case Index` table in `ARCHITECTURE.md`: - -```markdown -| | `Examples/[Basic|Advanced]//` | `keyApi1()`, `keyApi2()` | One-line description | -``` - ---- - -## Verification Checklist - -- [ ] 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 -- [ ] `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` -- [ ] Case Index row added/updated in `ARCHITECTURE.md` -- [ ] Project builds without errors - ---- - -## 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 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 -- NEVER call `joinChannel` before requesting camera/microphone permissions -- NEVER skip updating the Case Index in `ARCHITECTURE.md` diff --git a/iOS/APIExample-SwiftUI/.agent/skills/query-cases/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md similarity index 96% rename from iOS/APIExample-SwiftUI/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample-SwiftUI/.agents/skills/query-cases/SKILL.md index 03f4a9dda..14f6505a9 100644 --- a/iOS/APIExample-SwiftUI/.agent/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/.agent/skills/review-case/SKILL.md b/iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md similarity index 68% rename from iOS/APIExample-SwiftUI/.agent/skills/review-case/SKILL.md rename to iOS/APIExample-SwiftUI/.agents/skills/review-case/SKILL.md index 04c4437b9..d8ed3220b 100644 --- a/iOS/APIExample-SwiftUI/.agent/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 @@ -13,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 @@ -40,21 +52,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 +135,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 new file mode 100644 index 000000000..e0e66caa0 --- /dev/null +++ b/iOS/APIExample-SwiftUI/.agents/skills/upsert-case/SKILL.md @@ -0,0 +1,240 @@ +--- +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, Xcode target membership, + and Case Index update. +license: MIT +metadata: + author: APIExample Team + version: 1.0.0 + platform: iOS +--- + +# upsert-case — APIExample-SwiftUI + +## When to Use + +- **Add**: the feature has no existing case in `Examples/Basic/` or `Examples/Advanced/` +- **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. + +## Files to Touch + +| Scenario | Files | +|----------|-------| +| 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 + +``` +APIExample-SwiftUI/Examples/[Basic|Advanced]// +``` + +## Step 2 — Create the RTC Class + +Create `RTC.swift` — owns the engine lifecycle: + +```swift +import AgoraRtcKit +import SwiftUI + +class RTC: NSObject, ObservableObject { + 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. + } + + // 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 + } +} + +extension RTC: AgoraRtcEngineDelegate { + 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) uid: \(uid)", level: .info) + } + } + + 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: "Error: \(errorCode.rawValue)", level: .error) + } + } +} +``` + +## Step 3 — Create the SwiftUI Views + +Create `.swift` with Entry and Main views: + +```swift +import SwiftUI + +struct Entry: View { + @State private var channelName = "" + @State private var isActive = false + @State private var configs: [String: Any] = [:] + + var body: some View { + VStack { + TextField("Enter channel name".localized, text: $channelName) + .textFieldStyle(.roundedBorder).padding() + Button("Join".localized) { + configs = ["channelName": channelName] + isActive = true + }.disabled(channelName.isEmpty) + NavigationLink(destination: (configs: configs), + isActive: $isActive) { EmptyView() } + } + } +} + +struct : View { + @State var configs: [String: Any] = [:] + @StateObject private var rtc = RTC() + + var body: some View { + VStack { /* UI here */ } + .onAppear { rtc.setupRTC(configs: configs) } + .onDisappear { rtc.onDestroy() } + } +} +``` + +## Step 4 — Register the MenuItem + +Add to the `menus` array in `APIExample-SwiftUI/ContentView.swift`: + +```swift +MenuItem(name: "".localized, view: AnyView(Entry())) +``` + +## 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`: + +```markdown +| | `Examples/[Basic|Advanced]//` | `keyApi1()`, `keyApi2()` | One-line description | +``` + +--- + +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) +- [ ] RTC class inherits `NSObject`, conforms to `ObservableObject` and `AgoraRtcEngineDelegate` +- [ ] Engine created in `setupRTC`, destroyed in `onDestroy` +- [ ] 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` +- [ ] 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 + +--- + +## NEVER + +- NEVER create `AgoraRtcEngineKit` in the Entry view +- 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 +- NEVER call `joinChannel` before requesting camera/microphone permissions +- NEVER skip updating the Case Index in `ARCHITECTURE.md` diff --git a/iOS/APIExample-SwiftUI/AGENTS.md b/iOS/APIExample-SwiftUI/AGENTS.md index 1ca46420c..15bc2f769 100644 --- a/iOS/APIExample-SwiftUI/AGENTS.md +++ b/iOS/APIExample-SwiftUI/AGENTS.md @@ -17,15 +17,16 @@ 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 | 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-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj b/iOS/APIExample-SwiftUI/APIExample-SwiftUI.xcodeproj/project.pbxproj index 29719b49b..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 */; }; @@ -175,7 +173,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 = ""; }; @@ -254,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 = ""; }; @@ -625,7 +622,6 @@ 4C9309662CBA91520085EFF9 /* CreateDataStream */, 4C9309872CBA91530085EFF9 /* CustomAudioRender */, 4C93096E2CBA91520085EFF9 /* CustomPCMAudioSource */, - 4C93095A2CBA91520085EFF9 /* FaceCapture */, 4C9309712CBA91520085EFF9 /* JoinMultiChannel */, 4C9309692CBA91520085EFF9 /* KtvCopyrightMusic */, 4C9309632CBA91520085EFF9 /* LiveStreaming */, @@ -685,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 = ( @@ -1014,6 +1001,7 @@ knownRegions = ( en, Base, + "zh-Hans", ); mainGroup = 4C9306802CB9607E0085EFF9; productRefGroup = 4C93068A2CB9607E0085EFF9 /* Products */; @@ -1219,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 */, @@ -1262,6 +1248,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 +1403,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 +1442,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 +1472,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 +1503,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/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/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/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/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/ContentView.swift b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/ContentView.swift index daa2f15ab..496436f7e 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,48 +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: "Agora Beauty".localized, - view: AnyView(AgoraBeautyEntry())), - MenuItem(name: "Rhythm Player".localized, - view: AnyView(RhythmPlayerEntry())), - MenuItem(name: "Create Data Stream".localized, + MenuItem(name: "Send Data Stream".localized, view: AnyView(CreateDataStreamEntry())), MenuItem(name: "Media Channel Relay".localized, view: AnyView(MediaChannelRelayEntry())), @@ -84,21 +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: "Face Capture".localized, - view: AnyView(FaceCaptureEntry())), MenuItem(name: "Simulcast".localized, view: AnyView(SimulcastEntry())), MenuItem(name: "Multipath".localized, - view: AnyView(MultipathEntry())), - MenuItem(name: "LocalCompositeGraph".localized, view: AnyView(LocalCompositeGraphEntry())) + view: AnyView(MultipathEntry())) ]) ] @@ -125,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) @@ -152,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/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/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/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/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/APIExample-SwiftUI/Resources/Localizable.strings b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings similarity index 75% rename from iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/Localizable.strings rename to iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings index 586b06e8c..6ea3fe861 100644 --- a/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/Localizable.strings +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/en.lproj/Localizable.strings @@ -1,44 +1,41 @@ -/* - 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"; "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"; -"Media Injection" = "Media Injection"; -"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"; -"Raw Media Data" = "Raw Audio/Video Data"; +"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"; -"Super Resolution" = "Super Resolution"; -"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"; @@ -112,8 +109,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"; @@ -123,17 +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"; -"Group Video Chat" = "Group Video Call"; "Screen Share + Camera" = "Screen Share + Camera"; "please input channel name!" = "Please enter channel name!"; -"Video Process" = "Video Enhancement"; -"Agora Beauty" = "Agora Beauty"; +"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"; @@ -141,27 +136,17 @@ "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"; -"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+"; @@ -172,17 +157,13 @@ "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"; "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"; @@ -213,7 +194,6 @@ "Relay" = "Relay"; "Pause" = "Pause"; "Resume" = "Resume"; -"Stop" = "Stop"; "Play" = "Play"; "software" = "Software"; "hardware" = "Hardware"; @@ -232,7 +212,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"; @@ -276,4 +255,24 @@ "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"; +"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..452b28357 --- /dev/null +++ b/iOS/APIExample-SwiftUI/APIExample-SwiftUI/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,278 @@ +/* + 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" = "转码"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source" = "视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Picture In Picture" = "画中画"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Pre-call 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" = "选择角色"; +"Send 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" = "切换摄像头"; +"screenshot" = "截图"; +"remote user has not joined, and cannot take a screenshot" = "远端用户未加入, 无法截图"; +"Screenshot successful" = "截图成功"; +"please input Token!" = "请输入Token"; +"please input AppId!" = "请输入APPID"; +"stopMicrophoneRecording" = "停止麦克风采集"; +"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文件是否正确"; +"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" = "低延时强降噪模式"; +"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" = "销毁视频轨道"; +"Create Video Track(Encode)" = "创建编码视频轨道"; +"Create VideoTrack" = "创建视频轨道"; +"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" = "最多配置三个"; +"Local Composite Graph" = "本地合图屏幕共享"; +"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..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 | @@ -27,8 +33,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 +42,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 | @@ -80,8 +84,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" @@ -91,27 +95,25 @@ 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" - │ ├── AgoraBeauty/ # "Agora Beauty" - │ ├── RhythmPlayer/ # "Rhythm Player" - │ ├── CreateDataStream/ # "Create Data Stream" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 + │ ├── 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" - │ ├── FaceCapture/ # "Face Capture" │ ├── Simulcast/ # "Simulcast" │ └── Multipath/ # "Multipath" │ - ├── Resources/ # Audio/video sample files + ├── Resources/ # Audio/video sample files and en/zh-Hans localizations ├── Assets.xcassets/ └── Preview Content/ ``` @@ -131,12 +133,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 @@ -153,7 +158,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` @@ -178,8 +184,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-SwiftUI/Podfile b/iOS/APIExample-SwiftUI/Podfile index 8723d3546..a59f9d423 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 @@ -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-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-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/.agent/skills/query-cases/SKILL.md b/iOS/APIExample/.agents/skills/query-cases/SKILL.md similarity index 96% rename from iOS/APIExample/.agent/skills/query-cases/SKILL.md rename to iOS/APIExample/.agents/skills/query-cases/SKILL.md index f35362ee4..ebe425a63 100644 --- a/iOS/APIExample/.agent/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/.agent/skills/review-case/SKILL.md b/iOS/APIExample/.agents/skills/review-case/SKILL.md similarity index 71% rename from iOS/APIExample/.agent/skills/review-case/SKILL.md rename to iOS/APIExample/.agents/skills/review-case/SKILL.md index 70e70d6eb..cab4f5f99 100644 --- a/iOS/APIExample/.agent/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 @@ -13,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 @@ -24,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 @@ -79,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. --- @@ -131,6 +132,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/.agent/skills/upsert-case/SKILL.md b/iOS/APIExample/.agents/skills/upsert-case/SKILL.md similarity index 54% rename from iOS/APIExample/.agent/skills/upsert-case/SKILL.md rename to iOS/APIExample/.agents/skills/upsert-case/SKILL.md index 9e1c531d2..6e8e93ff3 100644 --- a/iOS/APIExample/.agent/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 | --- @@ -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) + } } } ``` @@ -146,7 +205,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`: @@ -158,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) @@ -166,10 +239,15 @@ 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()` - [ ] 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 --- @@ -180,6 +258,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/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/iOS/APIExample/APIExample.xcodeproj/project.pbxproj b/iOS/APIExample/APIExample.xcodeproj/project.pbxproj index cb72c4c1a..6410afadd 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 */; }; @@ -76,19 +77,23 @@ 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; - 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 +102,8 @@ 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; }; + 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 */; }; 8B1422C326B50AB500FFF91B /* ExtensionAudioFilter.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 8B1422BD26B50AB400FFF91B /* ExtensionAudioFilter.hpp */; }; @@ -118,13 +124,12 @@ 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 */; }; + 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 */; }; @@ -133,21 +138,21 @@ 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 */; }; - 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 +160,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 +169,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,38 +182,30 @@ 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 */; }; 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 */; }; 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 */; }; @@ -335,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 = ""; }; @@ -355,11 +349,19 @@ 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 = ""; }; - 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 = ""; }; @@ -373,6 +375,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 = ""; }; @@ -388,10 +392,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 = ""; }; - 67CB2F0B27EB318200CB19D2 /* SpatialAudio.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SpatialAudio.storyboard; 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 = ""; }; @@ -417,7 +426,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 = ""; }; @@ -427,20 +435,32 @@ 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 = ""; }; 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 = ""; }; + 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 = ""; }; @@ -451,7 +471,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 = ""; }; - DDFD50272C205CF40050B344 /* TransparentRender.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = TransparentRender.storyboard; 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 +494,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 = ""; }; @@ -495,13 +513,9 @@ 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 = ""; }; - E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = KtvCopyrightMusic.storyboard; 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,14 +541,19 @@ 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 = ""; }; + F734791B2DF5C9CD0030C0CE /* AgoraBeautyMaterial.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = AgoraBeautyMaterial.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 = ""; }; F73479232DF5CA820030C0CE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AgoraBeauty.storyboard; sourceTree = ""; }; 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 */ @@ -800,7 +819,7 @@ isa = PBXGroup; children = ( DD6DCEE12CBD1D68008D7B72 /* hlg-hdr.yuv */, - F734791B2DF5C9CD0030C0CE /* beauty_material.bundle */, + F734791B2DF5C9CD0030C0CE /* AgoraBeautyMaterial.bundle */, DDFD50292C2065D00050B344 /* yuv_limit_range_alpha_1280_540_right.mp4 */, 8B349FE22681E2CE007247F2 /* agora-logo.png */, 576CA80925A9CC3A0091520B /* output.raw */, @@ -901,10 +920,20 @@ 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 = ( - 5744CE0325BA99FF0099AB66 /* VideoChat.strings */, 5744CE0525BA99FF0099AB66 /* VideoChat.swift */, 5744CE0625BA99FF0099AB66 /* VideoChat.storyboard */, ); @@ -932,7 +961,6 @@ 671BD6B227E1DB2D0076D5E1 /* CustomAudioRender */ = { isa = PBXGroup; children = ( - 671BD6B327E1DB2D0076D5E1 /* CustomAudioRender.strings */, 671BD6B527E1DB2D0076D5E1 /* CustomAudioRender.storyboard */, 671BD6B727E1DB2D0076D5E1 /* CustomAudioRender.swift */, ); @@ -1015,7 +1043,6 @@ 8BA5458E26AFEC8D00ED4295 /* SimpleFilter */ = { isa = PBXGroup; children = ( - 8BA5458F26AFEC8D00ED4295 /* SimpleFilter.strings */, 8BA5459126AFEC8D00ED4295 /* SimpleFilter.storyboard */, 8BA5459326AFEC8D00ED4295 /* SimpleFilter.swift */, ); @@ -1025,7 +1052,6 @@ 8BC751CF273E502700552265 /* LiveStreaming */ = { isa = PBXGroup; children = ( - 8BC751D0273E502700552265 /* LiveStreaming.strings */, 8BC751D2273E502700552265 /* LiveStreaming.storyboard */, 8BC751D4273E502700552265 /* LiveStreaming.swift */, ); @@ -1059,7 +1085,6 @@ ABB399EB2DF9605B00AF5033 /* Multipath */, ABB399F02DF9605B00AF5033 /* Simulcast */, DDFD50242C205BDF0050B344 /* TransparentRender */, - E7883AE32B074746003CCF44 /* FaceCapture */, E726BFFD2A96FD3A006870E2 /* AudioWaveform */, E726BFF42A949F70006870E2 /* AudioRouterPlayer */, E7163F7B2964149800EBBD55 /* ARKit */, @@ -1106,6 +1131,7 @@ E7163F8929651D8900EBBD55 /* AR.scnassets */, 03D13BDD2448758B00B599B3 /* Info.plist */, 03D13BCF2448758900B599B3 /* AppDelegate.swift */, + BDD65ABDD9A5B24C43142396 /* SceneDelegate.swift */, 03D13BD82448758B00B599B3 /* Assets.xcassets */, 03D13BDA2448758B00B599B3 /* LaunchScreen.storyboard */, 033A9F2C252D737900BC26E1 /* Localizable.strings */, @@ -1135,7 +1161,6 @@ isa = PBXGroup; children = ( DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */, - DD6ED7EE2CB69FFB007B3B5D /* LocalCompositeGraph.strings */, DD6ED7EF2CB69FFB007B3B5D /* LocalCompositeGraph.swift */, ); path = LocalCompositeGraph; @@ -1145,7 +1170,6 @@ isa = PBXGroup; children = ( DDCED02F2CBAA093004B1C9D /* RtePlayer.storyboard */, - DDCED0312CBAA093004B1C9D /* RtePlayer.strings */, DDCED0322CBAA093004B1C9D /* RtePlayer.swift */, DD71821D2CBBDF1000523123 /* PlayerObserver.swift */, ); @@ -1161,10 +1185,18 @@ path = TransparentRender; sourceTree = ""; }; + E6A5AF82C3B9A87CA080B694 /* SenseRender */ = { + isa = PBXGroup; + children = ( + 7DB5758B38DCA9DE302090EB /* SenseBeautyRender.h */, + 72375779AFD65DD129FAF105 /* SenseBeautyRender.m */, + ); + path = SenseRender; + sourceTree = ""; + }; E7163F7B2964149800EBBD55 /* ARKit */ = { isa = PBXGroup; children = ( - E7163F7C2964149800EBBD55 /* ARKit.strings */, E7163F7E2964149800EBBD55 /* ARKit.swift */, E7163F7F2964149800EBBD55 /* ARKit.storyboard */, ); @@ -1191,7 +1223,6 @@ E726BFF42A949F70006870E2 /* AudioRouterPlayer */ = { isa = PBXGroup; children = ( - E726BFF52A949F70006870E2 /* AudioRouterPlayer.strings */, E726BFF72A949F70006870E2 /* AudioRouterPlayer.swift */, E726BFF82A949F70006870E2 /* AudioRouterPlayer.storyboard */, ); @@ -1201,7 +1232,6 @@ E726BFFD2A96FD3A006870E2 /* AudioWaveform */ = { isa = PBXGroup; children = ( - E726BFFE2A96FD3A006870E2 /* AudioWaveform.strings */, E726C0002A96FD3A006870E2 /* AudioWaveform.storyboard */, E726C0022A96FD3A006870E2 /* AudioWaveform.swift */, ); @@ -1216,7 +1246,6 @@ F728B9D22CA295D7007813BB /* PIPBaseViewController.swift */, F728B9D62CA295D7007813BB /* PixelBufferPIPViewController */, E728B84428B5FFCB00674A4A /* PictureInPicture.swift */, - F728B9DF2CA29625007813BB /* PictureInPicture.strings */, ); path = PictureInPicture; sourceTree = ""; @@ -1224,7 +1253,6 @@ E728B85228B86B0700674A4A /* CustomVideoSourcePushMulti */ = { isa = PBXGroup; children = ( - E728B85328B86B0700674A4A /* CustomVideoSourcePushMulti.strings */, E728B85528B86B0700674A4A /* CustomVideoSourcePushMulti.storyboard */, E728B85728B86B0700674A4A /* CustomVideoSourcePushMulti.swift */, ); @@ -1235,7 +1263,6 @@ E74788A629C7FB6800CD7415 /* JoinChannelVideo(Recorder) */ = { isa = PBXGroup; children = ( - E74788A729C7FB6800CD7415 /* JoinChannelVideoRecorder.strings */, E74788A929C7FB6800CD7415 /* JoinChannelVideoRecorder.storyboard */, E74788AB29C7FB6800CD7415 /* JoinChannelVideoRecorder.swift */, ); @@ -1267,6 +1294,7 @@ isa = PBXGroup; children = ( E76347BD2AAEF4AF005D130F /* FURender */, + E6A5AF82C3B9A87CA080B694 /* SenseRender */, ); path = Render; sourceTree = ""; @@ -1283,27 +1311,15 @@ E77D54C128F55E9100D51C1E /* JoinChannelVideo(Token) */ = { isa = PBXGroup; children = ( - E77D54C228F55E9100D51C1E /* JoinChannelVideoToken.strings */, E77D54C428F55E9100D51C1E /* JoinChannelVideoToken.storyboard */, E77D54C628F55E9100D51C1E /* JoinChannelVideoToken.swift */, ); path = "JoinChannelVideo(Token)"; sourceTree = ""; }; - E7883AE32B074746003CCF44 /* FaceCapture */ = { - isa = PBXGroup; - children = ( - E7883AE42B074746003CCF44 /* FaceCapture.strings */, - E7883AE62B074746003CCF44 /* FaceCapture.storyboard */, - E7883AE82B074746003CCF44 /* FaceCapture.swift */, - ); - path = FaceCapture; - sourceTree = ""; - }; E7899BD62861673600851463 /* CreateDataStream */ = { isa = PBXGroup; children = ( - E7899BD72861673600851463 /* CreateDataStream.strings */, E7899BD92861673600851463 /* CreateDataStream.storyboard */, E7899BDB2861673600851463 /* CreateDataStream.swift */, ); @@ -1313,7 +1329,6 @@ E7A49CB229011E7500F06DD4 /* MutliCamera */ = { isa = PBXGroup; children = ( - E7A49CB329011E7500F06DD4 /* MutliCamera.strings */, E7A49CB529011E7500F06DD4 /* MutliCamera.storyboard */, E7A49CB729011E7500F06DD4 /* MutliCamera.swift */, ); @@ -1327,6 +1342,7 @@ E7A49CEC29029E0000F06DD4 /* ThirdBeautify.storyboard */, E7A49CEE29029E0000F06DD4 /* ThirdBeautify.swift */, E7A49CEF29029E0000F06DD4 /* FUBeautify */, + 047A5C4316EA23A3E1583092 /* SenseBeautify */, ); path = ThirdBeautify; sourceTree = ""; @@ -1335,7 +1351,6 @@ isa = PBXGroup; children = ( E7A49D0029029E1900F06DD4 /* Manager */, - E7A49CF029029E0000F06DD4 /* FUBeautify.strings */, E7A49CF429029E0000F06DD4 /* FUBeautify.storyboard */, E7A49CF229029E0000F06DD4 /* FUBeautifyVC.h */, E7A49CF729029E0000F06DD4 /* FUBeautifyVC.m */, @@ -1352,6 +1367,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 = ( @@ -1368,7 +1411,6 @@ F73479222DF5CA820030C0CE /* AgoraBeauty.storyboard */, F734791D2DF5CA0D0030C0CE /* AgoraBeauty.swift */, F73479252DF5CAEB0030C0CE /* AgoraBeautyManager.swift */, - F734791F2DF5CA140030C0CE /* AgoraBeauty.strings */, ); path = AgoraBeauty; sourceTree = ""; @@ -1450,7 +1492,8 @@ 03D13BCA2448758900B599B3 /* Resources */, 1B6F6CF9B678035E221EAFDE /* [CP] Embed Pods Frameworks */, 0339BEBA25205B80007D4FDD /* Embed App Extensions */, - E76F80122AF0A7A200CCB9D6 /* ShellScript */, + E76F80122AF0A7A200CCB9D6 /* Copy Optional Beauty Resources */, + DB2429109D6D95174925E0A2 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1538,7 +1581,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,39 +1594,27 @@ 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 */, 033A9F66252D8B2A00BC26E1 /* VoiceChanger.storyboard in Resources */, E7A49CB929011E7500F06DD4 /* MutliCamera.storyboard in Resources */, @@ -1594,33 +1624,27 @@ 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 */, 03D13BD72448758900B599B3 /* Main.storyboard in Resources */, F728B9D72CA295D7007813BB /* PictureInPicture.storyboard in Resources */, + E986961B46231F8163D6456D /* SenseBeautify.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1642,14 +1666,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"; @@ -1721,8 +1741,26 @@ 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; }; - E76F80122AF0A7A200CCB9D6 /* ShellScript */ = { + 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 /* Copy Optional Beauty Resources */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -1730,13 +1768,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 */ @@ -1778,7 +1817,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 +1826,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 */, @@ -1826,7 +1865,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 */, @@ -1838,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 */, @@ -1856,6 +1895,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; }; @@ -2049,14 +2100,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 +2136,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 = ""; @@ -2118,6 +2154,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 = ( @@ -2136,14 +2181,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 +2191,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 +2213,7 @@ children = ( ABB399E82DF9605B00AF5033 /* Multipath.storyboard */, ABB399F62DF9608700AF5033 /* en */, + ABB90AD02DF72E8D007FDDEB /* zh-Hans */, ); name = Multipath.storyboard; sourceTree = ""; @@ -2192,98 +2223,74 @@ children = ( ABB399ED2DF9605B00AF5033 /* Simulcast.storyboard */, AB8473EC2DF960D800B4DB73 /* en */, + ABB90ACE2DF72E2F007FDDEB /* zh-Hans */, ); name = Simulcast.storyboard; sourceTree = ""; }; - DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */ = { + BA7993DC75813765F1AF9A03 /* SenseBeautify.storyboard */ = { isa = PBXVariantGroup; children = ( - DD6ED7EB2CB69FFB007B3B5D /* Base */, + 0621233B3B3FD80035BA62CB /* Base */, + B5BED75DAF7F75615310244E /* zh-Hans */, ); - name = LocalCompositeGraph.storyboard; + name = SenseBeautify.storyboard; sourceTree = ""; }; - DD6ED7EE2CB69FFB007B3B5D /* LocalCompositeGraph.strings */ = { + DD6ED7EC2CB69FFB007B3B5D /* LocalCompositeGraph.storyboard */ = { isa = PBXVariantGroup; children = ( + DD6ED7EB2CB69FFB007B3B5D /* Base */, 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; + name = RtePlayer.storyboard; sourceTree = ""; }; - E7163F7C2964149800EBBD55 /* ARKit.strings */ = { + DDFD50272C205CF40050B344 /* TransparentRender.storyboard */ = { isa = PBXVariantGroup; children = ( - E7163F7D2964149800EBBD55 /* zh-Hans */, + F7FD50272C205CF40050B344 /* Base */, + F7FD50282C205CF40050B344 /* zh-Hans */, ); - name = ARKit.strings; + name = TransparentRender.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 +2300,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 */, - ); - 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 */ = { + E7A49CB02900FC6400F06DD4 /* KtvCopyrightMusic.storyboard */ = { isa = PBXVariantGroup; children = ( - E7A49CB429011E7500F06DD4 /* zh-Hans */, + F7A49CB02900FC6400F06DD4 /* Base */, + F7A49CB12900FC6400F06DD4 /* zh-Hans */, ); - name = MutliCamera.strings; + name = KtvCopyrightMusic.storyboard; sourceTree = ""; }; E7A49CB529011E7500F06DD4 /* MutliCamera.storyboard */ = { isa = PBXVariantGroup; children = ( E7A49CB629011E7500F06DD4 /* Base */, + E7A49CB429011E7500F06DD4 /* zh-Hans */, ); name = MutliCamera.storyboard; sourceTree = ""; @@ -2382,34 +2354,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 = ""; @@ -2430,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", @@ -2439,7 +2399,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 = ""; @@ -2464,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", @@ -2473,7 +2435,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 = ""; @@ -2537,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; @@ -2593,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; @@ -2620,11 +2586,13 @@ 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", ); - MARKETING_VERSION = 4.6.4; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2691,11 +2659,13 @@ 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", ); - MARKETING_VERSION = 4.6.4; + MARKETING_VERSION = 4.7.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2763,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", @@ -2800,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/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 60144479b..dcc11f7d7 100644 --- a/iOS/APIExample/APIExample/Common/AgoraExtension.swift +++ b/iOS/APIExample/APIExample/Common/AgoraExtension.swift @@ -94,13 +94,17 @@ extension AgoraAudioScenario { switch self { case .default: return "Default".localized case .gameStreaming: return "Game Streaming".localized + case .chatRoom: return "Chat Room".localized + case .chorus: return "Chorus".localized + case .meeting: return "Meeting".localized + case .aiClient: return "AI Client".localized default: return "\(self.rawValue)" } } static func allValues() -> [AgoraAudioScenario] { - return [.default, .gameStreaming] + return [.default, .gameStreaming, .chatRoom, .chorus, .meeting, .aiClient] } } @@ -265,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/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/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/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/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/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/AgoraBeauty.swift b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift index e5e1abb37..37ea1d52a 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift @@ -139,7 +139,7 @@ class AgoraBeautyMain: BaseViewController { }) beautyManager = AgoraBeautyManager(agoraKit: agoraKit) - beautyManager.beautyMakeupStyle = "default makeup style".localized + beautyManager.beautyMakeupStyle = "Makeup-Young" beautyManager.makeUpEnable = false setupUI() } @@ -154,8 +154,8 @@ class AgoraBeautyMain: BaseViewController { LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) } } - AgoraRtcEngineKit.destroy() beautyManager.destory() + AgoraRtcEngineKit.destroy() } } @@ -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) @@ -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 } } @@ -714,6 +728,6 @@ extension AgoraBeautyMain { @objc func beautyShapeSegmentAction(_ view: UISegmentedControl) { let style = AgoraFaceShapeStyle(rawValue: UInt(view.selectedSegmentIndex)) ?? .female - beautyManager.beautyShapeStyle = style == .male ? "Male Template Title".localized : "Female Template Title".localized + beautyManager.beautyShapeStyle = style == .male ? "Beauty-Ordinary" : "Beauty-Basic" } } diff --git a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift index e1c28dd2f..83f4569ef 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift @@ -15,8 +15,8 @@ class AgoraBeautyManager { private lazy var faceshapeOption = AgoraFaceShapeBeautyOptions() private var styleParam: [String : Any] = ["enable_mu": false] private var m_bundle_copied = false; - let beauty_material_path: String = NSHomeDirectory() + "/Documents/beauty_material.bundle"; - let m_current_material_name = "beauty_material_v2.0.0"; + let beauty_material_path: String = NSHomeDirectory() + "/Documents/AgoraBeautyMaterial.bundle"; + let m_current_material_name = "beauty_material_functional"; init(agoraKit: AgoraRtcEngineKit? = nil) { self.agoraKit = agoraKit @@ -35,23 +35,36 @@ class AgoraBeautyManager { extension: "clear_vision", enabled: true, sourceType: .primaryCamera) - copyBeautyBundle() + guard copyBeautyBundle() else { + agoraKit?.enableExtension(withVendor: "agora_video_filters_clear_vision", + extension: "clear_vision", + enabled: false, + sourceType: .primaryCamera) + return + } let path = beauty_material_path + "/" + m_current_material_name; videoEffectObject = agoraKit?.createVideoEffectObject(bundlePath: path, sourceType: AgoraMediaSourceType.primaryCamera) agoraKit?.setParameters("{\"rtc.video.yuvconverter_enable_hardware_buffer\":true}") } - private func copyBeautyBundle() { + private func copyBeautyBundle() -> Bool { if (m_bundle_copied) { - return + return true + } + guard let bundle_path = Bundle.main.path(forResource: "AgoraBeautyMaterial", ofType: "bundle") else { + return false } - let bundle_path = Bundle.main.path(forResource: "beauty_material", ofType: "bundle") if FileManager.default.fileExists(atPath: beauty_material_path) { try? FileManager.default.removeItem(atPath: beauty_material_path) } - try? FileManager.default.copyItem(atPath: bundle_path!, toPath: beauty_material_path) + do { + try FileManager.default.copyItem(atPath: bundle_path, toPath: beauty_material_path) + } catch { + return false + } m_bundle_copied = true + return true } private func addEffect(node : UInt) { @@ -147,6 +160,7 @@ class AgoraBeautyManager { } set { guard let effectObj = videoEffectObject else { return } + effectObj.setVideoEffectStringParam(option: "beauty_effect_option", key: "whiten_lut_path", stringValue: "") effectObj.setVideoEffectFloatParam(option: "beauty_effect_option", key: "lightness", floatValue: newValue) } } @@ -623,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", @@ -638,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", @@ -656,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, @@ -699,23 +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/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/AgoraBeauty.strings b/iOS/APIExample/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/AgoraBeauty.strings index 8a23ea288..06e8117f4 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/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/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings index 7a6635018..e8247a5fd 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/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/AudioRouterPlayer/zh-Hans.lproj/AudioRouterPlayer.strings b/iOS/APIExample/APIExample/Examples/Advanced/AudioRouterPlayer/zh-Hans.lproj/AudioRouterPlayer.strings index bed444285..9bcd3686e 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" = "选择播放器类型(第三方/AVPlayer)"; -/* 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/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/AudioWaveform/zh-Hans.lproj/AudioWaveform.strings b/iOS/APIExample/APIExample/Examples/Advanced/AudioWaveform/zh-Hans.lproj/AudioWaveform.strings index f8e59a150..4f1ed31b1 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/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/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/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/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/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/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/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/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/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/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/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomPcmAudioSource/zh-Hans.lproj/CustomPcmAudioSource.strings index b51ebfa81..8c060f683 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" = "推送 PCM"; -/* 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/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/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/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/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/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/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings b/iOS/APIExample/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings index 282d5d185..e90ce64d9 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" = "创建视频轨道"; -/* 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" = "销毁视频轨道"; + +/* 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" = "创建编码视频轨道"; + +/* Element = "button"; normalTitle = "Button"; ObjectID = "zBJ-mw-TwL"; */ +"zBJ-mw-TwL.normalTitle" = "创建编码视频轨道"; 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 435447453..000000000 --- a/iOS/APIExample/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings +++ /dev/null @@ -1,21 +0,0 @@ - -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; - -/* 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"; 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..b99c1f66a 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" = "离开 Ex 频道"; + +/* 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/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..2dbcd0155 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.h @@ -0,0 +1,34 @@ +// +// 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; + +- (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 new file mode 100644 index 000000000..dfe69a25e --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/SenseBeautify/Manager/VideoProcessingManager.m @@ -0,0 +1,238 @@ +// +// 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 +} + +- (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; + + 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 && _outputPixelBuffer && _outputCVTexture) { + int _cacheW = (int)CVPixelBufferGetWidth(_outputPixelBuffer); + int _cacheH = (int)CVPixelBufferGetHeight(_outputPixelBuffer); + if (_cacheH != heigh || _cacheW != width) { + [self releaseOutputResources]; + } + } + 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)); + 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 ?: pixelBuffer; +} + +- (void)dealloc { + [self cleanup]; +} + +- (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..94c18f5b2 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 { @@ -31,12 +30,16 @@ 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) + } 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/Examples/Advanced/ThirdBeautify/zh-Hans.lproj/ThirdBeautify.strings b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/zh-Hans.lproj/ThirdBeautify.strings index 80f461572..b39a62b03 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/zh-Hans.lproj/ThirdBeautify.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/ThirdBeautify/zh-Hans.lproj/ThirdBeautify.strings @@ -1,9 +1,8 @@ +/* Element = "button"; configuration.title = "join"; ObjectID = "5sl-Dr-7g0"; */ +"5sl-Dr-7g0.configuration.title" = "加入频道"; -/* Class = "UIButton"; configuration.title = "join"; ObjectID = "5sl-Dr-7g0"; */ -"5sl-Dr-7g0.configuration.title" = "join"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "5sl-Dr-7g0"; */ +"5sl-Dr-7g0.normalTitle" = "加入频道"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "5sl-Dr-7g0"; */ -"5sl-Dr-7g0.normalTitle" = "Join Channel"; - -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "iNv-W6-5tF"; */ -"iNv-W6-5tF.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "iNv-W6-5tF"; */ +"iNv-W6-5tF.placeholder" = "输入频道名"; diff --git a/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/TransparentRender.storyboard b/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/Base.lproj/TransparentRender.storyboard similarity index 100% rename from iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/TransparentRender.storyboard rename to iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/Base.lproj/TransparentRender.storyboard diff --git a/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/TransparentRender.swift b/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/TransparentRender.swift index d33d5da1c..8bfcddba1 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/TransparentRender.swift +++ b/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/TransparentRender.swift @@ -180,9 +180,7 @@ extension TransparentRenderViewController: AgoraRtcMediaPlayerVideoFrameDelegate newVideoFrame.format = 12 newVideoFrame.textureBuf = videoFrame.pixelBuffer newVideoFrame.rotation = videoFrame.rotation - let pb = videoFrame.pixelBuffer newVideoFrame.alphaStitchMode = .alphaStitchRight - let _ = agoraKit.pushExternalVideoFrame(newVideoFrame) -// LogUtils.log(message: "pushExternalVideoFrame: \(ret)", level: .info) + let _ = agoraKit.pushExternalVideoFrame(newVideoFrame, videoTrackId: 0) } } diff --git a/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/zh-Hans.lproj/TransparentRender.strings b/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/zh-Hans.lproj/TransparentRender.strings new file mode 100644 index 000000000..98be60d09 --- /dev/null +++ b/iOS/APIExample/APIExample/Examples/Advanced/TransparentRender/zh-Hans.lproj/TransparentRender.strings @@ -0,0 +1,5 @@ +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "QL3-Pi-NBk"; */ +"QL3-Pi-NBk.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "Y8V-3i-Fuo"; */ +"Y8V-3i-Fuo.normalTitle" = "加入频道"; 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/VideoChat/zh-Hans.lproj/VideoChat.strings b/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/zh-Hans.lproj/VideoChat.strings index 76c027b27..c629a9409 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/zh-Hans.lproj/VideoChat.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/VideoChat/zh-Hans.lproj/VideoChat.strings @@ -1,15 +1,15 @@ /* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "02W-eg-RJa"; */ -"02W-eg-RJa.placeholder" = "Enter channel name"; +"02W-eg-RJa.placeholder" = "输入频道名"; /* Class = "UIButton"; normalTitle = "Join"; ObjectID = "Ct9-Ey-CTP"; */ -"Ct9-Ey-CTP.normalTitle" = "Join Channel"; +"Ct9-Ey-CTP.normalTitle" = "加入频道"; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "ZJg-We-nJi"; */ -"ZJg-We-nJi.normalTitle" = "Button"; +"ZJg-We-nJi.normalTitle" = "视频方向"; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "m05-DP-KFr"; */ -"m05-DP-KFr.normalTitle" = "Button"; +"m05-DP-KFr.normalTitle" = "分辨率"; /* Class = "UIButton"; normalTitle = "Button"; ObjectID = "oui-fu-yvV"; */ -"oui-fu-yvV.normalTitle" = "Button"; +"oui-fu-yvV.normalTitle" = "帧率"; 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/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings b/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings index b745afdb8..b29e2ac79 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings @@ -1,9 +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 = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; - -/* Class = "UIButton"; normalTitle = "Send metadata"; ObjectID = "ucb-dZ-rMR"; */ -"ucb-dZ-rMR.normalTitle" = "Send SEI Message"; +/* Element = "button"; normalTitle = "Send metadata"; ObjectID = "ucb-dZ-rMR"; */ +"ucb-dZ-rMR.normalTitle" = "发送SEI消息"; 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/VideoProcess/zh-Hans.lproj/VideoProcess.strings b/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/zh-Hans.lproj/VideoProcess.strings index 8a23ea288..c57155ed2 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/zh-Hans.lproj/VideoProcess.strings +++ b/iOS/APIExample/APIExample/Examples/Advanced/VideoProcess/zh-Hans.lproj/VideoProcess.strings @@ -1,77 +1,77 @@ +/* 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 = "label"; text = "Color Enhancement"; ObjectID = "5ZW-F5-wP6"; */ +"5ZW-F5-wP6.text" = "色彩增强"; -/* Class = "UILabel"; text = "Color Enhancement"; ObjectID = "5ZW-F5-wP6"; */ -"5ZW-F5-wP6.text" = "Color Enhancement"; +/* Element = "button"; configuration.title = "Face Shape"; ObjectID = "8Rv-nY-rrP"; */ +"8Rv-nY-rrP.configuration.title" = "脸型"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "Bla-ht-C3j"; */ -"Bla-ht-C3j.title" = "Face Beautify"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "8Rv-nY-rrP"; */ +"8Rv-nY-rrP.normalTitle" = "脸型"; -/* Class = "UILabel"; text = "Video Denoise"; ObjectID = "Cdl-89-b0G"; */ -"Cdl-89-b0G.text" = "Video Denoise"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "Bla-ht-C3j"; */ +"Bla-ht-C3j.title" = "美颜"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "Er0-F3-9xn"; */ -"Er0-F3-9xn.title" = "Face Beautify"; +/* Element = "label"; text = "Video Denoise"; ObjectID = "Cdl-89-b0G"; */ +"Cdl-89-b0G.text" = "视频降噪"; -/* Class = "UILabel"; text = "Skin Protect"; ObjectID = "J82-5l-UTm"; */ -"J82-5l-UTm.text" = "Skin Protect"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "Er0-F3-9xn"; */ +"Er0-F3-9xn.title" = "美颜"; -/* Class = "UILabel"; text = "Virtual Background"; ObjectID = "OLG-wB-DHO"; */ -"OLG-wB-DHO.text" = "Virtual Background"; +/* Element = "label"; text = "Skin Protect"; ObjectID = "J82-5l-UTm"; */ +"J82-5l-UTm.text" = "肤色保护"; -/* Class = "UILabel"; text = "Sharpness"; ObjectID = "PkA-h3-Mk4"; */ -"PkA-h3-Mk4.text" = "Sharpness"; +/* Element = "label"; text = "Virtual Background"; ObjectID = "OLG-wB-DHO"; */ +"OLG-wB-DHO.text" = "虚拟背景"; -/* Class = "UISegmentedControl"; Qhf-Ob-NYA.segmentTitles[0] = "Image"; ObjectID = "Qhf-Ob-NYA"; */ -"Qhf-Ob-NYA.segmentTitles[0]" = "Image"; +/* Element = "label"; text = "Sharpness"; ObjectID = "PkA-h3-Mk4"; */ +"PkA-h3-Mk4.text" = "锐利"; -/* Class = "UISegmentedControl"; Qhf-Ob-NYA.segmentTitles[1] = "Color"; ObjectID = "Qhf-Ob-NYA"; */ -"Qhf-Ob-NYA.segmentTitles[1]" = "Color"; +/* Element = "segmentedControl"; segmentTitles[n] = "Image"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[0]" = "图片"; -/* Class = "UISegmentedControl"; Qhf-Ob-NYA.segmentTitles[2] = "Blur"; ObjectID = "Qhf-Ob-NYA"; */ -"Qhf-Ob-NYA.segmentTitles[2]" = "Blur"; +/* Element = "segmentedControl"; segmentTitles[n] = "Color"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[1]" = "颜色"; -"Qhf-Ob-NYA.segmentTitles[3]" = "Video"; +/* Element = "segmentedControl"; segmentTitles[n] = "Blur"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[2]" = "毛玻璃"; -/* Class = "UILabel"; text = "Low light Enhancement"; ObjectID = "RiO-Eg-x0D"; */ -"RiO-Eg-x0D.text" = "Low Light Enhancement"; +/* Element = "segmentedControl"; segmentTitles[n] = "Video"; ObjectID = "Qhf-Ob-NYA"; */ +"Qhf-Ob-NYA.segmentTitles[3]" = "视频"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "W0Y-Vm-X3Y"; */ -"W0Y-Vm-X3Y.title" = "Face Beautify"; +/* Element = "label"; text = "Low light Enhancement"; ObjectID = "RiO-Eg-x0D"; */ +"RiO-Eg-x0D.text" = "暗光增强"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "ZSH-Zd-njr"; */ -"ZSH-Zd-njr.placeholder" = "Enter channel name"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "W0Y-Vm-X3Y"; */ +"W0Y-Vm-X3Y.title" = "美颜"; -/* Class = "UILabel"; text = "Smoothness"; ObjectID = "bOk-r8-Pn2"; */ -"bOk-r8-Pn2.text" = "Smoothness"; +/* Element = "textField"; placeholder = "Enter channel name"; ObjectID = "ZSH-Zd-njr"; */ +"ZSH-Zd-njr.placeholder" = "请输入频道名"; -/* Class = "UILabel"; text = "Redness"; ObjectID = "f1C-7c-nGG"; */ -"f1C-7c-nGG.text" = "Redness"; +/* Element = "label"; text = "Smoothness"; ObjectID = "bOk-r8-Pn2"; */ +"bOk-r8-Pn2.text" = "平滑"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "haN-2j-4b9"; */ -"haN-2j-4b9.title" = "Face Beautify"; +/* Element = "label"; text = "Redness"; ObjectID = "f1C-7c-nGG"; */ +"f1C-7c-nGG.text" = "红润"; -/* Class = "UIViewController"; title = "Simple Filter"; ObjectID = "jKA-9Y-IUF"; */ -"jKA-9Y-IUF.title" = "Simple Filter"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "haN-2j-4b9"; */ +"haN-2j-4b9.title" = "美颜"; -/* Class = "UILabel"; text = "Lightening"; ObjectID = "k4I-na-mXN"; */ -"k4I-na-mXN.text" = "Lightening"; +/* Element = "viewController"; title = "Simple Filter"; ObjectID = "jKA-9Y-IUF"; */ +"jKA-9Y-IUF.title" = "简易滤镜"; -/* Class = "UILabel"; text = "Strength"; ObjectID = "mLC-kh-u4J"; */ -"mLC-kh-u4J.text" = "Strength"; +/* Element = "label"; text = "Lightening"; ObjectID = "k4I-na-mXN"; */ +"k4I-na-mXN.text" = "美白"; -/* Class = "UISwitch"; title = "Face Beautify"; ObjectID = "qcX-s3-yAy"; */ -"qcX-s3-yAy.title" = "Face Beautify"; +/* Element = "label"; text = "Strength"; ObjectID = "mLC-kh-u4J"; */ +"mLC-kh-u4J.text" = "强度"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "xcq-px-Ijl"; */ -"xcq-px-Ijl.normalTitle" = "Join"; +/* Element = "label"; text = "Whitening"; ObjectID = "nVP-eV-sen"; */ +"nVP-eV-sen.text" = "提亮"; -/* Class = "UIButton"; normalTitle = "Face Shape"; ObjectID = "8Rv-nY-rrP"; */ -"8Rv-nY-rrP.normalTitle" = "Face Shape"; +/* Element = "switch"; title = "Face Beautify"; ObjectID = "qcX-s3-yAy"; */ +"qcX-s3-yAy.title" = "美颜"; -/* 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/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/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/iOS/APIExample/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings index 5a897d8e1..ed1deb75a 100644 --- a/iOS/APIExample/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings +++ b/iOS/APIExample/APIExample/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" = "AI 音色调节"; + +/* 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/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/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/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings index 9907f01d4..033d18f2f 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings @@ -1,35 +1,50 @@ +/* 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 = "textField"; placeholder = "Relay Channel"; ObjectID = "C45-aE-S4p"; */ +"C45-aE-S4p.placeholder" = "转发频道"; -/* 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 = "button"; configuration.title = "Start"; ObjectID = "QU9-AY-IUX"; */ +"QU9-AY-IUX.configuration.title" = "开始"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ -"iZP-Ce-Oxt.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "QU9-AY-IUX"; */ +"QU9-AY-IUX.normalTitle" = "开始"; -/* 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 = "button"; configuration.title = "Button"; ObjectID = "ZSJ-cf-f63"; */ +"ZSJ-cf-f63.configuration.title" = "音频使用场景"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; +/* Element = "button"; normalTitle = "Button"; ObjectID = "ZSJ-cf-f63"; */ +"ZSJ-cf-f63.normalTitle" = "音频使用场景"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ -"myR-6e-1zj.normalTitle" = "Button"; +/* Element = "label"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频参数配置"; -"C45-aE-S4p.placeholder" = "Forward Channel"; +/* 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/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(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings index f0cf2e852..8b541a0bb 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings @@ -1,21 +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 = "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"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "视频实时通话"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "VpM-9W-auG"; */ -"VpM-9W-auG.normalTitle" = "Button"; +/* Element = "button"; configuration.title = "Leave"; ObjectID = "vu7-G0-QbZ"; */ +"vu7-G0-QbZ.configuration.title" = "离开频道"; -/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ -"kbN-ZR-nNn.normalTitle" = "Join Channel"; - -/* 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 = "button"; normalTitle = "Button"; ObjectID = "vu7-G0-QbZ"; */ +"vu7-G0-QbZ.normalTitle" = "离开频道"; 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(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings index 96895fcd3..0911bee15 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings @@ -1,25 +1,23 @@ +/* Element = "textField"; placeholder = "Please the input APPID"; ObjectID = "2mk-8S-AvJ"; */ +"2mk-8S-AvJ.placeholder" = "请输入 App ID"; -/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ -"GWc-L5-fZV.placeholder" = "Enter channel name"; +/* Element = "textField"; placeholder = "Please the input Token"; ObjectID = "5la-r7-Gna"; */ +"5la-r7-Gna.placeholder" = "请输入 Token"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "Iy0-Dq-h5x"; */ -"Iy0-Dq-h5x.title" = "Join Channel"; +/* Element = "label"; text = "Token"; ObjectID = "9wv-tJ-ykp"; */ +"9wv-tJ-ykp.text" = "Token"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "VpM-9W-auG"; */ -"VpM-9W-auG.normalTitle" = "Button"; +/* 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 Channel"; +/* Element = "label"; text = "AppID"; ObjectID = "aN0-QQ-AHf"; */ +"aN0-QQ-AHf.text" = "AppID"; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "kf0-3f-UI5"; */ -"kf0-3f-UI5.normalTitle" = "Button"; +/* Element = "button"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ -"p70-sh-D1h.title" = "Real-time Video Call"; +/* Element = "viewController"; 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"; - -"5la-r7-Gna.placeholder" = "Enter Token"; - -"2mk-8S-AvJ.placeholder" = "Enter APPID"; +/* Element = "label"; text = "Channel Name"; ObjectID = "vai-NU-aAm"; */ +"vai-NU-aAm.text" = "频道名"; 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/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings index abda7bf13..6a9d55c5c 100644 --- a/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings +++ b/iOS/APIExample/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.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 Channel"; - -/* 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" = "Real-time 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/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/Resources/beauty_material.bundle/beauty_material_v1.0.0/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/config.json deleted file mode 100644 index fc1b02889..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "engine_option": { - "identifier": "agora_beauty_material_100", - "version": "1.0.0", - "aspect": "16:9", - "preferred_fps": 24 - }, - "user_interface_option": { - "模板女": "muban_female/", - "模板男": "muban_male/", - "学姐妆": "xuejie/", - "学妹妆": "xuemei/", - "原生": "yuansheng/", - "冷白": "lengbai/", - "嫩白": "nenbai/" - }, - "beauty_config" : "模板男", - "makeup_config" : "学姐妆", - "filter_config" : "冷白" -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/lengbai/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/lengbai/config.json deleted file mode 100644 index 036b67a4b..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/lengbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/lengbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_female/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_female/config.json deleted file mode 100644 index c21c243f6..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_female/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.5, - "lightness": 0.7, - "redness": 0.5, - "contrast": 2, - "sharpness": 0.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 0, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 400, - "strength": 20 - } - ] -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_male/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_male/config.json deleted file mode 100644 index 0def94a5a..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_male/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.5, - "lightness": 0.7, - "redness": 0.5, - "contrast": 1, - "sharpness": 0.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 1, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 400, - "strength": 20 - } - ] -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/nenbai/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/nenbai/config.json deleted file mode 100644 index 6ddbae760..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/nenbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/nenbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuejie/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuejie/config.json deleted file mode 100644 index b561c7c41..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuejie/config.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 1, - "browColor": 1, - "browStrength": 1.0, - "lashStyle": 1, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 1, - "shadowStrength": 1.0, - "pupilStyle": 1, - "pupilStrength": 1.0, - "blushStyle": 1, - "blushColor": 1, - "blushStrength": 1.0, - "lipStyle": 1, - "lipColor": 1, - "lipStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuejie.png", - "strength": 0.5 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuemei/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuemei/config.json deleted file mode 100644 index 4ee5c17d0..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuemei/config.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 2, - "browColor": 2, - "browStrength": 1.0, - "lashStyle": 5, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 6, - "shadowStrength": 1.0, - "pupilStyle": 2, - "pupilStrength": 1.0, - "blushStyle": 2, - "blushColor": 5, - "blushStrength": 1.0, - "lipStyle": 2, - "lipColor": 5, - "lipStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuemei.png", - "strength": 0.5 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/yuansheng/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/yuansheng/config.json deleted file mode 100644 index 75b424363..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/yuansheng/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/yuansheng32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/config.json deleted file mode 100644 index 99991247f..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "engine_option": { - "identifier": "agora_beauty_material_200", - "version": "2.0.0", - "aspect": "16:9", - "preferred_fps": 24 - }, - "user_interface_option": { - "模板女": "muban_female/", - "模板男": "muban_male/", - "模板自然": "muban_natural/", - "学姐妆": "xuejie/", - "学妹妆": "xuemei/", - "原生": "yuansheng/", - "冷白": "lengbai/", - "嫩白": "nenbai/" - }, - "beauty_config" : "模板女", - "makeup_config" : "学妹妆", - "filter_config" : "原生" -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/lengbai/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/lengbai/config.json deleted file mode 100644 index 036b67a4b..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/lengbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/lengbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_female/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_female/config.json deleted file mode 100644 index fddcdd89d..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_female/config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.9, - "lightness": 0.9, - "redness": 1.0, - "sharpness": 1.0, - "contrast_strength": 1.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 0, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 302, - "strength": 10 - }, - { - "area": 304, - "strength": 50 - }, - { - "area": 305, - "strength": 50 - }, - { - "area": 306, - "strength": 30 - }, - { - "area": 400, - "strength": 20 - }, - { - "area": 402, - "strength": 30 - } - ], - "face_buffing_option": { - "eye_pouch": 0.5, - "brighten_eye": 0.9, - "nasolabial_fold": 0.7, - "whiten_teeth": 0.7 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_male/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_male/config.json deleted file mode 100644 index bd7f9419c..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_male/config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.6, - "lightness": 0.3, - "redness": 0.2, - "sharpness": 0.5, - "contrast_strength": 1.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 1, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 302, - "strength": 10 - }, - { - "area": 304, - "strength": 50 - }, - { - "area": 305, - "strength": 50 - }, - { - "area": 306, - "strength": 30 - }, - { - "area": 400, - "strength": 20 - }, - { - "area": 402, - "strength": 30 - } - ], - "face_buffing_option": { - "eye_pouch": 0.7, - "brighten_eye": 0.9, - "nasolabial_fold": 0.0, - "whiten_teeth": 0.5 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_natural/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_natural/config.json deleted file mode 100644 index 92c68bc0c..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_natural/config.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.5, - "lightness": 0.7, - "redness": 0.5, - "contrast": 1, - "sharpness": 0.0, - "contrast_strength": 0.5 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 2, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 302, - "strength": 10 - }, - { - "area": 304, - "strength": 50 - }, - { - "area": 305, - "strength": 50 - }, - { - "area": 306, - "strength": 30 - }, - { - "area": 400, - "strength": 20 - }, - { - "area": 402, - "strength": 30 - } - ], - "face_buffing_option": { - "eye_pouch": 0.5, - "brighten_eye": 0.9, - "nasolabial_fold": 0.5, - "whiten_teeth": 0.5 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/nenbai/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/nenbai/config.json deleted file mode 100644 index 6ddbae760..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/nenbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/nenbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuejie/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuejie/config.json deleted file mode 100644 index 8fdde0cae..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuejie/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 1, - "browColor": 1, - "browStrength": 1.0, - "lashStyle": 1, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 1, - "shadowStrength": 1.0, - "pupilStyle": 1, - "pupilStrength": 1.0, - "blushStyle": 1, - "blushColor": 1, - "blushStrength": 1.0, - "lipStyle": 1, - "lipColor": 1, - "lipStrength": 1.0, - "wocanStyle": 1, - "wocanStrength": 1.0, - "facialStyle": 4, - "facialStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuejie.png", - "strength": 0.5 - }, - "style_makeup_option" : { - "styleIntensity" : 0.95 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuemei/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuemei/config.json deleted file mode 100644 index 5b80c816f..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuemei/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 2, - "browColor": 2, - "browStrength": 1.0, - "lashStyle": 5, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 6, - "shadowStrength": 1.0, - "pupilStyle": 2, - "pupilStrength": 1.0, - "blushStyle": 2, - "blushColor": 5, - "blushStrength": 1.0, - "lipStyle": 2, - "lipColor": 5, - "lipStrength": 1.0, - "wocanStyle": 3, - "wocanStrength": 1.0, - "facialStyle": 5, - "facialStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuemei.png", - "strength": 0.5 - }, - "style_makeup_option" : { - "styleIntensity" : 0.95 - } -} diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/yuansheng/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/yuansheng/config.json deleted file mode 100644 index 75b424363..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/yuansheng/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/yuansheng32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/config.json deleted file mode 100644 index 38de795ee..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "engine_option": { - "identifier": "agora_beauty_material_200", - "version": "2.0.0", - "aspect": "16:9", - "preferred_fps": 24, - "encrypted": true - }, - "user_interface_option": { - "模板女": "muban_female/", - "模板男": "muban_male/", - "模板自然": "muban_natural/", - "学姐妆": "xuejie/", - "学妹妆": "xuemei/", - "原生": "yuansheng/", - "冷白": "lengbai/", - "嫩白": "nenbai/" - }, - "beauty_config" : "模板女", - "makeup_config" : "学妹妆", - "filter_config" : "原生" -} \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/lengbai/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/lengbai/config.json deleted file mode 100644 index c589fa40f..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/lengbai/config.json +++ /dev/null @@ -1,2 +0,0 @@ -÷«�êÓ__ nסT s³��@ôܰÍ‘¥¼D ´ú¤Iޤ ¬膗™„ -{¾Шê\'Ql.ù÷¶]Io¢ø¤­¾TJpQwÜ\ÔIÃq<ºE†༊ 2ý̓Àϊ-ӳî‘“b7Cõ|ŸÆ \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_female/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_female/config.json deleted file mode 100644 index 3ea30a93b..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_female/config.json and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_male/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_male/config.json deleted file mode 100644 index 4e0c6a9d4..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_male/config.json and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_natural/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_natural/config.json deleted file mode 100644 index 9930f8b85..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_natural/config.json and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/nenbai/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/nenbai/config.json deleted file mode 100644 index 06062b263..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/nenbai/config.json +++ /dev/null @@ -1,2 +0,0 @@ -÷«�êÓ__ nסT s³��@ôܰÍ‘¥¼D ´ú¤Iޤ ¬膗™„ -{¾Шê\'Ql.ù÷¶]Ioé]:V®z†ÕöXá‚ ܑ7ñWÓ óVÍê3ë�$­Gð·’Ös՝¯šF \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuejie/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuejie/config.json deleted file mode 100644 index f9522caf5..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuejie/config.json and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuemei/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuemei/config.json deleted file mode 100644 index ab3f807dc..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuemei/config.json and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/yuansheng/config.json b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/yuansheng/config.json deleted file mode 100644 index 02c750cec..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/yuansheng/config.json +++ /dev/null @@ -1,2 +0,0 @@ -÷«�êÓ__ nסT s³��@ôܰÍ‘¥¼D ´ú¤Iޤ ¬膗™„ -{¾Шê\'Ql.ù÷¶]Io‡8³Â¾£ÜVê J™ˆڏP×è®ß̏Ä!ç`nÚÊrYŠôÛìø ��ò \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.png deleted file mode 100644 index f32018f8f..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.png deleted file mode 100644 index fda6f2845..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.png deleted file mode 100644 index f1135bf0c..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.png deleted file mode 100644 index 45a1e8b9e..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.png deleted file mode 100644 index 08b3adfb3..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.txt deleted file mode 100644 index ff7914b65..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.txt +++ /dev/null @@ -1,26 +0,0 @@ -30.8540 54.4375 -45.0110 38.4755 -63.9685 28.0135 -85.7750 25.6785 -107.477 27.7120 -128.663 31.9345 -149.564 38.2160 -49.9845 50.6535 -69.6945 47.2265 -89.8265 46.6565 -109.794 48.3315 -129.602 51.2215 -149.033 54.8515 -146.121 54.4940 -133.676 38.9200 -116.063 28.5060 -95.5600 25.7185 -74.7805 26.9705 -54.3160 30.3035 -34.4040 35.6200 -128.359 50.6790 -109.779 47.2090 -90.8395 46.3015 -71.8655 47.1395 -52.9890 49.3300 -34.3750 52.0495 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.png deleted file mode 100644 index db0be064d..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.txt deleted file mode 100644 index ff7914b65..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.txt +++ /dev/null @@ -1,26 +0,0 @@ -30.8540 54.4375 -45.0110 38.4755 -63.9685 28.0135 -85.7750 25.6785 -107.477 27.7120 -128.663 31.9345 -149.564 38.2160 -49.9845 50.6535 -69.6945 47.2265 -89.8265 46.6565 -109.794 48.3315 -129.602 51.2215 -149.033 54.8515 -146.121 54.4940 -133.676 38.9200 -116.063 28.5060 -95.5600 25.7185 -74.7805 26.9705 -54.3160 30.3035 -34.4040 35.6200 -128.359 50.6790 -109.779 47.2090 -90.8395 46.3015 -71.8655 47.1395 -52.9890 49.3300 -34.3750 52.0495 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.png deleted file mode 100644 index c4a4103ff..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.txt deleted file mode 100644 index ff7914b65..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.txt +++ /dev/null @@ -1,26 +0,0 @@ -30.8540 54.4375 -45.0110 38.4755 -63.9685 28.0135 -85.7750 25.6785 -107.477 27.7120 -128.663 31.9345 -149.564 38.2160 -49.9845 50.6535 -69.6945 47.2265 -89.8265 46.6565 -109.794 48.3315 -129.602 51.2215 -149.033 54.8515 -146.121 54.4940 -133.676 38.9200 -116.063 28.5060 -95.5600 25.7185 -74.7805 26.9705 -54.3160 30.3035 -34.4040 35.6200 -128.359 50.6790 -109.779 47.2090 -90.8395 46.3015 -71.8655 47.1395 -52.9890 49.3300 -34.3750 52.0495 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.png deleted file mode 100644 index d726fd35b..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.png deleted file mode 100644 index 5d94417e5..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.png deleted file mode 100644 index 398757f71..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.png deleted file mode 100644 index aa638875e..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadowscreen006.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadowscreen006.png deleted file mode 100644 index cb18c3fbd..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadowscreen006.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.png deleted file mode 100644 index 93abbc15b..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.png deleted file mode 100644 index 97ba1d83e..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.png deleted file mode 100644 index d0c86269f..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.png deleted file mode 100644 index 7d13c2af8..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.txt deleted file mode 100644 index 03bd13173..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/lengbai32.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/lengbai32.png deleted file mode 100644 index cfa92e494..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/lengbai32.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/nenbai32.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/nenbai32.png deleted file mode 100644 index dcadb4af9..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/nenbai32.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/xuejie.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/xuejie.png deleted file mode 100644 index e55c1e367..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/xuejie.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/xuemei.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/xuemei.png deleted file mode 100644 index 5554af2c1..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/xuemei.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/yuansheng32.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/yuansheng32.png deleted file mode 100644 index 6b9714cb7..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/filters/yuansheng32.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.png deleted file mode 100644 index dfe1b80e8..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.txt deleted file mode 100644 index f697d5f34..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.png deleted file mode 100644 index 0b6321931..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.txt deleted file mode 100644 index f697d5f34..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.png deleted file mode 100644 index 93e1f83fd..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.txt deleted file mode 100644 index f697d5f34..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.png deleted file mode 100644 index 6a620aaca..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.txt deleted file mode 100644 index f697d5f34..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.png deleted file mode 100644 index b20527b07..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.png deleted file mode 100644 index bfad3f3db..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.png deleted file mode 100644 index 24a48252c..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.png b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.png deleted file mode 100644 index 1580a77fa..000000000 Binary files a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.png and /dev/null differ diff --git a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.txt b/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.txt deleted file mode 100644 index d244d17c1..000000000 --- a/iOS/APIExample/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file 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 773972d74..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 { @@ -31,76 +30,63 @@ 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"), - 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: "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: "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"), ]) ] - 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/APIExample/zh-Hans.lproj/Localizable.strings b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings index 2b8e5dc67..089d691cb 100644 --- a/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings +++ b/iOS/APIExample/APIExample/zh-Hans.lproj/Localizable.strings @@ -6,268 +6,289 @@ Copyright © 2020 Agora Corp. All rights reserved. */ -"Join a channel (Token)" = "Real-time Video Call/Live Streaming (Token Authentication)"; -"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"; -"RTMP Streaming" = "RTMP Streaming"; -"Media Injection" = "Media Injection"; -"Video Metadata" = "SEI Message"; -"Voice Changer" = "Voice Effects/Audio Effects"; -"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)" = "Multiple Custom Video Sources"; -"Custom Video Render" = "Custom Video Render(Metal)"; -"Quick Switch Channel" = "Quick Channel Switch"; -"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 Sharing"; -"Local Composite Graph" = "Local Video Composition"; -"Super Resolution" = "Super Resolution"; -"Media Channel Relay" = "Cross-Channel Stream Relay"; -"Set Resolution" = "Set Video Resolution"; -"Set Fps" = "Set Video Frame Rate"; -"Set Orientation" = "Set Video Orientation"; -"Set Chat Beautifier" = "Set Chat Voice Beautifier"; -"Set Timbre Transformation" = "Set Timbre Transformation"; -"Set Voice Changer" = "Set Voice Changer Effects"; -"Set Style Transformation" = "Set Music Style Effects"; -"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 Configuration"; -"Set Audio Scenario" = "Set Audio Usage Scenario"; -"Default" = "Default"; -"Music Standard" = "Standard Music"; -"Music Standard Stereo" = "Standard Stereo Music"; -"Music High Quality" = "High Quality Music"; -"Music High Quality Stereo" = "High Quality Stereo Music"; -"Speech Standard" = "Standard Speech"; -"Chat Room Gaming" = "Entertainment Chat Room"; -"Education" = "Education"; -"Game Streaming" = "High Quality Chat Room"; -"High Defination" = "High Quality Music"; -"Chat Room Entertainment" = "Gaming Chat"; -"Show Room" = "Show Room"; -"Cancel" = "Cancel"; -"Sure" = "Confirm"; -"Stop" = "Stop"; -"Off" = "Original Voice"; -"FemaleFresh" = "Chat Voice Beautifier: Fresh(Female)"; -"FemaleVitality" = "Chat Voice Beautifier: Energetic(Female)"; -"MaleMagnetic" = "Chat Voice Beautifier: Magnetic(Male)"; -"Vigorous" = "Full-bodied"; -"Deep" = "Deep"; -"Mellow" = "Mellow"; -"Falsetto" = "Falsetto"; -"Full" = "Full"; -"Clear" = "Clear"; -"Resounding" = "Resounding"; -"Ringing" = "Ringing"; -"Spacial" = "Spacious"; -"Ethereal" = "Ethereal"; -"Old Man" = "Old Boy"; -"Baby Boy" = "Little Boy"; -"Baby Girl" = "Little Girl"; -"ZhuBaJie" = "Zhu Bajie"; -"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旁路推流"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source" = "视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Video Data" = "视频裸数据"; +"Simple Filter Extension" = "插件系统"; +"Pre-call Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Local Composite Graph" = "本地合图"; +"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" = "Concert"; -"Vocal Concert(Old Version)" = "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" = "Recording Studio"; -"Studio(Old Version)" = "Recording Studio(Old Version)"; -"Phonograph" = "Phonograph"; -"Virtual Stereo" = "Virtual Stereo"; -"Dry Level" = "Original Sound Intensity"; -"Wet Level" = "Early Reflection Signal Intensity"; -"Room Size" = "Room Size"; -"Wet Delay" = "Early Reflection Signal Delay"; -"Strength" = "Reverb Sustain Intensity"; -"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"; -"Resolution" = "Resolution"; -"Frame Rate" = "Frame Rate"; -"Orientation" = "Video Orientation"; -"Broadcaster" = "Broadcaster"; -"Audience" = "Audience"; -"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"; -"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"; -"Content Inspect" = "Video Content Moderation"; -"Video Neutral" = "Video Normal"; -"Video Sexy" = "Video Sexy"; -"Video Porn" = "Video Pornographic"; -"SwitchCamera" = "Switch Camera"; -"remote user has not joined, and cannot take a screenshot" = "Remote user hasn't joined, cannot take screenshot"; -"Screenshot successful" = "Screenshot successful"; -"Picture In Picture(require iOS15)" = "Picture In Picture(requires iOS15)"; -"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+)" = "Multiple Cameras(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 Effects"; -"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 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 the same Apple account, after copying on Mac it will automatically sync to other devices with same account. Phone can directly tap input box to paste.\n\n II: Use https://cl1p.net/ online clipboard:\n\n1. Enter a URL starting with cl1p.net. Example: cl1p.net/uqztgjnqcalmd\n\n2. Paste anything you want.\n\n3. On another device, enter the same URL to 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" = "选择角色"; +"Send Data Stream" = "发送数据流"; +"Raw Audio Data" = "音频裸数据"; +"Screen Share + Camera" = "屏幕共享 + 摄像头"; +"please input channel name!" = "请输入频道名!"; +"Video Process" = "视频增强"; +"Beauty 2.0" = "美颜 2.0"; +"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" = "画中画"; +"please input Token!" = "请输入Token"; +"please input AppId!" = "请输入APPID"; +"stopMicrophoneRecording" = "停止麦克风采集"; +"KTV Copyright Music" = "KTV版权音乐"; +"Multi Camera" = "多路摄像头"; +"Multipath" = "网络传输高级功能"; +"Simulcast" = "同源多流"; +"Open Rear Camera" = "打开后摄像头"; +"Close Rear Camera" = "关闭后摄像头"; +"Third-party Beauty" = "第三方美颜"; +"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方法"; +"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" = "Once enabled, this function 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"; -"Join Channel" = "Join Channel"; -"Leave Channel" = "Leave Channel"; -"PIP Support iOS 15+" = "Picture in Picture only supports iOS 15+"; -"Red" = "Red"; -"Blue" = "Blue"; -"Pink" = "Pink"; -"Purple" = "Purple"; -"Yellow" = "Yellow"; -"preload Channel" = "Preload"; -"cancel preload" = "Cancel Preload"; -"Audio Router(Third Party Player)" = "Audio Router(Third Party Player)"; -"Player Type(ijkplayer/avplayer)" = "Player Selection(ijkplayer/native)"; -"Audio Waveform" = "Audio Spectrum"; +"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" = "停止录制"; +"Join Channel" = "加入频道"; +"Leave Channel" = "离开频道"; +"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" = "Set whether to enable AI noise reduction and set noise reduction mode."; +"Set AINS Mode" = "设置是否开启 AI 降噪功能并设置降噪模式。"; -"AGGRESSIVE" = "Aggressive Noise Reduction Mode"; -"BALANCED" = "(Default)Balanced Noise Reduction Mode"; -"ULTRALOWLATENCY" = "Ultra-low Latency Strong Noise Reduction Mode"; -"Face Capture" = "Face Capture Data"; -"Please contact Agora customer service to obtain a face capture certificate" = "Please contact Agora customer service to obtain face capture certificate"; -"Second camera" = "Second Camera"; -"This device does not support Center Stage" = "This device does not support Center Stage"; -"Wide" = "Wide"; -"Length Wide" = "Ultra Wide"; -"Telephoto" = "Telephoto"; -"Front camera" = "Front Camera"; -"Rear camera" = "Rear Camera"; -"The camera has changed:" = "Camera has changed:"; -"CameraStabilizationMode" = "Stabilization:"; +"AGGRESSIVE" = "强降噪模式"; +"BALANCED" = "(默认)均衡降噪模式"; +"ULTRALOWLATENCY" = "低延时强降噪模式"; +"Second camera" = "第二路摄像头"; +"This device does not support Center Stage" = "此设备不支持 Center Stage"; +"Wide" = "广角"; +"Length Wide" = "超广角"; +"Telephoto" = "长焦"; +"Front camera" = "前置"; +"Rear camera" = "后置"; +"The camera has changed:" = "摄像头已改变:"; +"CameraStabilizationMode" = "防抖:"; -"Start Audio Mixing" = "Start Audio Forwarding"; -"Stop Audio Mixing" = "Stop Audio Forwarding"; +"Start Audio Mixing" = "开启音频转发"; +"Stop Audio Mixing" = "停止音频转发"; -"General"="General Scenario"; -"Meeting"="Meeting Scenario"; -"1v1"="1v1 Video Call"; -"Live Show"="Live Show"; +"General"= "通用场景"; +"Meeting"= "会议场景"; +"Chat Room" = "语聊房"; +"Chorus" = "实时合唱"; +"AI Client" = "对话式 AI 客户端"; +"1v1"= "1v1 视频通话"; +"Live Show"= "秀场直播"; -"Set Voice AI Tuner" = "Set AI Voice Tuner"; -"Mature Male Voice" = "Mature Male Voice"; -"Fresh Male Voice" = "Fresh Male Voice"; -"Elegant Female Voice" = "Elegant Female Voice"; -"Sweet Female Voice" = "Sweet Female Voice"; -"Warm Male Singing Voice" = "Warm Male Singing Voice"; -"Gentle Female Singing Voice" = "Gentle Female Singing Voice"; -"Husky Male Singing Voice" = "Husky Male Singing Voice"; -"Warm Elegant Female Singing Voice" = "Warm Elegant Female Singing Voice"; -"Powerful Male Singing Voice" = "Powerful Male Singing Voice"; -"Dreamy Female Singing Voice" = "Dreamy Female Singing Voice"; +"Set Voice AI Tuner" = "设置AI调音器"; +"Mature Male Voice" = "大叔声"; +"Fresh Male Voice" = "清新男音"; +"Elegant Female Voice" = "御姐音"; +"Sweet Female Voice" = "萝莉音"; +"Warm Male Singing Voice" = "暖男歌声"; +"Gentle Female Singing Voice" = "温柔女歌声"; +"Husky Male Singing Voice" = "烟嗓叔音歌声"; +"Warm Elegant Female Singing Voice" = "温暖御姐歌声"; +"Powerful Male Singing Voice" = "力量男歌声"; +"Dreamy Female Singing Voice" = "梦幻女歌声"; -"Transparent Render"="Transparent Rendering"; +"Transparent Rendering"= "透明渲染"; -"URL Streaming(RTE Player)"="URL Streaming(RTE Player)"; +"URL Streaming (RTE Player)"= "URL拉流(RTE Player)"; -"Makeup Enable" = "Makeup Switch"; -"Eyebrow Style" = "Eyebrow Style"; -"Eyebrow Color" = "Eyebrow Color"; -"Eyebrow Strength" = "Eyebrow Intensity"; -"Eyelash Style" = "Eyelash Style"; -"Eyelash Color" = "Eyelash Color"; -"Eyelash Strength" = "Eyelash Intensity"; -"Eyeshadow Style" = "Eyeshadow Style"; -"Eyeshadow Color" = "Eyeshadow Color"; -"Eyeshadow Strength" = "Eyeshadow Intensity"; -"Pupil Style" = "Pupil Style"; -"Pupil Color" = "Pupil Color"; -"Pupil Strength" = "Pupil Intensity"; -"Blush Style" = "Blush Style"; -"Blush Color" = "Blush Color"; -"Blush Strength" = "Blush Intensity"; -"Lip Style" = "Lipstick Style"; -"Lip Color" = "Lipstick Color"; -"Lip Strength" = "Lipstick Intensity"; +"Makeup Enable" = "美妆开关"; +"Eyebrow Style" = "眉毛类型"; +"Eyebrow Color" = "眉毛颜色"; +"Eyebrow Strength" = "眉毛强度"; +"Eyelash Style" = "眼睫毛类型"; +"Eyelash Color" = "眼睫毛颜色"; +"Eyelash Strength" = "眼睫毛强度"; +"Eyeshadow Style" = "眼影类型"; +"Eyeshadow Color" = "眼影颜色"; +"Eyeshadow Strength" = "眼影强度"; +"Pupil Style" = "瞳孔类型"; +"Pupil Color" = "瞳孔颜色"; +"Pupil Strength" = "瞳孔强度"; +"Blush Style" = "腮红类型"; +"Blush Color" = "腮红颜色"; +"Blush Strength" = "腮红强度"; +"Lip Style" = "唇彩类型"; +"Lip Color" = "唇彩颜色"; +"Lip Strength" = "唇彩强度"; -"Eyebrow Style Off" = "Off"; -"Eyebrow Style Type 1" = "Type 1"; -"Eyebrow Style Type 2" = "Type 2"; +"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" = "None"; -"Eyebrow Color Black" = "Type 1"; -"Eyebrow Color Brown" = "Type 2"; +"Eyebrow Color None" = "无"; +"Eyebrow Color Black" = "类型1"; +"Eyebrow Color Brown" = "类型2"; -"Blush Color None" = "None"; -"Blush Color Shade 1" = "Shade 1"; -"Blush Color Shade 2" = "Shade 2"; -"Blush Color Shade 3" = "Shade 3"; -"Blush Color Shade 4" = "Shade 4"; -"Blush Color Shade 5" = "Shade 5"; +"Blush Color None" = "无"; +"Blush Color Shade 1" = "1号色"; +"Blush Color Shade 2" = "2号色"; +"Blush Color Shade 3" = "3号色"; +"Blush Color Shade 4" = "4号色"; +"Blush Color Shade 5" = "5号色"; -"Face Shape Enable" = "Face Shape Switch"; -"Face Shape Gender" = "Gender"; -"Face Shape Gender Male" = "Male"; -"Face Shape Gender Female" = "Female"; -"Face Shape Intensity" = "Intensity"; +"Face Shape Enable" = "美型开关"; +"Face Shape Gender" = "性别"; +"Face Shape Gender Male" = "男性"; +"Face Shape Gender Female" = "女性"; +"Face Shape Intensity" = "强度"; -"Face Shape Area Head Scale" = "Head Scale"; -"Face Shape Area Forehead" = "Forehead"; -"Face Shape Area Face Contour" = "Face Contour"; -"Face Shape Area Face Length" = "Face Length"; -"Face Shape Area Face Width" = "Face Width"; -"Face Shape Area Cheek Bone" = "Cheekbone"; -"Face Shape Area Cheek" = "Cheek"; -"Face Shape Area Chin" = "Chin"; -"Face Shape Area Eye Scale" = "Eye Scale"; -"Face Shape Area Nose Length" = "Nose Length"; -"Face Shape Area Nose Width" = "Nose Width"; -"Face Shape Area Mouth Scale" = "Mouth Scale"; +"Face Shape Area Head Scale" = "头部比例"; +"Face Shape Area Forehead" = "额头"; +"Face Shape Area Face Contour" = "脸型轮廓"; +"Face Shape Area Face Length" = "脸长"; +"Face Shape Area Face Width" = "脸宽"; +"Face Shape Area Cheek Bone" = "颧骨"; +"Face Shape Area Cheek" = "脸颊"; +"Face Shape Area Chin" = "下巴"; +"Face Shape Area Eye Scale" = "眼睛比例"; +"Face Shape Area Nose Length" = "鼻子长度"; +"Face Shape Area Nose Width" = "鼻子宽度"; +"Face Shape Area Mouth Scale" = "嘴巴比例"; -"Live Channel Name" = "Live Channel Name"; +"Live Channel Name" = "直播频道名"; "default makeup style" = "学妹妆"; +"Wocan" = "卧蚕"; +"Wocan Strength" = "卧蚕强度"; + +"Male Template Title" = "模板男"; +"Female Template Title" = "模板女"; + +"3D Voice" = "3D 人声"; +"Pitch Correction" = "音高校正"; +"Neutral" = "中性"; +"Sweet" = "甜美"; +"Solid" = "浑厚"; +"Bass" = "低沉"; +"Post Capture" = "采集后"; +"PreRenderer" = "渲染前"; +"PreEncoder" = "编码前"; +"Post Capture Origin" = "原始采集后"; +"Player Type(thirdPartyPlayer/avplayer)" = "播放器类型(第三方播放器/AVPlayer)"; +"The camera has changed" = "摄像头已切换"; +"Maxmum 3 layers can be selected" = "最多可选择 3 层"; +"No remote user" = "暂无远端用户"; +"mute" = "静音"; +"Voice Blur" = "人声模糊"; +"Airborne Simulation" = "空气传播模拟"; +"Attenuation" = "衰减"; +"Set Pitch Correction" = "设置音高校正"; +"Set Voice Conversion" = "设置音色转换"; +"VideoMetadata" = "视频元数据"; +"ARKit" = "ARKit"; diff --git a/iOS/APIExample/APIExample/zh-Hans.lproj/Main.strings b/iOS/APIExample/APIExample/zh-Hans.lproj/Main.strings index 32a694f8d..6a011217f 100644 --- a/iOS/APIExample/APIExample/zh-Hans.lproj/Main.strings +++ b/iOS/APIExample/APIExample/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 = "textField"; placeholder = "Please enter"; ObjectID = "AaW-co-Otq"; */ +"AaW-co-Otq.placeholder" = "请输入"; + +/* 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 = "navigationItem"; title = "Join Channel"; ObjectID = "O4p-Hd-Lr5"; */ +"O4p-Hd-Lr5.title" = "加入频道"; + +/* Element = "label"; text = "Label"; ObjectID = "Ruy-K9-CLg"; */ +"Ruy-K9-CLg.text" = "标签"; -/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "O4p-Hd-Lr5"; */ -"O4p-Hd-Lr5.title" = "Join Channel"; +/* Element = "navigationItem"; title = "Agora API Example"; ObjectID = "Ygc-Og-WKK"; */ +"Ygc-Og-WKK.title" = "Agora API 示例"; -/* Class = "UILabel"; text = "Label"; ObjectID = "Ruy-K9-CLg"; */ -"Ruy-K9-CLg.text" = "Label"; +/* Element = "label"; text = "Label"; ObjectID = "d6u-rE-3US"; */ +"d6u-rE-3US.text" = "标签"; -/* Class = "UINavigationItem"; title = "Agora API Example"; ObjectID = "Ygc-Og-WKK"; */ -"Ygc-Og-WKK.title" = "Agora API Example"; +/* Element = "label"; text = "Label"; ObjectID = "dW4-HZ-foF"; */ +"dW4-HZ-foF.text" = "标签"; -/* Class = "UIViewController"; title = "Log View Controller"; ObjectID = "ekP-NH-UjU"; */ -"ekP-NH-UjU.title" = "Log View Controller"; +/* Element = "viewController"; title = "Log View Controller"; ObjectID = "ekP-NH-UjU"; */ +"ekP-NH-UjU.title" = "日志"; -/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "iib-g5-GmB"; */ -"iib-g5-GmB.title" = "Join Channel Video"; +/* Element = "viewController"; title = "Join Channel Video"; ObjectID = "iib-g5-GmB"; */ +"iib-g5-GmB.title" = "视频实时通话"; -/* 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" = "加入频道"; diff --git a/iOS/APIExample/ARCHITECTURE.md b/iOS/APIExample/ARCHITECTURE.md index d5c1be3a5..511c72ccc 100644 --- a/iOS/APIExample/ARCHITECTURE.md +++ b/iOS/APIExample/ARCHITECTURE.md @@ -1,5 +1,24 @@ # ARCHITECTURE.md — APIExample +## Application Lifecycle + +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 +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 | @@ -7,7 +26,7 @@ | 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 | @@ -32,19 +51,18 @@ | ScreenShare | `Examples/Advanced/ScreenShare/ScreenShare.swift` | `startScreenCapture()`, `updateScreenCapture()`, `stopScreenCapture()`, `setScreenCaptureScenario()` | Screen capture and sharing via ReplayKit extension | | LocalCompositeGraph | `Examples/Advanced/LocalCompositeGraph/LocalCompositeGraph.swift` | `startLocalVideoTranscoder()`, `startCameraCapture()`, `startScreenCapture()`, `enableVirtualBackground()` | Composite multiple video sources locally before publishing | | VideoProcess | `Examples/Advanced/VideoProcess/VideoProcess.swift` | `setBeautyEffectOptions()`, `enableVirtualBackground()`, `enableExtension()` | Built-in beauty, virtual background, and video enhancement | -| AgoraBeauty | `Examples/Advanced/AgoraBeauty/AgoraBeauty.swift` | `enableExtension()`, `enableVirtualBackground()` | Agora beauty extension with virtual background | -| RhythmPlayer | `Examples/Advanced/RhythmPlayer/RhythmPlayer.swift` | `startRhythmPlayer()`, `stopRhythmPlayer()` | Play metronome-style rhythm audio | +| AgoraBeauty | `Examples/Advanced/AgoraBeauty/AgoraBeauty.swift` | `createVideoEffectObject()`, `setVideoEffectStringParam()`, `enableVirtualBackground()` | Agora beauty extension with virtual background | +| RhythmPlayer (hidden) | `Examples/Advanced/RhythmPlayer/RhythmPlayer.swift` | `startRhythmPlayer()`, `stopRhythmPlayer()` | Source retained for reference; hidden from the menu because the Rhythm Player APIs are deprecated since RTC SDK 4.6.0 | | CreateDataStream | `Examples/Advanced/CreateDataStream/CreateDataStream.swift` | `createDataStream()`, `sendStreamMessage()` | Create and send data stream messages between users | | MediaChannelRelay | `Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift` | `startOrUpdateChannelMediaRelay()`, `stopChannelMediaRelay()`, `pauseAllChannelMediaRelay()`, `resumeAllChannelMediaRelay()` | Relay media stream to multiple destination channels | | SpatialAudio | `Examples/Advanced/SpatialAudio/SpatialAudio.swift` | `createMediaPlayer()`, `updateChannel()` | 3D spatial audio with media player integration | | 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 | -| 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 | @@ -57,12 +75,12 @@ 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) └── APIExample/ ├── AppDelegate.swift + ├── SceneDelegate.swift # Window owned by the application scene ├── ViewController.swift # Root menu controller — MenuItem registration lives here ├── Info.plist ├── APIExample.entitlements @@ -97,42 +115,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" - │ ├── RhythmPlayer/ # "Rhythm Player" - │ ├── CreateDataStream/ # "Create Data Stream" + │ ├── AgoraBeauty/ # "Beauty 2.0" + │ ├── RhythmPlayer/ # Hidden — APIs deprecated since RTC SDK 4.6.0 + │ ├── 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" - │ ├── FaceCapture/ # "Face Capture" - │ ├── TransparentRender/ # "Transparent Render" + │ ├── TransparentRender/ # "Transparent Rendering" │ ├── RtePlayer/ # "URL Streaming (RTE Player)" │ ├── Simulcast/ # "Simulcast" │ ├── Multipath/ # "Multipath" @@ -167,9 +184,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 @@ -193,17 +212,22 @@ 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 -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/iOS/APIExample/Podfile b/iOS/APIExample/Podfile index 094f92e43..225da67a9 100644 --- a/iOS/APIExample/Podfile +++ b/iOS/APIExample/Podfile @@ -1,11 +1,10 @@ -# 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' def common_pods - pod 'AgoraRtcEngine_iOS', '4.6.4' - # pod 'sdk', :path => 'sdk.podspec' + # pod 'AgoraRtcEngine_iOS', '4.7.0' + pod 'sdk', :path => 'sdk.podspec' end target 'APIExample' do @@ -22,6 +21,7 @@ target 'APIExample' do # pod 'SwiftLint', '~> 0.53.0' pod 'SnapKit', '~> 5.7.0' + # pod 'SenseLib', :path => 'sense.podspec' # pod 'fuLib', :path => 'fu.podspec' end @@ -37,5 +37,18 @@ 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 + +# 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 acefe6e3c..7dc358493 100644 --- a/iOS/APIExample/README.md +++ b/iOS/APIExample/README.md @@ -9,11 +9,12 @@ 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 -- Xcode 13.0+ +- 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 @@ -23,14 +24,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 ``` @@ -71,15 +68,32 @@ To build and run the sample application, get an App Id: You are all set. Now connect your iPhone or iPad device and run the project. +### Agora Beauty 2.0 Resources + +The Agora Beauty 2.0 material package is not included in this repository. Before building locally, obtain `AgoraBeautyMaterial.bundle.zip` from Agora technical support and extract it to `iOS/APIExample/APIExample/Resources/`. + +The extracted resources should have the following structure: + +```text +iOS/APIExample/APIExample/Resources/ +└── AgoraBeautyMaterial.bundle/ + ├── beauty_material_functional/ + │ ├── config.json + │ └── ... + └── ... +``` + +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) - 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/README.zh.md b/iOS/APIExample/README.zh.md new file mode 100644 index 000000000..de12e402f --- /dev/null +++ b/iOS/APIExample/README.zh.md @@ -0,0 +1,82 @@ +# 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/api-reference/faq/integration/local_network_privacy) + +## 环境准备 + +- 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 模拟器 + +## 快速开始 + +以下步骤说明如何安装依赖、配置项目并运行示例。 + +### 安装依赖 + +进入当前示例目录,然后安装 CocoaPods 依赖。CocoaPods 的安装方式请参考[官方指南](https://guides.cocoapods.org/using/getting-started.html)。 + +```bash +cd iOS/APIExample +pod install +``` + +确认 `APIExample.xcworkspace` 已成功生成。 + +### 获取 App ID + +1. 登录 [Agora Console](https://console.agora.io/) 并创建项目。 +2. 在项目详情页复制 App ID。 +3. 使用 Xcode 打开 `APIExample.xcworkspace`。 +4. 编辑 `APIExample/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 美颜 2.0 资源 + +Agora 美颜 2.0 资源包不包含在仓库中。本地构建前,从 Agora 技术支持获取 `AgoraBeautyMaterial.bundle.zip`,并解压到 `iOS/APIExample/APIExample/Resources/`。 + +解压后的资源目录结构如下: + +```text +iOS/APIExample/APIExample/Resources/ +└── AgoraBeautyMaterial.bundle/ + ├── beauty_material_functional/ + │ ├── config.json + │ └── ... + └── ... +``` + +解压后的 bundle 已被 Git 忽略。 + +## 联系我们 + +- 常见问题请查看 [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) +- 完整 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/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_build.sh b/iOS/APIExample/cloud_build.sh index 0135ea8ba..5067a3cf3 100755 --- a/iOS/APIExample/cloud_build.sh +++ b/iOS/APIExample/cloud_build.sh @@ -11,6 +11,19 @@ fi cd ${PROJECT_PATH} +curl --fail --location -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -o AgoraBeautyMaterial.bundle.zip "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/AgoraBeautyMaterial.bundle.zip" || exit 1 +rm -rf APIExample/Resources/AgoraBeautyMaterial.bundle +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 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 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 @@ -22,6 +35,7 @@ test -f FULib/authpack.h || exit 1 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" diff --git a/iOS/APIExample/cloud_project.sh b/iOS/APIExample/cloud_project.sh deleted file mode 100755 index f18177c28..000000000 --- a/iOS/APIExample/cloud_project.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env sh - -PROJECT_PATH=$PWD - -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -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 -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 'bytedEffect'#pod 'bytedEffect'#g" Podfile -perl -i -pe "s#\#pod 'fuLib'#pod 'fuLib'#g" Podfile - -pod install --repo-update || exit 1 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/iOS/ARCHITECTURE.md b/iOS/ARCHITECTURE.md index 6648ac527..db5a88948 100644 --- a/iOS/ARCHITECTURE.md +++ b/iOS/ARCHITECTURE.md @@ -1,10 +1,34 @@ # 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 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` | + +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. + +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 diff --git a/macOS/.agent/skills/review-case/SKILL.md b/macOS/.agent/skills/review-case/SKILL.md deleted file mode 100644 index 92d813ec2..000000000 --- a/macOS/.agent/skills/review-case/SKILL.md +++ /dev/null @@ -1,380 +0,0 @@ ---- -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 - version: 1.0.0 - platform: macOS ---- - -# Review Case Skill — macOS - -## 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 `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)") - } -} - -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) { ... } -} -``` - ---- - -### 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 diff --git a/macOS/.agent/skills/review-case/references/correct-lifecycle.swift b/macOS/.agent/skills/review-case/references/correct-lifecycle.swift deleted file mode 100644 index 853b274f6..000000000 --- a/macOS/.agent/skills/review-case/references/correct-lifecycle.swift +++ /dev/null @@ -1,20 +0,0 @@ -// Correct Engine Lifecycle Pattern - -override func viewDidLoad() { - super.viewDidLoad() - initializeAgoraEngine() // Create once -} - -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() -} diff --git a/macOS/.agent/skills/upsert-case/references/example-template.swift b/macOS/.agent/skills/upsert-case/references/example-template.swift deleted file mode 100644 index 1c0c8327e..000000000 --- a/macOS/.agent/skills/upsert-case/references/example-template.swift +++ /dev/null @@ -1,62 +0,0 @@ -import Cocoa -import AgoraRtcKit -import AGEVideoLayout - -class Main: BaseViewController { - - var agoraKit: AgoraRtcEngineKit! - var remoteUid: UInt = 0 - - @IBOutlet weak var Container: AGEVideoContainer! - - // MARK: - Lifecycle - - 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() - } - - func joinChannel() { - let token = KeyCenter.Token(channelName: "test") - agoraKit.joinChannel(byToken: token, channelName: "test", info: nil, uid: 0) - } - - func leaveChannel() { - agoraKit.leaveChannel(nil) - agoraKit.destroy() - } -} - -// MARK: - AgoraRtcEngineDelegate - -extension Main: AgoraRtcEngineDelegate { - - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { - // Handle join success - } - - 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 - } -} diff --git a/macOS/.agent/skills/upsert-case/references/lifecycle-pattern.swift b/macOS/.agent/skills/upsert-case/references/lifecycle-pattern.swift deleted file mode 100644 index 3821b2c3f..000000000 --- a/macOS/.agent/skills/upsert-case/references/lifecycle-pattern.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Proper Engine Lifecycle Pattern - -// Create -let config = AgoraRtcEngineConfig() -config.appId = KeyCenter.AppId -agoraKit = AgoraRtcEngineKit(config: config, delegate: self) - -// Use -agoraKit.joinChannel(byToken: token, channelName: channel, info: nil, uid: 0) - -// Cleanup -agoraKit.leaveChannel(nil) -agoraKit.destroy() 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/skills/review-case/SKILL.md b/macOS/.agents/skills/review-case/SKILL.md new file mode 100644 index 000000000..bba3eb4ef --- /dev/null +++ b/macOS/.agents/skills/review-case/SKILL.md @@ -0,0 +1,109 @@ +--- +name: review-case +description: > + Code review for API examples. Ensures examples follow project conventions, + handle lifecycle correctly, manage threads safely, and use APIs properly. +license: MIT +metadata: + author: APIExample Team + version: 1.0.0 + platform: macOS +--- + +# Review Case — macOS + +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. + +## Lifecycle and Ownership + +- 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. + +```swift +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 + } + super.viewWillBeRemovedFromSplitView() +} +``` + +## 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 new file mode 100644 index 000000000..ba7327aa5 --- /dev/null +++ b/macOS/.agents/skills/review-case/references/correct-lifecycle.swift @@ -0,0 +1,20 @@ +// 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() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) +} + +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/.agent/skills/review-case/references/correct-thread-safety.swift b/macOS/.agents/skills/review-case/references/correct-thread-safety.swift similarity index 68% rename from macOS/.agent/skills/review-case/references/correct-thread-safety.swift rename to macOS/.agents/skills/review-case/references/correct-thread-safety.swift index 6e46a7b49..921e57dd8 100644 --- a/macOS/.agent/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/.agent/skills/review-case/references/incorrect-lifecycle.swift b/macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift similarity index 63% rename from macOS/.agent/skills/review-case/references/incorrect-lifecycle.swift rename to macOS/.agents/skills/review-case/references/incorrect-lifecycle.swift index 81180c567..e1951fba0 100644 --- a/macOS/.agent/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/.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 79% rename from macOS/.agent/skills/upsert-case/SKILL.md rename to macOS/.agents/skills/upsert-case/SKILL.md index 4e7b61909..c6e2f2d37 100644 --- a/macOS/.agent/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 new file mode 100644 index 000000000..3c2e792d3 --- /dev/null +++ b/macOS/.agents/skills/upsert-case/references/example-template.swift @@ -0,0 +1,78 @@ +import Cocoa +import AgoraRtcKit + +// 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? + private var tokenRequestID = 0 + + override func viewDidLoad() { + super.viewDidLoad() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + } + + // 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() { + tokenRequestID += 1 // Discard Token responses after leave or scene removal. + agoraKit?.leaveChannel(nil) + } + + 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) { + 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, 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 new file mode 100644 index 000000000..139789b4c --- /dev/null +++ b/macOS/.agents/skills/upsert-case/references/lifecycle-pattern.swift @@ -0,0 +1,23 @@ +// Lifecycle fragments inside a BaseViewController + AgoraRtcEngineDelegate case. +// See example-template.swift for the complete skeleton and asynchronous Token guards. + +// Create once when the case controller loads. +let config = AgoraRtcEngineConfig() +config.appId = KeyCenter.AppId +agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + +// 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. + +// 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/.agent/skills/upsert-case/references/thread-safety-pattern.swift b/macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift similarity index 72% rename from macOS/.agent/skills/upsert-case/references/thread-safety-pattern.swift rename to macOS/.agents/skills/upsert-case/references/thread-safety-pattern.swift index 6658ba75e..b6da0aba2 100644 --- a/macOS/.agent/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/.gitignore b/macOS/.gitignore index 19d5dacb9..0a126dffc 100644 --- a/macOS/.gitignore +++ b/macOS/.gitignore @@ -8,6 +8,7 @@ xcshareddata */libs *.zip +APIExample/Resources/AgoraBeautyMaterial.bundle/ agora_sdk *.xcarchive diff --git a/macOS/AGENTS.md b/macOS/AGENTS.md index 2116452eb..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:** @@ -61,9 +63,10 @@ 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 | +| 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/APIExample.xcodeproj/project.pbxproj b/macOS/APIExample.xcodeproj/project.pbxproj index 0b25ba2bb..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 */; }; @@ -57,7 +56,6 @@ 03896D3724F8A011008593CD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03896D3524F8A011008593CD /* Main.storyboard */; }; 03B12DA4250E8F7F00E55818 /* AgoraExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */; }; 3CD1D961EB36C1CAA0A379A8 /* Pods_SimpleFilter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7330EE777B818DE1FD428DA3 /* Pods_SimpleFilter.framework */; }; - 57645A03259B1C22007B1E30 /* CreateDataStream.strings in Resources */ = {isa = PBXBuildFile; fileRef = 576459FE259B1C22007B1E30 /* CreateDataStream.strings */; }; 57645A04259B1C22007B1E30 /* CreateDataStream.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 57645A00259B1C22007B1E30 /* CreateDataStream.storyboard */; }; 57645A05259B1C22007B1E30 /* CreateDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57645A02259B1C22007B1E30 /* CreateDataStream.swift */; }; 5770E2D5258C9E6F00812A80 /* Picker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5770E2D3258C9E6F00812A80 /* Picker.xib */; }; @@ -97,13 +95,11 @@ ABB399E12DF9559000AF5033 /* Simulcast.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ABB399DD2DF9559000AF5033 /* Simulcast.storyboard */; }; ABB399E22DF9559000AF5033 /* Multipath.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ABB399D82DF9559000AF5033 /* Multipath.storyboard */; }; DD33C1AA2CBF9DEA0046F50C /* RtePlayer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DD33C1A52CBF9DEA0046F50C /* RtePlayer.storyboard */; }; - DD33C1AB2CBF9DEA0046F50C /* RtePlayer.strings in Resources */ = {isa = PBXBuildFile; fileRef = DD33C1A72CBF9DEA0046F50C /* RtePlayer.strings */; }; DD33C1AC2CBF9DEA0046F50C /* RtePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33C1A82CBF9DEA0046F50C /* RtePlayer.swift */; }; DD33C1AE2CBFA0490046F50C /* PlayerObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33C1AD2CBFA0490046F50C /* PlayerObserver.swift */; }; DD33C1B02CBFBF4E0046F50C /* hlg-hdr.yuv in Resources */ = {isa = PBXBuildFile; fileRef = DD33C1AF2CBFBF4E0046F50C /* hlg-hdr.yuv */; }; DD8A1F922CA55AA4001CEC51 /* AgoraPCMPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8A1F912CA55AA4001CEC51 /* AgoraPCMPlayer.swift */; }; DD962C642CBA491600B8AAA7 /* AgoraALD.driver in Resources */ = {isa = PBXBuildFile; fileRef = DD962C632CBA491600B8AAA7 /* AgoraALD.driver */; }; - E702C1E728B4DB4800D7C7ED /* LiveStreaming.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E702C1E328B4DB4800D7C7ED /* LiveStreaming.storyboard */; }; E702C1E828B4DB4800D7C7ED /* LiveStreaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = E702C1E528B4DB4800D7C7ED /* LiveStreaming.swift */; }; E71E7B0B289B7D7900B846C7 /* SimpleFilter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8BD4AE79272518D600E95B87 /* SimpleFilter.framework */; }; E71E7B0C289B96FA00B846C7 /* VideoProcess.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E71E7B0E289B96FA00B846C7 /* VideoProcess.storyboard */; }; @@ -111,7 +107,6 @@ E72055EE28FE7FC70030E6D1 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E72055ED28FE7FC70030E6D1 /* Util.swift */; }; E7218BA628BF5E65005A01D5 /* agora-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = E7218BA528BF5E65005A01D5 /* agora-logo.png */; }; E728955128E5B8600013E7E6 /* Pods_APIExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F65EF2B97B89DE4581B426B /* Pods_APIExample.framework */; }; - E732E92A2A3960A2004403AF /* MultiCameraSourece.strings in Resources */ = {isa = PBXBuildFile; fileRef = E732E9252A3960A2004403AF /* MultiCameraSourece.strings */; }; E732E92B2A3960A2004403AF /* MultiCameraSourece.swift in Sources */ = {isa = PBXBuildFile; fileRef = E732E9272A3960A2004403AF /* MultiCameraSourece.swift */; }; E732E92C2A3960A2004403AF /* MultiCameraSourece.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E732E9282A3960A2004403AF /* MultiCameraSourece.storyboard */; }; E74877D728A261D700CA2F58 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E74877D428A261D700CA2F58 /* NetworkManager.swift */; }; @@ -137,26 +132,18 @@ E763A83A288AA99B00FE99FA /* AudioProcessor.mm in Sources */ = {isa = PBXBuildFile; fileRef = E763A82B288AA99B00FE99FA /* AudioProcessor.mm */; }; E763A83B288AA99B00FE99FA /* ExtensionVideoFilter.hpp in Headers */ = {isa = PBXBuildFile; fileRef = E763A82C288AA99B00FE99FA /* ExtensionVideoFilter.hpp */; }; E779025B2A482CFC008791AD /* KFMP4Demuxer.m in Sources */ = {isa = PBXBuildFile; fileRef = E779025A2A482CFC008791AD /* KFMP4Demuxer.m */; }; - E77D54BE28F553DA00D51C1E /* JoinChannelVideoToken.strings in Resources */ = {isa = PBXBuildFile; fileRef = E77D54B928F553DA00D51C1E /* JoinChannelVideoToken.strings */; }; E77D54BF28F553DA00D51C1E /* JoinChannelVideoToken.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E77D54BB28F553DA00D51C1E /* JoinChannelVideoToken.storyboard */; }; E77D54C028F553DA00D51C1E /* JoinChannelVideoToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77D54BD28F553DA00D51C1E /* JoinChannelVideoToken.swift */; }; E77D54D228F5649500D51C1E /* AgoraSampleBufferRender.m in Sources */ = {isa = PBXBuildFile; fileRef = E77D54D128F5649500D51C1E /* AgoraSampleBufferRender.m */; }; E77D54D628F56D3A00D51C1E /* sample.yuv in Resources */ = {isa = PBXBuildFile; fileRef = E77D54D528F56D3A00D51C1E /* sample.yuv */; }; E77D54D828F5702B00D51C1E /* SampleBufferDisplayView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E77D54D728F5702B00D51C1E /* SampleBufferDisplayView.xib */; }; - E7883AE02B046CB8003CCF44 /* FaceCapture.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7883ADB2B046CB8003CCF44 /* FaceCapture.strings */; }; - E7883AE12B046CB8003CCF44 /* FaceCapture.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7883ADD2B046CB8003CCF44 /* FaceCapture.storyboard */; }; - E7883AE22B046CB8003CCF44 /* FaceCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7883ADF2B046CB8003CCF44 /* FaceCapture.swift */; }; E7899BD42860B2F600851463 /* NSData+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7899BD32860B2F600851463 /* NSData+Extension.swift */; }; - E7AD0DEA29CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DE529CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.strings */; }; E7AD0DEB29CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DE729CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.storyboard */; }; E7AD0DEC29CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7AD0DE929CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.swift */; }; E7AD0DF629CAF78C00C9A4B0 /* LocalVideoTranscoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7AD0DF229CAF78C00C9A4B0 /* LocalVideoTranscoding.swift */; }; E7AD0DF929CD84F800C9A4B0 /* sample.mov in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DF829CD84F800C9A4B0 /* sample.mov */; }; - E7AD0E0029CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.strings in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DFB29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.strings */; }; E7AD0E0129CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E7AD0DFD29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard */; }; E7AD0E0229CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7AD0DFF29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.swift */; }; - F7453DBC2E03BCA5008DDFC8 /* beauty_material.bundle in Resources */ = {isa = PBXBuildFile; fileRef = F7453DBB2E03BCA5008DDFC8 /* beauty_material.bundle */; }; - F7F28B922E03BADD003B150D /* VideoProcess.strings in Resources */ = {isa = PBXBuildFile; fileRef = F7F28B902E03BADD003B150D /* VideoProcess.strings */; }; 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 */; }; @@ -223,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 = ""; }; @@ -245,6 +231,7 @@ 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraExtension.swift; sourceTree = ""; }; 1476E0A207E49E1B380D7B0F /* 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 = ""; }; 1D2C9FD747B518ECC54253C0 /* 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 = ""; }; + 25174BFDD491D9A484E1875A /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CustomAudioRender.strings"; sourceTree = ""; }; 4C8551EF6F12F734D8F7C1F5 /* 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 = ""; }; 576459FF259B1C22007B1E30 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CreateDataStream.strings"; sourceTree = ""; }; 57645A01259B1C22007B1E30 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CreateDataStream.storyboard; sourceTree = ""; }; @@ -263,6 +250,8 @@ 57A635F32593544600EDC2F7 /* effectA.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = effectA.wav; sourceTree = ""; }; 57AF397A259B31AA00601E02 /* RawAudioData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawAudioData.swift; sourceTree = ""; }; 57AF3980259B329B00601E02 /* RawAudioData.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RawAudioData.storyboard; sourceTree = ""; }; + 5E4225DBB9B0A96507C37BE9 /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CustomVideoRender.strings"; sourceTree = ""; }; + 6055C7F8575CB3F54282E81B /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CustomVideoSourcePush.strings"; sourceTree = ""; }; 67033721279E76560069F4B3 /* VideoProcess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoProcess.swift; sourceTree = ""; }; 67033725279E81DF0069F4B3 /* bg.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = bg.jpg; sourceTree = ""; }; 671BD66827DF3FAF0076D5E1 /* AgoraMetalRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraMetalRender.swift; sourceTree = ""; }; @@ -295,6 +284,9 @@ ABB399DB2DF9559000AF5033 /* Simulcast.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Simulcast.storyboard; path = Base.lproj/Simulcast.storyboard; sourceTree = ""; }; ABB399E42DF9570800AF5033 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Multipath.strings; sourceTree = ""; }; ABB399E62DF9571800AF5033 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Simulcast.strings; sourceTree = ""; }; + ABB90B022DF88509007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Simulcast.strings"; sourceTree = ""; }; + ABB90B082DF8854B007FDDEB /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Multipath.strings"; sourceTree = ""; }; + BCEEB002E3D3E70B81FA8CB0 /* zh-Hans */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/JoinChannelVideo.strings"; sourceTree = ""; }; DD33C1A42CBF9DEA0046F50C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/RtePlayer.storyboard; sourceTree = ""; }; DD33C1A62CBF9DEA0046F50C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RtePlayer.strings"; sourceTree = ""; }; DD33C1A82CBF9DEA0046F50C /* RtePlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RtePlayer.swift; sourceTree = ""; }; @@ -352,9 +344,6 @@ E77D54D128F5649500D51C1E /* AgoraSampleBufferRender.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AgoraSampleBufferRender.m; sourceTree = ""; }; E77D54D528F56D3A00D51C1E /* sample.yuv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sample.yuv; sourceTree = ""; }; E77D54D728F5702B00D51C1E /* SampleBufferDisplayView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SampleBufferDisplayView.xib; sourceTree = ""; }; - E7883ADC2B046CB8003CCF44 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/FaceCapture.strings"; sourceTree = ""; }; - E7883ADE2B046CB8003CCF44 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/FaceCapture.storyboard; sourceTree = ""; }; - E7883ADF2B046CB8003CCF44 /* FaceCapture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaceCapture.swift; sourceTree = ""; }; E7899BD32860B2F600851463 /* NSData+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSData+Extension.swift"; sourceTree = ""; }; E7AD0DE629CAAF2B00C9A4B0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/JoinChannelVideoRecorder.strings"; sourceTree = ""; }; E7AD0DE829CAAF2B00C9A4B0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/JoinChannelVideoRecorder.storyboard; sourceTree = ""; }; @@ -364,7 +353,7 @@ E7AD0DFC29CDA4F100C9A4B0 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CustomVideoSourcePushMulti.strings"; sourceTree = ""; }; E7AD0DFE29CDA4F100C9A4B0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomVideoSourcePushMulti.storyboard; sourceTree = ""; }; E7AD0DFF29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomVideoSourcePushMulti.swift; sourceTree = ""; }; - F7453DBB2E03BCA5008DDFC8 /* beauty_material.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = beauty_material.bundle; sourceTree = ""; }; + F7453DBB2E03BCA5008DDFC8 /* AgoraBeautyMaterial.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = AgoraBeautyMaterial.bundle; sourceTree = ""; }; F7F28B8A2E03BADD003B150D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AgoraBeauty.storyboard; sourceTree = ""; }; F7F28B8C2E03BADD003B150D /* AgoraBeauty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraBeauty.swift; sourceTree = ""; }; F7F28B8D2E03BADD003B150D /* AgoraBeautyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgoraBeautyManager.swift; sourceTree = ""; }; @@ -549,7 +538,7 @@ 034C629D25297ABB00296ECF /* Resources */ = { isa = PBXGroup; children = ( - F7453DBB2E03BCA5008DDFC8 /* beauty_material.bundle */, + F7453DBB2E03BCA5008DDFC8 /* AgoraBeautyMaterial.bundle */, DD33C1AF2CBFBF4E0046F50C /* hlg-hdr.yuv */, E7AD0DF829CD84F800C9A4B0 /* sample.mov */, E77D54D528F56D3A00D51C1E /* sample.yuv */, @@ -579,7 +568,6 @@ ABB399D92DF9559000AF5033 /* Multipath */, ABB399DE2DF9559000AF5033 /* Simulcast */, DD33C1A92CBF9DEA0046F50C /* RtePlayer */, - E7883ADA2B046CB8003CCF44 /* FaceCapture */, E732E9242A3960A2004403AF /* MultiCameraSourece */, E7AD0DFA29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti */, E7AD0DEF29CAF78C00C9A4B0 /* LocalVideoTranscoding */, @@ -659,7 +647,6 @@ 5770E2C0258C580E00812A80 /* Component */, 57887A7F25885FC2006E962A /* Settings */, 034C62922528474D00296ECF /* StatisticsInfo.swift */, - 034C62862528255F00296ECF /* WindowsCenter.swift */, 034C626325257EA600296ECF /* GlobalSettings.swift */, 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */, 034C62742525C68C00296ECF /* CustomEncryption */, @@ -694,7 +681,6 @@ 576459FD259B1C22007B1E30 /* CreateDataStream */ = { isa = PBXGroup; children = ( - 576459FE259B1C22007B1E30 /* CreateDataStream.strings */, 57645A00259B1C22007B1E30 /* CreateDataStream.storyboard */, 57645A02259B1C22007B1E30 /* CreateDataStream.swift */, ); @@ -871,7 +857,6 @@ children = ( DD33C1AD2CBFA0490046F50C /* PlayerObserver.swift */, DD33C1A52CBF9DEA0046F50C /* RtePlayer.storyboard */, - DD33C1A72CBF9DEA0046F50C /* RtePlayer.strings */, DD33C1A82CBF9DEA0046F50C /* RtePlayer.swift */, ); path = RtePlayer; @@ -880,7 +865,6 @@ E702C1E028B4DB4800D7C7ED /* LiveStreaming */ = { isa = PBXGroup; children = ( - E702C1E328B4DB4800D7C7ED /* LiveStreaming.storyboard */, E75279D329F6737100358F18 /* LiveStreaming.storyboard */, E702C1E528B4DB4800D7C7ED /* LiveStreaming.swift */, ); @@ -899,7 +883,6 @@ E732E9242A3960A2004403AF /* MultiCameraSourece */ = { isa = PBXGroup; children = ( - E732E9252A3960A2004403AF /* MultiCameraSourece.strings */, E732E9272A3960A2004403AF /* MultiCameraSourece.swift */, E732E9282A3960A2004403AF /* MultiCameraSourece.storyboard */, ); @@ -918,27 +901,15 @@ E77D54B828F553DA00D51C1E /* JoinChannelVideo(Token) */ = { isa = PBXGroup; children = ( - E77D54B928F553DA00D51C1E /* JoinChannelVideoToken.strings */, E77D54BB28F553DA00D51C1E /* JoinChannelVideoToken.storyboard */, E77D54BD28F553DA00D51C1E /* JoinChannelVideoToken.swift */, ); path = "JoinChannelVideo(Token)"; sourceTree = ""; }; - E7883ADA2B046CB8003CCF44 /* FaceCapture */ = { - isa = PBXGroup; - children = ( - E7883ADB2B046CB8003CCF44 /* FaceCapture.strings */, - E7883ADD2B046CB8003CCF44 /* FaceCapture.storyboard */, - E7883ADF2B046CB8003CCF44 /* FaceCapture.swift */, - ); - path = FaceCapture; - sourceTree = ""; - }; E7AD0DE429CAAF2B00C9A4B0 /* JoinChannelVideo(Recorder) */ = { isa = PBXGroup; children = ( - E7AD0DE529CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.strings */, E7AD0DE729CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.storyboard */, E7AD0DE929CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.swift */, ); @@ -957,7 +928,6 @@ E7AD0DFA29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti */ = { isa = PBXGroup; children = ( - E7AD0DFB29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.strings */, E7AD0DFD29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard */, E7AD0DFF29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.swift */, ); @@ -980,7 +950,6 @@ F7F28B8B2E03BADD003B150D /* AgoraBeauty.storyboard */, F7F28B8C2E03BADD003B150D /* AgoraBeauty.swift */, F7F28B8D2E03BADD003B150D /* AgoraBeautyManager.swift */, - F7F28B902E03BADD003B150D /* VideoProcess.strings */, ); path = AgoraBeauty; sourceTree = ""; @@ -1016,6 +985,7 @@ 03896D2A24F8A00F008593CD /* Resources */, 032C0FA2254873AC00D80A57 /* Embed Frameworks */, D1D867115490181C41253FA3 /* [CP] Embed Pods Frameworks */, + F7453DBD2E03BCA5008DDFC8 /* Copy Optional Beauty Resources */, ); buildRules = ( ); @@ -1094,13 +1064,7 @@ E75279D529F6737100358F18 /* LiveStreaming.storyboard in Resources */, E77D54BF28F553DA00D51C1E /* JoinChannelVideoToken.storyboard in Resources */, 033A9FDB252EB05A00BC26E1 /* PrecallTest.storyboard in Resources */, - E7AD0E0029CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.strings in Resources */, - E7883AE12B046CB8003CCF44 /* FaceCapture.storyboard in Resources */, - E7AD0DEA29CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.strings in Resources */, 033A9FFA252EB5FD00BC26E1 /* ScreenShare.storyboard in Resources */, - E732E92A2A3960A2004403AF /* MultiCameraSourece.strings in Resources */, - E77D54BE28F553DA00D51C1E /* JoinChannelVideoToken.strings in Resources */, - 57645A03259B1C22007B1E30 /* CreateDataStream.strings in Resources */, 5770E2D5258C9E6F00812A80 /* Picker.xib in Resources */, E75279B529F6651800358F18 /* Localizable.strings in Resources */, 57887A67258856B7006E962A /* Settings.storyboard in Resources */, @@ -1117,7 +1081,6 @@ 033A9FC2252EB02D00BC26E1 /* CustomAudioSource.storyboard in Resources */, 57AF3981259B329B00601E02 /* RawAudioData.storyboard in Resources */, E7AD0E0129CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard in Resources */, - F7F28B922E03BADD003B150D /* VideoProcess.strings in Resources */, F7F28B932E03BADD003B150D /* AgoraBeauty.storyboard in Resources */, 033A9FE5252EB59000BC26E1 /* VoiceChanger.storyboard in Resources */, 033A9FBD252EB02600BC26E1 /* CustomAudioRender.storyboard in Resources */, @@ -1137,11 +1100,8 @@ 671BD67B27E0A4F50076D5E1 /* output.raw in Resources */, E71E7B0C289B96FA00B846C7 /* VideoProcess.storyboard in Resources */, 033A9FCC252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard in Resources */, - E7883AE02B046CB8003CCF44 /* FaceCapture.strings in Resources */, 671BD67127DF478A0076D5E1 /* CustomVideoRender.storyboard in Resources */, DD33C1AA2CBF9DEA0046F50C /* RtePlayer.storyboard in Resources */, - F7453DBC2E03BCA5008DDFC8 /* beauty_material.bundle in Resources */, - E702C1E728B4DB4800D7C7ED /* LiveStreaming.storyboard in Resources */, 57A635F42593544600EDC2F7 /* effectA.wav in Resources */, AB1B2D922EC701AF007C8776 /* LocalVideoTranscoding.storyboard in Resources */, E7AD0DEB29CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.storyboard in Resources */, @@ -1150,7 +1110,6 @@ 033A9FF0252EB5EB00BC26E1 /* ChannelMediaRelay.storyboard in Resources */, E75279C329F66CE600358F18 /* QuickSwitchChannel.storyboard in Resources */, 034C62A125297ABB00296ECF /* audiomixing.mp3 in Resources */, - DD33C1AB2CBF9DEA0046F50C /* RtePlayer.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1229,6 +1188,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 */ @@ -1284,12 +1262,10 @@ 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 */, E77D54C028F553DA00D51C1E /* JoinChannelVideoToken.swift in Sources */, - E7883AE22B046CB8003CCF44 /* FaceCapture.swift in Sources */, 0336A1CA25034F4700D61B7F /* ExternalAudio.mm in Sources */, E779025B2A482CFC008791AD /* KFMP4Demuxer.m in Sources */, DD8A1F922CA55AA4001CEC51 /* AgoraPCMPlayer.swift in Sources */, @@ -1331,6 +1307,7 @@ isa = PBXVariantGroup; children = ( 033A9FB4252EAEB500BC26E1 /* Base */, + BCEEB002E3D3E70B81FA8CB0 /* zh-Hans */, ); name = JoinChannelVideo.storyboard; sourceTree = ""; @@ -1348,6 +1325,7 @@ isa = PBXVariantGroup; children = ( 033A9FBE252EB02600BC26E1 /* Base */, + 25174BFDD491D9A484E1875A /* zh-Hans */, ); name = CustomAudioRender.storyboard; sourceTree = ""; @@ -1365,6 +1343,7 @@ isa = PBXVariantGroup; children = ( 033A9FCD252EB03F00BC26E1 /* Base */, + 6055C7F8575CB3F54282E81B /* zh-Hans */, ); name = CustomVideoSourcePush.storyboard; sourceTree = ""; @@ -1450,18 +1429,11 @@ name = Main.storyboard; sourceTree = ""; }; - 576459FE259B1C22007B1E30 /* CreateDataStream.strings */ = { - isa = PBXVariantGroup; - children = ( - 576459FF259B1C22007B1E30 /* zh-Hans */, - ); - name = CreateDataStream.strings; - sourceTree = ""; - }; 57645A00259B1C22007B1E30 /* CreateDataStream.storyboard */ = { isa = PBXVariantGroup; children = ( 57645A01259B1C22007B1E30 /* Base */, + 576459FF259B1C22007B1E30 /* zh-Hans */, ); name = CreateDataStream.storyboard; sourceTree = ""; @@ -1478,6 +1450,7 @@ isa = PBXVariantGroup; children = ( 671BD66E27DF478A0076D5E1 /* Base */, + 5E4225DBB9B0A96507C37BE9 /* zh-Hans */, ); name = CustomVideoRender.storyboard; sourceTree = ""; @@ -1505,6 +1478,7 @@ children = ( ABB399D62DF9559000AF5033 /* Multipath.storyboard */, ABB399E42DF9570800AF5033 /* en */, + ABB90B082DF8854B007FDDEB /* zh-Hans */, ); name = Multipath.storyboard; sourceTree = ""; @@ -1514,6 +1488,7 @@ children = ( ABB399DB2DF9559000AF5033 /* Simulcast.storyboard */, ABB399E62DF9571800AF5033 /* en */, + ABB90B022DF88509007FDDEB /* zh-Hans */, ); name = Simulcast.storyboard; sourceTree = ""; @@ -1522,24 +1497,9 @@ isa = PBXVariantGroup; children = ( DD33C1A42CBF9DEA0046F50C /* Base */, - ); - name = RtePlayer.storyboard; - sourceTree = ""; - }; - DD33C1A72CBF9DEA0046F50C /* RtePlayer.strings */ = { - isa = PBXVariantGroup; - children = ( DD33C1A62CBF9DEA0046F50C /* zh-Hans */, ); - name = RtePlayer.strings; - sourceTree = ""; - }; - E702C1E328B4DB4800D7C7ED /* LiveStreaming.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E75279D129F6728900358F18 /* zh-Hans */, - ); - name = LiveStreaming.storyboard; + name = RtePlayer.storyboard; sourceTree = ""; }; E71E7B0E289B96FA00B846C7 /* VideoProcess.storyboard */ = { @@ -1552,18 +1512,11 @@ name = VideoProcess.storyboard; sourceTree = ""; }; - E732E9252A3960A2004403AF /* MultiCameraSourece.strings */ = { - isa = PBXVariantGroup; - children = ( - E732E9262A3960A2004403AF /* zh-Hans */, - ); - name = MultiCameraSourece.strings; - sourceTree = ""; - }; E732E9282A3960A2004403AF /* MultiCameraSourece.storyboard */ = { isa = PBXVariantGroup; children = ( E732E9292A3960A2004403AF /* Base */, + E732E9262A3960A2004403AF /* zh-Hans */, ); name = MultiCameraSourece.storyboard; sourceTree = ""; @@ -1600,6 +1553,7 @@ isa = PBXVariantGroup; children = ( E75279D429F6737100358F18 /* Base */, + E75279D129F6728900358F18 /* zh-Hans */, ); name = LiveStreaming.storyboard; sourceTree = ""; @@ -1613,66 +1567,29 @@ name = MediaPlayer.storyboard; sourceTree = ""; }; - E77D54B928F553DA00D51C1E /* JoinChannelVideoToken.strings */ = { - isa = PBXVariantGroup; - children = ( - E77D54BA28F553DA00D51C1E /* zh-Hans */, - ); - name = JoinChannelVideoToken.strings; - sourceTree = ""; - }; E77D54BB28F553DA00D51C1E /* JoinChannelVideoToken.storyboard */ = { isa = PBXVariantGroup; children = ( E77D54BC28F553DA00D51C1E /* Base */, + E77D54BA28F553DA00D51C1E /* zh-Hans */, ); name = JoinChannelVideoToken.storyboard; sourceTree = ""; }; - E7883ADB2B046CB8003CCF44 /* FaceCapture.strings */ = { - isa = PBXVariantGroup; - children = ( - E7883ADC2B046CB8003CCF44 /* zh-Hans */, - ); - name = FaceCapture.strings; - sourceTree = ""; - }; - E7883ADD2B046CB8003CCF44 /* FaceCapture.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E7883ADE2B046CB8003CCF44 /* Base */, - ); - name = FaceCapture.storyboard; - sourceTree = ""; - }; - E7AD0DE529CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.strings */ = { - isa = PBXVariantGroup; - children = ( - E7AD0DE629CAAF2B00C9A4B0 /* zh-Hans */, - ); - name = JoinChannelVideoRecorder.strings; - sourceTree = ""; - }; E7AD0DE729CAAF2B00C9A4B0 /* JoinChannelVideoRecorder.storyboard */ = { isa = PBXVariantGroup; children = ( E7AD0DE829CAAF2B00C9A4B0 /* Base */, + E7AD0DE629CAAF2B00C9A4B0 /* zh-Hans */, ); name = JoinChannelVideoRecorder.storyboard; sourceTree = ""; }; - E7AD0DFB29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.strings */ = { - isa = PBXVariantGroup; - children = ( - E7AD0DFC29CDA4F100C9A4B0 /* zh-Hans */, - ); - name = CustomVideoSourcePushMulti.strings; - sourceTree = ""; - }; E7AD0DFD29CDA4F100C9A4B0 /* CustomVideoSourcePushMulti.storyboard */ = { isa = PBXVariantGroup; children = ( E7AD0DFE29CDA4F100C9A4B0 /* Base */, + E7AD0DFC29CDA4F100C9A4B0 /* zh-Hans */, ); name = CustomVideoSourcePushMulti.storyboard; sourceTree = ""; @@ -1681,17 +1598,10 @@ isa = PBXVariantGroup; children = ( F7F28B8A2E03BADD003B150D /* Base */, - ); - name = AgoraBeauty.storyboard; - sourceTree = ""; - }; - F7F28B902E03BADD003B150D /* VideoProcess.strings */ = { - isa = PBXVariantGroup; - children = ( F7F28B8E2E03BADD003B150D /* en */, F7F28B8F2E03BADD003B150D /* zh-Hans */, ); - name = VideoProcess.strings; + name = AgoraBeauty.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -1749,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; @@ -1805,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; @@ -1837,7 +1747,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.examples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1869,7 +1779,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 4.6.2; + MARKETING_VERSION = 4.7.0; PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.examples; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; 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/AgoraExtension.swift b/macOS/APIExample/Common/AgoraExtension.swift index 3475f4a9e..7b3fd0c75 100644 --- a/macOS/APIExample/Common/AgoraExtension.swift +++ b/macOS/APIExample/Common/AgoraExtension.swift @@ -138,13 +138,17 @@ extension AgoraAudioScenario { switch self { case .default: return "Default".localized case .gameStreaming: return "Game Streaming".localized + case .chatRoom: return "Chat Room".localized + case .chorus: return "Chorus".localized + case .meeting: return "Meeting".localized + case .aiClient: return "AI Client".localized default: return "\(self.rawValue)" } } static func allValues() -> [AgoraAudioScenario] { - return [.default, .gameStreaming] + return [.default, .gameStreaming, .chatRoom, .chorus, .meeting, .aiClient] } } 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/Common/KeyCenter.swift b/macOS/APIExample/Common/KeyCenter.swift index 7de1cee79..b0d36a7af 100644 --- a/macOS/APIExample/Common/KeyCenter.swift +++ b/macOS/APIExample/Common/KeyCenter.swift @@ -38,11 +38,4 @@ struct KeyCenter { 注意:App证书放在客户端不安全,推荐放在服务端以确保 App 证书不会泄露。 */ static let Certificate: String? = nil - - - /** - Face Capture License - 面捕数据鉴权license - */ - static let FaceCaptureLicense: String? = nil } 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/AgoraBeauty/AgoraBeauty.swift b/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift index 246b558fa..2fe86d8fd 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeauty.swift @@ -33,7 +33,6 @@ class AgoraBeauty: BaseViewController { let backgroundTypes = AgoraVirtualBackgroundSourceType.allValues() var agoraKit: AgoraRtcEngineKit! var beautyManager: AgoraBeautyManager! - var beautifyOption = AgoraBeautyOptions() var skinProtect = 0.5 var strength = 0.5 var whintening = 0.5 @@ -77,7 +76,7 @@ class AgoraBeauty: BaseViewController { private func setupBeautyManager() { beautyManager = AgoraBeautyManager(agoraKit: agoraKit) - beautyManager.beautyMakeupStyle = "default makeup style".localized + beautyManager.beautyMakeupStyle = "Makeup-Young" beautyManager.makeUpEnable = false } @@ -88,6 +87,7 @@ class AgoraBeauty: BaseViewController { LogUtils.log(message: "Left channel", level: .info) } } + beautyManager.destory() AgoraRtcEngineKit.destroy() } @@ -97,10 +97,10 @@ class AgoraBeauty: BaseViewController { channelField.field.placeholderString = "Channel Name".localized joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized - lightenSlider.floatValue = beautifyOption.lighteningLevel - ruddySlider.floatValue = beautifyOption.rednessLevel - sharpSlider.floatValue = beautifyOption.sharpnessLevel - smoothingSlider.floatValue = beautifyOption.smoothnessLevel + lightenSlider.floatValue = beautyManager.lightness + ruddySlider.floatValue = beautyManager.redness + sharpSlider.floatValue = beautyManager.sharpness + smoothingSlider.floatValue = beautyManager.smoothness initSelectResolutionPicker() initSelectFpsPicker() @@ -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") } }) @@ -174,15 +174,7 @@ class AgoraBeauty: BaseViewController { } @IBAction func onBeautySliderChange(_ sender: NSSwitch) { - if sender.state == .on { - if agoraKit.isFeatureAvailable(onDevice: .videoPreprocessBeauty) { - agoraKit.setBeautyEffectOptions(sender.state == .on, options: beautifyOption) - } else { - showAlert(message: "The feature is unavailable in the device!") - } - } else { - agoraKit.setBeautyEffectOptions(sender.state == .on, options: beautifyOption) - } + beautyManager.basicBeautyEnable = sender.state == .on } @IBAction func onLightenSliderChange(_ sender: NSSlider) { @@ -386,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) @@ -396,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/AgoraBeauty/AgoraBeautyManager.swift b/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift index b940fd2cc..c81dea486 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/AgoraBeautyManager.swift @@ -15,8 +15,8 @@ class AgoraBeautyManager { private lazy var faceshapeOption = AgoraFaceShapeBeautyOptions() private var styleParam: [String : Any] = ["enable_mu": false] private var m_bundle_copied = false; - let beauty_material_path: String = NSHomeDirectory() + "/Documents/beauty_material.bundle"; - let m_current_material_name = "beauty_material_v2.0.0"; + let beauty_material_path: String = NSHomeDirectory() + "/Documents/AgoraBeautyMaterial.bundle"; + let m_current_material_name = "beauty_material_functional"; init(agoraKit: AgoraRtcEngineKit? = nil) { self.agoraKit = agoraKit @@ -35,23 +35,36 @@ class AgoraBeautyManager { extension: "clear_vision", enabled: true, sourceType: .primaryCamera) - copyBeautyBundle() + guard copyBeautyBundle() else { + agoraKit?.enableExtension(withVendor: "agora_video_filters_clear_vision", + extension: "clear_vision", + enabled: false, + sourceType: .primaryCamera) + return + } let path = beauty_material_path + "/" + m_current_material_name; videoEffectObject = agoraKit?.createVideoEffectObject(bundlePath: path, sourceType: AgoraMediaSourceType.primaryCamera) agoraKit?.setParameters("{\"rtc.video.yuvconverter_enable_hardware_buffer\":true}") } - private func copyBeautyBundle() { + private func copyBeautyBundle() -> Bool { if (m_bundle_copied) { - return + return true + } + guard let bundle_path = Bundle.main.path(forResource: "AgoraBeautyMaterial", ofType: "bundle") else { + return false } - let bundle_path = Bundle.main.path(forResource: "beauty_material", ofType: "bundle") if FileManager.default.fileExists(atPath: beauty_material_path) { try? FileManager.default.removeItem(atPath: beauty_material_path) } - try? FileManager.default.copyItem(atPath: bundle_path!, toPath: beauty_material_path) + do { + try FileManager.default.copyItem(atPath: bundle_path, toPath: beauty_material_path) + } catch { + return false + } m_bundle_copied = true + return true } private func addEffect(node : UInt) { @@ -161,6 +174,7 @@ class AgoraBeautyManager { } set { guard let effectObj = videoEffectObject else { return } + effectObj.setVideoEffectStringParam(option: "beauty_effect_option", key: "whiten_lut_path", stringValue: "") effectObj.setVideoEffectFloatParam(option: "beauty_effect_option", key: "lightness", floatValue: newValue) } } 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 8fe54b862..4300b23c6 100644 --- a/macOS/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/VideoProcess.strings +++ b/macOS/APIExample/Examples/Advanced/AgoraBeauty/zh-Hans.lproj/VideoProcess.strings @@ -1,47 +1,32 @@ -// -///* Class = "NSTextFieldCell"; title = "Low light Enhancement"; ObjectID = "11E-tR-tFb"; */ -//"11E-tR-tFb.title" = "Low light Enhancement"; -// -///* Class = "NSTextFieldCell"; title = "美颜"; ObjectID = "4WO-Em-0Qq"; */ -//"4WO-Em-0Qq.title" = "美颜"; -// -///* Class = "NSTextFieldCell"; title = "Strength"; ObjectID = "7PH-4u-nrD"; */ -//"7PH-4u-nrD.title" = "Strength"; -// -///* Class = "NSTextFieldCell"; title = "红润"; ObjectID = "85x-Bt-HuU"; */ -//"85x-Bt-HuU.title" = "红润"; -// -///* Class = "NSTextFieldCell"; title = "Color Enhancement"; ObjectID = "97m-MF-3to"; */ -//"97m-MF-3to.title" = "Color Enhancement"; -// -///* Class = "NSBox"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ -//"BP9-4w-AfJ.title" = "Box"; -// -///* 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 = "NSTextFieldCell"; title = "锐利"; ObjectID = "ZOR-gO-Ji2"; */ -//"ZOR-gO-Ji2.title" = "锐利"; -// -///* Class = "NSTextFieldCell"; title = "Skin Protect"; ObjectID = "ozL-f1-AXZ"; */ -//"ozL-f1-AXZ.title" = "Skin Protect"; -// -///* Class = "NSTextFieldCell"; title = "Video Denoise"; ObjectID = "plA-5C-vDg"; */ -//"plA-5C-vDg.title" = "Video Denoise"; -// -///* Class = "NSTextFieldCell"; title = "美白"; ObjectID = "q1I-U0-llK"; */ -//"q1I-U0-llK.title" = "美白"; -// -///* Class = "NSTextFieldCell"; title = "平滑"; ObjectID = "sFK-pV-vaj"; */ -//"sFK-pV-vaj.title" = "平滑"; -// -//"AAl-Un-v63.title" = "提亮"; -// -///* Class = "NSButtonCell"; normalTitle = "Face Shape"; ObjectID = "9VS-Ox-yEZ"; */ -//"9VS-Ox-yEZ.title" = "美型"; -// -///* Class = "NSButtonCell"; normalTitle = "Make Up"; ObjectID = "8aS-7m-YP7"; */ -//"8aS-7m-YP7.title" = "美妆"; +/* Element = "textFieldCell"; title = "Beauty"; ObjectID = "4WO-Em-0Qq"; */ +"4WO-Em-0Qq.title" = "美颜"; + +/* Element = "textFieldCell"; title = "Ruddy"; ObjectID = "85x-Bt-HuU"; */ +"85x-Bt-HuU.title" = "红润"; + +/* Element = "buttonCell"; title = "Make Up"; ObjectID = "8aS-7m-YP7"; */ +"8aS-7m-YP7.title" = "美妆"; + +/* Element = "buttonCell"; title = "Face Shape"; ObjectID = "9VS-Ox-yEZ"; */ +"9VS-Ox-yEZ.title" = "脸型"; + +/* 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 = "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" = "锐利"; + +/* Element = "textFieldCell"; title = "Lightening"; ObjectID = "ky2-qK-YAJ"; */ +"ky2-qK-YAJ.title" = "美白"; + +/* Element = "textFieldCell"; title = "Whitening"; ObjectID = "q1I-U0-llK"; */ +"q1I-U0-llK.title" = "美白"; + +/* Element = "textFieldCell"; title = "Smoothing"; ObjectID = "sFK-pV-vaj"; */ +"sFK-pV-vaj.title" = "平滑"; diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift b/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift index 2aeab21a3..2efcb54a9 100644 --- a/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift @@ -64,7 +64,7 @@ class AudioMixing: 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 @@ -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/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 ea4e8f9a4..9cec20865 100644 --- a/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -1,78 +1,56 @@ +/* Element = "buttonCell"; title = "Resume"; ObjectID = "8GX-mr-P4n"; */ +"8GX-mr-P4n.title" = "恢复播放"; -/* Class = "NSButtonCell"; title = "Resume"; ObjectID = "8GX-mr-P4n"; */ -"8GX-mr-P4n.title" = "Resume"; - -/* Class = "NSTextFieldCell"; title = "00 : 00"; ObjectID = "8Kf-Su-NKI"; */ +/* Element = "textFieldCell"; title = "00 : 00"; ObjectID = "8Kf-Su-NKI"; */ "8Kf-Su-NKI.title" = "00 : 00"; -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "8bV-OK-zbc"; */ -"8bV-OK-zbc.title" = "1V15"; - -/* Class = "NSTextFieldCell"; title = "Audio Effect Controls"; ObjectID = "EBL-gG-Ubf"; */ -"EBL-gG-Ubf.title" = "Audio Effect Controls"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "EhX-UJ-wov"; */ -"EhX-UJ-wov.placeholderString" = "Enter Channel Name"; - -/* Class = "NSButtonCell"; title = "Play"; ObjectID = "IUe-EM-mfG"; */ -"IUe-EM-mfG.title" = "Play"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "J6a-ul-c2H"; */ -"J6a-ul-c2H.title" = "1V3"; - -/* Class = "NSButtonCell"; title = "Pause"; ObjectID = "LgF-bS-HZ9"; */ -"LgF-bS-HZ9.title" = "Pause"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "P4E-oB-5Di"; */ -"P4E-oB-5Di.title" = "Join Channel"; - -/* Class = "NSButtonCell"; title = "Stop"; ObjectID = "PAO-8S-8lX"; */ -"PAO-8S-8lX.title" = "Stop"; +/* Element = "textFieldCell"; title = "Loop Back Recording"; ObjectID = "8qO-PU-Fev"; */ +"8qO-PU-Fev.title" = "系统音频混音"; -/* Class = "NSButtonCell"; title = "Resume"; ObjectID = "R5O-SE-8mk"; */ -"R5O-SE-8mk.title" = "Resume"; +/* Element = "textFieldCell"; title = "Audio Effect Controls"; ObjectID = "EBL-gG-Ubf"; */ +"EBL-gG-Ubf.title" = "音效控制"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "ch0-OR-L16"; */ -"ch0-OR-L16.title" = "1V1"; +/* Element = "buttonCell"; title = "Play"; ObjectID = "IUe-EM-mfG"; */ +"IUe-EM-mfG.title" = "播放"; -/* Class = "NSButtonCell"; title = "Stop"; ObjectID = "eUh-bN-yCK"; */ -"eUh-bN-yCK.title" = "Stop"; +/* Element = "buttonCell"; title = "Pause"; ObjectID = "LgF-bS-HZ9"; */ +"LgF-bS-HZ9.title" = "暂停"; -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "gWk-wf-hPu"; */ -"gWk-wf-hPu.title" = "1V8"; +/* Element = "buttonCell"; title = "disable"; ObjectID = "OcI-Tl-32x"; */ +"OcI-Tl-32x.title" = "关闭"; -/* Class = "NSTextFieldCell"; title = "Mixing Volume"; ObjectID = "hQ4-2Z-Twn"; */ -"hQ4-2Z-Twn.title" = "Mixing Volume"; +/* Element = "buttonCell"; title = "Stop"; ObjectID = "PAO-8S-8lX"; */ +"PAO-8S-8lX.title" = "停止"; -/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "Join Channel Audio"; +/* Element = "box"; title = "Box"; ObjectID = "PxV-Ne-Xed"; */ +"PxV-Ne-Xed.title" = "Box"; -/* Class = "NSTextFieldCell"; title = "Effect Volume"; ObjectID = "kh5-ZD-Sm3"; */ -"kh5-ZD-Sm3.title" = "Effect Volume"; +/* Element = "buttonCell"; title = "Resume"; ObjectID = "R5O-SE-8mk"; */ +"R5O-SE-8mk.title" = "恢复播放"; -/* Class = "NSTextFieldCell"; title = "Mixing Playback Volume"; ObjectID = "m1U-uA-7L4"; */ -"m1U-uA-7L4.title" = "Mixing Playback Volume"; +/* Element = "buttonCell"; title = "enable"; ObjectID = "SNu-S9-xaT"; */ +"SNu-S9-xaT.title" = "开启"; -/* Class = "NSButtonCell"; title = "Pause"; ObjectID = "mcr-Pl-O4W"; */ -"mcr-Pl-O4W.title" = "Pause"; +/* Element = "buttonCell"; title = "Play Additional Effect"; ObjectID = "cuY-mv-20C"; */ +"cuY-mv-20C.title" = "播放附加音效"; -/* Class = "NSTextFieldCell"; title = "Mixing Publish Volume"; ObjectID = "pHa-mK-6Ko"; */ -"pHa-mK-6Ko.title" = "Mixing Publish Volume"; +/* Element = "buttonCell"; title = "Stop"; ObjectID = "eUh-bN-yCK"; */ +"eUh-bN-yCK.title" = "停止"; -/* Class = "NSButtonCell"; title = "Start"; ObjectID = "pNA-hI-TUH"; */ -"pNA-hI-TUH.title" = "Start Mixing"; +/* Element = "buttonCell"; title = "Stop Additional Effect"; ObjectID = "iQT-DV-ynF"; */ +"iQT-DV-ynF.title" = "停止附加音效"; -/* Class = "NSTextFieldCell"; title = "Audio Mixing Controls"; ObjectID = "sLt-IU-VEu"; */ -"sLt-IU-VEu.title" = "Audio Mixing Controls"; +/* Element = "viewController"; title = "Audio Mixing"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "音频文件混音"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "szu-uz-G6W"; */ -"szu-uz-G6W.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Pause"; ObjectID = "mcr-Pl-O4W"; */ +"mcr-Pl-O4W.title" = "暂停"; -/* Class = "NSTextFieldCell"; title = "Loop Back Recording"; ObjectID = "8qO-PU-Fev"; */ -"8qO-PU-Fev.title" = "System Audio Mixing"; +/* Element = "buttonCell"; title = "Start"; ObjectID = "pNA-hI-TUH"; */ +"pNA-hI-TUH.title" = "开始混音"; -/* Class = "NSButtonCell"; title = "enable"; ObjectID = "SNu-S9-xaT"; */ -"SNu-S9-xaT.title" = "Enable"; +/* Element = "textFieldCell"; title = "Audio Mixing Controls"; ObjectID = "sLt-IU-VEu"; */ +"sLt-IU-VEu.title" = "混音控制"; -/* Class = "NSButtonCell"; title = "disable"; ObjectID = "OcI-Tl-32x"; */ -"OcI-Tl-32x.title" = "Disable"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "tOH-0i-cWb"; */ +"tOH-0i-cWb.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/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/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings index 5755d7ede..fa87086aa 100644 --- a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings @@ -1,21 +1,14 @@ +/* Element = "buttonCell"; title = "Pause Relay"; ObjectID = "1CI-ah-KbX"; */ +"1CI-ah-KbX.title" = "暂停转发"; -/* Class = "NSTextFieldCell"; placeholderString = "Relay Channnel Name"; ObjectID = "Ab2-sI-Ld3"; */ -"Ab2-sI-Ld3.placeholderString" = "Target Relay Channel Name"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "5CP-OV-fSv"; */ +"5CP-OV-fSv.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Stop Relay"; ObjectID = "Hvn-10-7hC"; */ -"Hvn-10-7hC.title" = "Stop Relay"; +/* Element = "viewController"; title = "Media Channel Relay"; ObjectID = "IBJ-wZ-9Xx"; */ +"IBJ-wZ-9Xx.title" = "跨频道流转发"; -/* Class = "NSViewController"; title = "Join Multiple Channels"; ObjectID = "IBJ-wZ-9Xx"; */ -"IBJ-wZ-9Xx.title" = "Join Multiple Channels"; +/* Element = "buttonCell"; title = "Start Relay"; ObjectID = "NQw-hk-hUl"; */ +"NQw-hk-hUl.title" = "开始转发"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Xtr-fU-GZ5"; */ -"Xtr-fU-GZ5.title" = "Leave Channel"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Zjl-Vt-wOj"; */ -"Zjl-Vt-wOj.title" = "Join Channel"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "p0a-zy-yqS"; */ -"p0a-zy-yqS.placeholderString" = "Enter Channel Name"; - -/* Class = "NSButtonCell"; title = "Start Relay"; ObjectID = "u6j-cJ-1Pe"; */ -"u6j-cJ-1Pe.title" = "Start Relay"; +/* Element = "box"; title = "Box"; ObjectID = "YuQ-EP-LeE"; */ +"YuQ-EP-LeE.title" = "Box"; 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/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/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/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/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings b/macOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings index 1f620e2b1..e663ed256 100644 --- a/macOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings +++ b/macOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings @@ -1,12 +1,11 @@ - -/* Class = "NSBox"; title = "Box"; ObjectID = "I4o-9l-2Vv"; */ +/* Element = "box"; title = "Box"; ObjectID = "I4o-9l-2Vv"; */ "I4o-9l-2Vv.title" = "Box"; -/* Class = "NSButtonCell"; title = "Send"; ObjectID = "eYM-ow-8en"; */ -"eYM-ow-8en.title" = "Send"; +/* Element = "buttonCell"; title = "Send"; ObjectID = "eYM-ow-8en"; */ +"eYM-ow-8en.title" = "发送"; -/* Class = "NSTextFieldCell"; title = "Received Messages"; ObjectID = "mGf-09-ljc"; */ -"mGf-09-ljc.title" = "Received Messages"; +/* Element = "textFieldCell"; title = "Received Messages"; ObjectID = "mGf-09-ljc"; */ +"mGf-09-ljc.title" = "收到的消息"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "mmH-hT-gAv"; */ -"mmH-hT-gAv.title" = "Join Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "mmH-hT-gAv"; */ +"mmH-hT-gAv.title" = "加入频道"; 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/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings b/macOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings index dceb1c9a7..44a496cb6 100644 --- a/macOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings +++ b/macOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings @@ -1,24 +1,8 @@ +/* Element = "box"; title = "Box"; ObjectID = "NGa-Jk-hrb"; */ +"NGa-Jk-hrb.title" = "Box"; -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "0pq-4D-qgt"; */ -"0pq-4D-qgt.title" = "1V8"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "OlB-dA-sCQ"; */ +"OlB-dA-sCQ.title" = "加入频道"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "J5P-DD-2lM"; */ -"J5P-DD-2lM.title" = "1V1"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "LpP-rx-fDz"; */ -"LpP-rx-fDz.title" = "Join Channel"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "Q9k-KS-Bb9"; */ -"Q9k-KS-Bb9.title" = "1V3"; - -/* Class = "NSViewController"; title = "Custom Audio Render"; ObjectID = "rPb-ur-msx"; */ -"rPb-ur-msx.title" = "Custom Audio Render"; - -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "sav-ba-mHX"; */ -"sav-ba-mHX.title" = "Leave Channel"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "uZ0-mF-1r9"; */ -"uZ0-mF-1r9.placeholderString" = "Enter Channel Name"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "zRn-Ca-xYL"; */ -"zRn-Ca-xYL.title" = "1V15"; +/* Element = "viewController"; title = "Custom Audio Render"; ObjectID = "rPb-ur-msx"; */ +"rPb-ur-msx.title" = "音频自渲染"; 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/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/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings b/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings index b545182ce..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,26 +1,11 @@ +/* Element = "viewController"; title = "Custom Audio Source (PCM)"; ObjectID = "8Q5-xy-D8A"; */ +"8Q5-xy-D8A.title" = "音频自采集(PCM)"; -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "5Bj-Be-5dr"; */ -"5Bj-Be-5dr.title" = "1V3"; +/* Element = "textFieldCell"; title = "Play the local PCM"; ObjectID = "UzT-xh-vr5"; */ +"UzT-xh-vr5.title" = "播放本地 PCM"; -/* Class = "NSViewController"; title = "Custom Audio Source"; ObjectID = "8Q5-xy-D8A"; */ -"8Q5-xy-D8A.title" = "Custom Audio Source"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "faM-cy-24G"; */ +"faM-cy-24G.title" = "加入频道"; -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "9hh-5D-rEK"; */ -"9hh-5D-rEK.placeholderString" = "Enter Channel Name"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "CkQ-CH-Xcd"; */ -"CkQ-CH-Xcd.title" = "1V8"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "MCC-IO-OYe"; */ -"MCC-IO-OYe.title" = "Join Channel"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Wpu-17-eWW"; */ -"Wpu-17-eWW.title" = "1V15"; - -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "dNt-Gv-ohJ"; */ -"dNt-Gv-ohJ.title" = "Leave Channel"; - -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "yKw-5m-DrZ"; */ -"yKw-5m-DrZ.title" = "1V1"; - -"UzT-xh-vr5.title" = "Play Local PCM"; +/* Element = "box"; title = "Box"; ObjectID = "vPk-Qe-fY8"; */ +"vPk-Qe-fY8.title" = "Box"; 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/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/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings b/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings index 743a7b4c9..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,24 +1,8 @@ +/* Element = "viewController"; title = "Custom Video Render"; ObjectID = "jEL-F4-BwV"; */ +"jEL-F4-BwV.title" = "视频自渲染(Metal)"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "4f5-cK-Lrg"; */ -"4f5-cK-Lrg.title" = "Join Channel"; +/* Element = "box"; title = "Box"; ObjectID = "jwa-os-AjB"; */ +"jwa-os-AjB.title" = "Box"; -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "8JX-YX-iAW"; */ -"8JX-YX-iAW.title" = "1V15"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "PpQ-ki-MC0"; */ -"PpQ-ki-MC0.title" = "1V8"; - -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "hzs-Vp-M59"; */ -"hzs-Vp-M59.title" = "1V1"; - -/* Class = "NSViewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "jEL-F4-BwV"; */ -"jEL-F4-BwV.title" = "Custom Audio Render"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "q4U-yg-aWx"; */ -"q4U-yg-aWx.title" = "1V3"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "xtu-Fh-nL8"; */ -"xtu-Fh-nL8.placeholderString" = "Enter Channel Name"; - -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "z6I-ve-sPC"; */ -"z6I-ve-sPC.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "pc2-jp-urQ"; */ +"pc2-jp-urQ.title" = "加入频道"; 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/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/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/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings index 07227886c..028389319 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings @@ -1,24 +1,11 @@ +/* Element = "box"; title = "Box"; ObjectID = "Oiu-jF-2jj"; */ +"Oiu-jF-2jj.title" = "Box"; -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "KSj-Qd-L7B"; */ -"KSj-Qd-L7B.placeholderString" = "Enter Channel Name"; +/* Element = "textFieldCell"; title = "HDR"; ObjectID = "SL7-Vu-fdB"; */ +"SL7-Vu-fdB.title" = "HDR"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "XQ9-2H-aV1"; */ -"XQ9-2H-aV1.title" = "Join Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "bCD-1H-KfP"; */ +"bCD-1H-KfP.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "esh-Yv-lrq"; */ -"esh-Yv-lrq.title" = "Leave Channel"; - -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "lxe-dD-iYs"; */ -"lxe-dD-iYs.title" = "1V1"; - -/* Class = "NSViewController"; title = "Custom Video Source (Push)"; ObjectID = "sXF-vm-Rrb"; */ -"sXF-vm-Rrb.title" = "Custom Video Source (Push)"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "tBU-fM-94k"; */ -"tBU-fM-94k.title" = "1V15"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "ukW-YV-Pc0"; */ -"ukW-YV-Pc0.title" = "1V8"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "z6y-AQ-Yeq"; */ -"z6y-AQ-Yeq.title" = "1V3"; +/* 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/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/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings index 9b82fdaad..5b30cad67 100644 --- a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePushMulti/zh-Hans.lproj/CustomVideoSourcePushMulti.strings @@ -1,26 +1,17 @@ +/* Element = "box"; title = "Box"; ObjectID = "Oiu-jF-2jj"; */ +"Oiu-jF-2jj.title" = "Box"; -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "KSj-Qd-L7B"; */ -"KSj-Qd-L7B.placeholderString" = "Enter Channel Name"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "bCD-1H-KfP"; */ +"bCD-1H-KfP.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "XQ9-2H-aV1"; */ -"XQ9-2H-aV1.title" = "Join Channel"; +/* Element = "buttonCell"; title = "Create Video Track(Encode)"; ObjectID = "c9C-Yd-1NT"; */ +"c9C-Yd-1NT.title" = "创建采集Track(编码)"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "esh-Yv-lrq"; */ -"esh-Yv-lrq.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Destory Track"; ObjectID = "fY0-1R-m7R"; */ +"fY0-1R-m7R.title" = "销毁Track"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "lxe-dD-iYs"; */ -"lxe-dD-iYs.title" = "1V1"; +/* Element = "viewController"; title = "Multiple Video Sources"; ObjectID = "sXF-vm-Rrb"; */ +"sXF-vm-Rrb.title" = "多路视频源"; -/* Class = "NSViewController"; title = "Custom Video Source (Push)"; ObjectID = "sXF-vm-Rrb"; */ -"sXF-vm-Rrb.title" = "Custom Audio Source (Push)"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "tBU-fM-94k"; */ -"tBU-fM-94k.title" = "1V15"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "ukW-YV-Pc0"; */ -"ukW-YV-Pc0.title" = "1V8"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "z6y-AQ-Yeq"; */ -"z6y-AQ-Yeq.title" = "1V3"; - -"c9C-Yd-1NT.title" = "Create Capture Track (Encoded)"; +/* Element = "buttonCell"; title = "Create VideoTrack"; ObjectID = "tbi-87-lo2"; */ +"tbi-87-lo2.title" = "创建采集Track"; diff --git a/macOS/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard b/macOS/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard deleted file mode 100644 index 1c1694420..000000000 --- a/macOS/APIExample/Examples/Advanced/FaceCapture/Base.lproj/FaceCapture.storyboard +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 b1851100b..000000000 --- a/macOS/APIExample/Examples/Advanced/FaceCapture/zh-Hans.lproj/FaceCapture.strings +++ /dev/null @@ -1,24 +0,0 @@ - -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "4rc-r1-Ay6"; */ -"4rc-r1-Ay6.title" = "Leave Channel"; - -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "Iws-j3-l2h"; */ -"Iws-j3-l2h.title" = "1V1"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Mmi-d8-vOm"; */ -"Mmi-d8-vOm.title" = "1V15"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "PtD-n2-sEW"; */ -"PtD-n2-sEW.placeholderString" = "Enter Channel Name"; - -/* 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 = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ -"cH4-ft-u77.title" = "1V8"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "guU-jX-Wkg"; */ -"guU-jX-Wkg.title" = "Join Channel"; 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/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings index 3c44ad060..ddcac7ccc 100644 --- a/macOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings +++ b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings @@ -1,27 +1,17 @@ +/* Element = "textFieldCell"; title = "StopMicrophoneRecording"; ObjectID = "8xH-sF-b4v"; */ +"8xH-sF-b4v.title" = "停止麦克风录制"; -/* Class = "NSTextFieldCell"; placeholderString = "Channel Name 2"; ObjectID = "Ab2-sI-Ld3"; */ -"Ab2-sI-Ld3.placeholderString" = "Enter Channel Name 2"; +/* Element = "viewController"; title = "Join Multiple Channels"; ObjectID = "IBJ-wZ-9Xx"; */ +"IBJ-wZ-9Xx.title" = "加入多频道"; -/* Class = "NSButtonCell"; title = "Unpublish"; ObjectID = "Hvn-10-7hC"; */ -"Hvn-10-7hC.title" = "Stop Publishing"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "MNt-M8-kuV"; */ +"MNt-M8-kuV.title" = "加入频道"; -/* Class = "NSViewController"; title = "Join Multiple Channels"; ObjectID = "IBJ-wZ-9Xx"; */ -"IBJ-wZ-9Xx.title" = "Join Multiple Channels"; +/* Element = "box"; title = "Box"; ObjectID = "opZ-iY-E6e"; */ +"opZ-iY-E6e.title" = "Box"; -/* Class = "NSButtonCell"; title = "Publish"; ObjectID = "Rau-85-whm"; */ -"Rau-85-whm.title" = "Publish"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "tEg-NJ-QGq"; */ +"tEg-NJ-QGq.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Xtr-fU-GZ5"; */ -"Xtr-fU-GZ5.title" = "Leave Channel"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Zjl-Vt-wOj"; */ -"Zjl-Vt-wOj.title" = "Join Channel"; - -/* Class = "NSTextFieldCell"; placeholderString = "Channel Name 1"; ObjectID = "p0a-zy-yqS"; */ -"p0a-zy-yqS.placeholderString" = "Enter Channel Name 1"; - -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "ttd-9y-14q"; */ -"ttd-9y-14q.title" = "Leave Ex Channel"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "u6j-cJ-1Pe"; */ -"OwT-ZJ-qpk.title" = "Join Ex Channel"; +/* Element = "buttonCell"; title = "take snapshot ex"; ObjectID = "wzZ-2k-bnO"; */ +"wzZ-2k-bnO.title" = "截图"; 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/LiveStreaming.swift b/macOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift index e695db3d0..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") } }) @@ -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() @@ -665,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) @@ -675,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/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 8a0f24487..bd7713af1 100644 --- a/macOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings +++ b/macOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings @@ -1,43 +1,43 @@ /* Class = "NSTextFieldCell"; title = "water"; ObjectID = "8YN-Yd-UZv"; */ -"Srd-aW-lWt.title" = "Watermark"; +"Srd-aW-lWt.title" = "水印"; /* Class = "NSBox"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ "BP9-4w-AfJ.title" = "Box"; /* Class = "NSSegmentedCell"; D2B-fw-Vnp.ibShadowedLabels[0] = "Auto"; ObjectID = "D2B-fw-Vnp"; */ -"2nu-cG-poA.ibShadowedLabels[0]" = "Auto"; +"2nu-cG-poA.ibShadowedLabels[0]" = "自动"; /* Class = "NSSegmentedCell"; D2B-fw-Vnp.ibShadowedLabels[1] = "Soft knitting"; ObjectID = "D2B-fw-Vnp"; */ -"2nu-cG-poA.ibShadowedLabels[1]" = "Software Encoding"; +"2nu-cG-poA.ibShadowedLabels[1]" = "软编"; /* Class = "NSSegmentedCell"; D2B-fw-Vnp.ibShadowedLabels[2] = "Hard knitting"; ObjectID = "D2B-fw-Vnp"; */ -"2nu-cG-poA.ibShadowedLabels[2]" = "Hardware Encoding"; +"2nu-cG-poA.ibShadowedLabels[2]" = "硬编"; /* Class = "NSButtonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ -"Lhu-U1-6qh.title" = "Join Channel"; +"Lhu-U1-6qh.title" = "加入频道"; /* Class = "NSTextFieldCell"; title = "first frame is drawn"; ObjectID = "PDX-e5-ZpY"; */ -"ZlG-nb-3NY.title" = "First Frame"; +"ZlG-nb-3NY.title" = "首帧出图"; /* Class = "NSTextFieldCell"; title = "S or l flow"; ObjectID = "XH3-Ib-cXr"; */ -"zOU-d9-jDn.title" = "Dual Stream"; +"zOU-d9-jDn.title" = "大小流"; -/* 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" = "RTC实时直播/主播/观众"; /* Class = "NSButtonCell"; title = "Snap Shot"; ObjectID = "cDh-B1-x3E"; */ -"hca-Of-3bM.title" = "Screenshot"; +"hca-Of-3bM.title" = "截图"; /* Class = "NSTextFieldCell"; title = "(Default: flow)"; ObjectID = "d9V-RQ-OX6"; */ -"G8a-XO-yaN.title" = "(Default: High Stream)"; +"G8a-XO-yaN.title" = "(默认: 大流)"; /* Class = "NSTextFieldCell"; title = "BF"; ObjectID = "gt3-r0-jqt"; */ -"Prc-Ti-Ayl.title" = "B Frame"; +"Prc-Ti-Ayl.title" = "B帧"; -"gCs-hv-sr4.title" = "Preload"; +"gCs-hv-sr4.title" = "预加载"; -"gBn-zJ-ZES.title" = "Padding Stream"; +"gBn-zJ-ZES.title" = "垫片推流"; -"iuE-dA-CLg.placeholderString"="Remote render frame rate (1-60), default 15"; -"dbV-BZ-hXR.placeholderString"="Local render frame rate (1-60), default 15"; +"iuE-dA-CLg.placeholderString"= "远端渲染帧率(1-60),默认15"; +"dbV-BZ-hXR.placeholderString"= "本地渲染帧率(1-60),默认15"; 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/LocalVideoTranscoding.swift b/macOS/APIExample/Examples/Advanced/LocalVideoTranscoding/LocalVideoTranscoding.swift index 46b308f40..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 @@ -355,8 +357,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 +367,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/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/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/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings b/macOS/APIExample/Examples/Advanced/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings index 5348a4b63..afb54a7a0 100644 --- a/macOS/APIExample/Examples/Advanced/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings +++ b/macOS/APIExample/Examples/Advanced/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings @@ -1,29 +1,35 @@ +/* Element = "buttonCell"; title = "Stop"; ObjectID = "5JL-UH-JLr"; */ +"5JL-UH-JLr.title" = "停止"; -/* Class = "NSButtonCell"; title = "Stop"; ObjectID = "5JL-UH-JLr"; */ -"5JL-UH-JLr.title" = "Stop"; - -/* Class = "NSBox"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ "BP9-4w-AfJ.title" = "Box"; -/* Class = "NSButtonCell"; title = "Pause"; ObjectID = "D7O-6d-Yj4"; */ -"D7O-6d-Yj4.title" = "Pause"; +/* Element = "buttonCell"; title = "AudioTrack1"; ObjectID = "C42-tM-xyI"; */ +"C42-tM-xyI.title" = "音轨 1"; + +/* Element = "buttonCell"; title = "Pause"; ObjectID = "D7O-6d-Yj4"; */ +"D7O-6d-Yj4.title" = "暂停"; + +/* Element = "textFieldCell"; title = "publish AudioTrack"; ObjectID = "E2w-Ng-jzG"; */ +"E2w-Ng-jzG.title" = "发布音轨"; + +/* Element = "buttonCell"; title = "Stp Pushing"; ObjectID = "F6H-KV-6va"; */ +"F6H-KV-6va.title" = "停止推流"; -/* Class = "NSButtonCell"; title = "Stp Pushing"; ObjectID = "F6H-KV-6va"; */ -"F6H-KV-6va.title" = "Stop Pushing"; +/* Element = "textFieldCell"; title = "play AudioTrack"; ObjectID = "GkQ-Np-rjU"; */ +"GkQ-Np-rjU.title" = "播放音轨"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ -"Lhu-U1-6qh.title" = "Join Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Start Pushing"; ObjectID = "TUJ-eJ-Vx7"; */ -"TUJ-eJ-Vx7.title" = "Start Pushing"; +/* Element = "buttonCell"; title = "Start Pushing"; ObjectID = "TUJ-eJ-Vx7"; */ +"TUJ-eJ-Vx7.title" = "开始推流"; -/* Class = "NSViewController"; title = "Media Player"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Media Player"; +/* Element = "viewController"; title = "Media Player"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "流媒体播放器"; -/* Class = "NSButtonCell"; title = "Play"; ObjectID = "a6R-z9-GrM"; */ -"a6R-z9-GrM.title" = "Play"; +/* Element = "buttonCell"; title = "Play"; ObjectID = "a6R-z9-GrM"; */ +"a6R-z9-GrM.title" = "播放"; -"GkQ-Np-rjU.title" = "Play Audio Track"; -"E2w-Ng-jzG.title" = "Push Audio Track"; -"C42-tM-xyI.title" = "Audio Track 1"; -"bh9-p5-hnL.title" = "Audio Track 1"; +/* Element = "buttonCell"; title = "AudioTrack1"; ObjectID = "bh9-p5-hnL"; */ +"bh9-p5-hnL.title" = "音轨 1"; 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/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/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings index 4c291978d..b284b381f 100644 --- a/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings +++ b/macOS/APIExample/Examples/Advanced/MultiCameraSourece/zh-Hans.lproj/MultiCameraSourece.strings @@ -1,24 +1,32 @@ +/* Element = "buttonCell"; title = "Start streaming"; ObjectID = "0fK-HU-248"; */ +"0fK-HU-248.title" = "开始推流"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "4rc-r1-Ay6"; */ -"4rc-r1-Ay6.title" = "Leave Channel"; +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +"BP9-4w-AfJ.title" = "Box"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "Iws-j3-l2h"; */ -"Iws-j3-l2h.title" = "1V1"; +/* Element = "buttonCell"; title = "Start streaming"; ObjectID = "JuU-kX-SM0"; */ +"JuU-kX-SM0.title" = "开始推流"; -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Mmi-d8-vOm"; */ -"Mmi-d8-vOm.title" = "1V15"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSTextFieldCell"; placeholderString = "join Channel"; ObjectID = "PtD-n2-sEW"; */ -"PtD-n2-sEW.placeholderString" = "Enter Channel Name"; +/* Element = "buttonCell"; title = "Start streaming"; ObjectID = "NaG-XB-zRS"; */ +"NaG-XB-zRS.title" = "开始推流"; -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "VNU-so-ajb"; */ -"VNU-so-ajb.title" = "1V3"; +/* Element = "buttonCell"; title = "Start collection"; ObjectID = "UNg-f5-Jxh"; */ +"UNg-f5-Jxh.title" = "开始采集"; -/* Class = "NSViewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Video Call/Live Streaming"; +/* Element = "viewController"; title = "Multi Camera"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "多路摄像头"; -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ -"cH4-ft-u77.title" = "1V8"; +/* Element = "buttonCell"; title = "Start collecting"; ObjectID = "irM-5L-9uT"; */ +"irM-5L-9uT.title" = "开始采集"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "guU-jX-Wkg"; */ -"guU-jX-Wkg.title" = "Join Channel"; +/* Element = "buttonCell"; title = "Start collecting"; ObjectID = "n4E-Pl-sJq"; */ +"n4E-Pl-sJq.title" = "开始采集"; + +/* Element = "buttonCell"; title = "Start streaming"; ObjectID = "u8r-u9-ylV"; */ +"u8r-u9-ylV.title" = "开始推流"; + +/* Element = "buttonCell"; title = "Start collecting"; ObjectID = "wsu-5T-ZW5"; */ +"wsu-5T-ZW5.title" = "开始采集"; 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/Multipath/zh-Hans.lproj/Multipath.strings b/macOS/APIExample/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings new file mode 100644 index 000000000..e38cfca2c --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/Multipath/zh-Hans.lproj/Multipath.strings @@ -0,0 +1,11 @@ +/* Element = "box"; title = "Box"; ObjectID = "AaY-nf-6bC"; */ +"AaY-nf-6bC.title" = "Box"; + +/* Element = "buttonCell"; title = "Join"; ObjectID = "PJX-oJ-utz"; */ +"PJX-oJ-utz.title" = "加入频道"; + +/* Element = "textFieldCell"; title = "Enable Multipath"; ObjectID = "Vkx-Fw-cJJ"; */ +"Vkx-Fw-cJJ.title" = "启用多路径"; + +/* Element = "viewController"; title = "Multipath"; ObjectID = "zLf-Ex-oud"; */ +"zLf-Ex-oud.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/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/PrecallTest/zh-Hans.lproj/PrecallTest.strings b/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings index d4d80617b..a10473ab8 100644 --- a/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings @@ -1,47 +1,47 @@ +/* Element = "buttonCell"; title = "Stop Test"; ObjectID = "4f3-Ea-NwT"; */ +"4f3-Ea-NwT.title" = "停止测试"; -/* Class = "NSButtonCell"; title = "Stop Test"; ObjectID = "4f3-Ea-NwT"; */ -"4f3-Ea-NwT.title" = "Stop Test"; +/* Element = "buttonCell"; title = "Start Test"; ObjectID = "4z6-Jy-1cc"; */ +"4z6-Jy-1cc.title" = "开始测试"; -/* Class = "NSButtonCell"; title = "Start Test"; ObjectID = "4z6-Jy-1cc"; */ -"4z6-Jy-1cc.title" = "Start Test"; +/* Element = "buttonCell"; title = "Start Test"; ObjectID = "5jA-zT-2bv"; */ +"5jA-zT-2bv.title" = "开始测试"; -/* Class = "NSButtonCell"; title = "Start Test"; ObjectID = "5jA-zT-2bv"; */ -"5jA-zT-2bv.title" = "Start Test"; +/* Element = "buttonCell"; title = "Stop Audio Device Loopback Test"; ObjectID = "BJO-I0-Opi"; */ +"BJO-I0-Opi.title" = "停止本地音频回路测试"; -/* Class = "NSButtonCell"; title = "Stop Audio Device Loopback Test"; ObjectID = "BJO-I0-Opi"; */ -"BJO-I0-Opi.title" = "Stop Audio Device Loopback Test"; +/* Element = "textFieldCell"; title = "Please say something.."; ObjectID = "BPe-Gx-enC"; */ +"BPe-Gx-enC.title" = "尝试说一些话..."; -/* Class = "NSTextFieldCell"; title = "Please say something.."; ObjectID = "BPe-Gx-enC"; */ -"BPe-Gx-enC.title" = "Please say something..."; +/* Element = "viewController"; title = "Pre-call Test"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "通话前网络/设备测试"; -/* Class = "NSViewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "Precall Test"; - -/* Class = "NSTextFieldCell"; title = "10"; ObjectID = "L6F-q4-SNZ"; */ +/* Element = "textFieldCell"; title = "10"; ObjectID = "L6F-q4-SNZ"; */ "L6F-q4-SNZ.title" = "10"; -/* Class = "NSTextFieldCell"; title = "Now you should hear what you said..."; ObjectID = "Yjn-ei-T3i"; */ -"Yjn-ei-T3i.title" = "Now you should hear what you said in the last 10 seconds..."; +/* Element = "buttonCell"; title = "Start Video Echo Test"; ObjectID = "Nkf-bY-zGl"; */ +"Nkf-bY-zGl.title" = "开始视频回路测试"; -/* Class = "NSTextFieldCell"; title = "10"; ObjectID = "aQJ-oH-NdD"; */ -"aQJ-oH-NdD.title" = "10"; +/* Element = "textFieldCell"; title = "Now you should hear what you said..."; ObjectID = "Yjn-ei-T3i"; */ +"Yjn-ei-T3i.title" = "现在你应该能听到前10秒的声音..."; -/* Class = "NSButtonCell"; title = "Stop Test"; ObjectID = "bGT-vl-2FZ"; */ -"bGT-vl-2FZ.title" = "Stop Test"; +/* Element = "textFieldCell"; title = "10"; ObjectID = "aQJ-oH-NdD"; */ +"aQJ-oH-NdD.title" = "10"; -/* Class = "NSButtonCell"; title = "Start Echo Test"; ObjectID = "cTC-4D-0SS"; */ -"cTC-4D-0SS.title" = "Start Echo Test"; +/* Element = "buttonCell"; title = "Stop Test"; ObjectID = "bGT-vl-2FZ"; */ +"bGT-vl-2FZ.title" = "停止测试"; -"Nkf-bY-zGl.title" = "Start Video Loopback Test"; +/* Element = "buttonCell"; title = "Start Audio Echo Test"; ObjectID = "cTC-4D-0SS"; */ +"cTC-4D-0SS.title" = "开始音频回声测试"; -/* Class = "NSButtonCell"; title = "Start Audio Device Loopback Test"; ObjectID = "fhC-uz-lo8"; */ -"fhC-uz-lo8.title" = "Start Audio Device Loopback Test"; +/* Element = "buttonCell"; title = "Start Audio Device Loopback Test"; ObjectID = "fhC-uz-lo8"; */ +"fhC-uz-lo8.title" = "开始本地音频回路测试"; -/* Class = "NSButtonCell"; title = "Start Lastmile Test"; ObjectID = "flT-Cc-shZ"; */ -"flT-Cc-shZ.title" = "Start Lastmile Network Test"; +/* Element = "buttonCell"; title = "Start Lastmile Test"; ObjectID = "flT-Cc-shZ"; */ +"flT-Cc-shZ.title" = "开始Lastmile网络测试"; -/* Class = "NSButtonCell"; title = "Stop Test"; ObjectID = "oar-3q-rdY"; */ -"oar-3q-rdY.title" = "Stop Test"; +/* Element = "buttonCell"; title = "Stop Test"; ObjectID = "oar-3q-rdY"; */ +"oar-3q-rdY.title" = "停止测试"; -/* Class = "NSButtonCell"; title = "Start Test"; ObjectID = "xsZ-UP-eoO"; */ -"xsZ-UP-eoO.title" = "Start Test"; +/* Element = "buttonCell"; title = "Start Test"; ObjectID = "xsZ-UP-eoO"; */ +"xsZ-UP-eoO.title" = "开始测试"; 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/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/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 d2620b099..4a642f40a 100644 --- a/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/zh-Hans.lproj/QuickSwitchChannel.strings +++ b/macOS/APIExample/Examples/Advanced/QuickSwitchChannel/zh-Hans.lproj/QuickSwitchChannel.strings @@ -1,12 +1,11 @@ +/* Element = "buttonCell"; title = "Switch Channel"; ObjectID = "3HM-bj-L6q"; */ +"3HM-bj-L6q.title" = "切换频道"; -/* Class = "NSButtonCell"; title = "Switch Channel"; ObjectID = "3HM-bj-L6q"; */ -"3HM-bj-L6q.title" = "Switch Channel"; - -/* Class = "NSBox"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ "BP9-4w-AfJ.title" = "Box"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ -"Lhu-U1-6qh.title" = "Join"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSViewController"; title = "Raw Video Data"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Raw Video Data"; +/* Element = "viewController"; title = "Quick Switch Channel"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "快速切换频道"; 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/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings b/macOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings index f0fedcf04..201b9591d 100644 --- a/macOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings +++ b/macOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings @@ -1,27 +1,20 @@ +/* Element = "buttonCell"; title = "Join"; ObjectID = "08h-NS-LKN"; */ +"08h-NS-LKN.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "06A-fH-QIv"; */ -"06A-fH-QIv.title" = "Join Channel"; +/* Element = "box"; title = "Box"; ObjectID = "8xw-Re-siW"; */ +"8xw-Re-siW.title" = "Box"; -/* Class = "NSTextFieldCell"; placeholderString = "rtmp://"; ObjectID = "LvF-qW-J2U"; */ -"LvF-qW-J2U.placeholderString" = "rtmp://"; +/* Element = "buttonCell"; title = "Add Streaming URL"; ObjectID = "LwR-8Z-de2"; */ +"LwR-8Z-de2.title" = "添加推流地址"; -/* Class = "NSButtonCell"; title = "Add Streaming URL"; ObjectID = "LwR-8Z-de2"; */ -"LwR-8Z-de2.title" = "Add Streaming URL"; +/* Element = "viewController"; title = "RTMP Streaming"; ObjectID = "aK7-YG-lDw"; */ +"aK7-YG-lDw.title" = "RTMP旁路推流"; -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "UGj-Te-IEu"; */ -"UGj-Te-IEu.placeholderString" = "Enter Channel Name"; +/* Element = "buttonCell"; title = "Remove All"; ObjectID = "oLm-T5-8kd"; */ +"oLm-T5-8kd.title" = "移除所有地址"; -/* Class = "NSViewController"; title = "RTMP Streaming"; ObjectID = "aK7-YG-lDw"; */ -"aK7-YG-lDw.title" = "RTMP Streaming"; +/* Element = "buttonCell"; title = "Remove Streaming URL"; ObjectID = "wDa-VN-Rvd"; */ +"wDa-VN-Rvd.title" = "移除推流地址"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "dYR-6U-xkr"; */ -"dYR-6U-xkr.title" = "Leave Channel"; - -/* Class = "NSButtonCell"; title = "Remove All"; ObjectID = "oLm-T5-8kd"; */ -"oLm-T5-8kd.title" = "Remove All URLs"; - -/* Class = "NSButtonCell"; title = "Remove Streaming URL"; ObjectID = "wDa-VN-Rvd"; */ -"wDa-VN-Rvd.title" = "Remove Streaming URL"; - -/* Class = "NSButtonCell"; title = "Transcoding"; ObjectID = "yMt-d6-3US"; */ -"yMt-d6-3US.title" = "Transcoding"; +/* Element = "buttonCell"; title = "Transcoding"; ObjectID = "yMt-d6-3US"; */ +"yMt-d6-3US.title" = "转码"; 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/RtePlayer/zh-Hans.lproj/RtePlayer.strings b/macOS/APIExample/Examples/Advanced/RtePlayer/zh-Hans.lproj/RtePlayer.strings index a75c25932..c031c548c 100644 --- a/macOS/APIExample/Examples/Advanced/RtePlayer/zh-Hans.lproj/RtePlayer.strings +++ b/macOS/APIExample/Examples/Advanced/RtePlayer/zh-Hans.lproj/RtePlayer.strings @@ -1,7 +1,5 @@ +/* Element = "box"; title = "Box"; ObjectID = "I4o-9l-2Vv"; */ +"I4o-9l-2Vv.title" = "Box"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "mmH-hT-gAv"; */ -"I4o-9l-2Vv.title" = "Please add the correct channel at the end of the URL"; - -"mmH-hT-gAv.title" = "Play"; - -"RaA-AV-UE8.title" = "Please use another device as a host to start a live channel, and fill in the corresponding channel name in the input box above."; +/* Element = "buttonCell"; title = "Join"; ObjectID = "mmH-hT-gAv"; */ +"mmH-hT-gAv.title" = "请在url末尾添加正确的频道"; 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/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/ScreenShare/zh-Hans.lproj/ScreenShare.strings b/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings index 6d845ff54..e26ea7277 100644 --- a/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings @@ -1,45 +1,23 @@ +/* Element = "buttonCell"; title = "Join"; ObjectID = "BfM-1S-adU"; */ +"BfM-1S-adU.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "1ik-om-mWj"; */ -"1ik-om-mWj.title" = "Join Channel"; +/* Element = "buttonCell"; title = "Generate thumbnail"; ObjectID = "G2K-yC-ti4"; */ +"G2K-yC-ti4.title" = "生成缩略图"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "6f9-0B-egB"; */ -"6f9-0B-egB.title" = "1V1"; +/* Element = "buttonCell"; title = "Window Share"; ObjectID = "GY6-hd-Bf5"; */ +"GY6-hd-Bf5.title" = "窗口共享"; -/* Class = "NSButtonCell"; title = "Display Share"; ObjectID = "ACV-0l-kRZ"; */ -"ACV-0l-kRZ.title" = "Display Share"; +/* Element = "viewController"; title = "Screen Share"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "屏幕共享"; -/* Class = "NSViewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "Stream Encryption"; +/* Element = "buttonCell"; title = "Display Share"; ObjectID = "SAX-wa-nOm"; */ +"SAX-wa-nOm.title" = "屏幕共享"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Owt-vb-7U9"; */ -"Owt-vb-7U9.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Share Half Screen"; ObjectID = "W6S-ui-VuF"; */ +"W6S-ui-VuF.title" = "分享部分区域"; -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "S4i-eh-YzK"; */ -"S4i-eh-YzK.title" = "1V3"; +/* Element = "box"; title = "Box"; ObjectID = "YrW-3X-cWk"; */ +"YrW-3X-cWk.title" = "Box"; -/* Class = "NSButtonCell"; title = "Stop Share"; ObjectID = "TlR-ef-9cf"; */ -"TlR-ef-9cf.title" = "Stop Share"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "aj5-Fn-je9"; */ -"aj5-Fn-je9.placeholderString" = "Enter Channel Name"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "cxo-X2-S8L"; */ -"cxo-X2-S8L.title" = "1V15"; - -/* Class = "NSButtonCell"; title = "Window Share"; ObjectID = "ftv-L5-p8U"; */ -"ftv-L5-p8U.title" = "Window Share"; - -/* Class = "NSButtonCell"; title = "Stop Share"; ObjectID = "ka7-2T-SiW"; */ -"ka7-2T-SiW.title" = "Stop Share"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "zu1-vg-leG"; */ -"zu1-vg-leG.title" = "1V8"; - -/* Class = "NSButtonCell"; title = "Share Half Screen"; ObjectID = "0Ao-Fe-BEt"; */ -"0Ao-Fe-BEt.title" = "Share Partial Screen"; - -/* Class = "NSButtonCell"; title = "Update Config"; ObjectID = "siB-l9-qc1"; */ -"siB-l9-qc1.title" = "Update Parameters"; - -"G2K-yC-ti4.title" = "Generate Thumbnail"; -"ghC-jR-1O0.title" = "Generate Thumbnail"; +/* Element = "buttonCell"; title = "Generate thumbnail"; ObjectID = "ghC-jR-1O0"; */ +"ghC-jR-1O0.title" = "生成缩略图"; diff --git a/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift b/macOS/APIExample/Examples/Advanced/SimpleFilter/SimpleFilter.swift index 447b92828..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) @@ -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/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/Simulcast/zh-Hans.lproj/Simulcast.strings b/macOS/APIExample/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings new file mode 100644 index 000000000..526bece0f --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/Simulcast/zh-Hans.lproj/Simulcast.strings @@ -0,0 +1,23 @@ +/* Element = "textFieldCell"; title = "Layer3:360p15fps"; ObjectID = "4r1-Ls-YfS"; */ +"4r1-Ls-YfS.title" = "Layer3:360p15fps"; + +/* Element = "textFieldCell"; title = "Layer4:270p15fps"; ObjectID = "8mp-Eu-o0G"; */ +"8mp-Eu-o0G.title" = "Layer4:270p15fps"; + +/* Element = "textFieldCell"; title = "Set Layers Config(Maxmum 3)"; ObjectID = "CI0-1k-Bf7"; */ +"CI0-1k-Bf7.title" = "配置多流(最大3个)"; + +/* Element = "viewController"; title = "Simulcast"; ObjectID = "Nyy-xX-po7"; */ +"Nyy-xX-po7.title" = "同源多流"; + +/* Element = "textFieldCell"; title = "Layer2:540p15fps"; ObjectID = "UJX-xR-w0P"; */ +"UJX-xR-w0P.title" = "Layer2:540p15fps"; + +/* Element = "textFieldCell"; title = "Layer1:720p30fps"; ObjectID = "nJw-cK-RYU"; */ +"nJw-cK-RYU.title" = "Layer1:720p30fps"; + +/* Element = "buttonCell"; title = "Join"; ObjectID = "tfR-14-W9p"; */ +"tfR-14-W9p.title" = "加入频道"; + +/* Element = "box"; title = "Box"; ObjectID = "yeh-Nx-xDB"; */ +"yeh-Nx-xDB.title" = "Box"; diff --git a/macOS/APIExample/Examples/Advanced/SpatialAudio/zh-Hans.lproj/SpatialAudio.strings b/macOS/APIExample/Examples/Advanced/SpatialAudio/zh-Hans.lproj/SpatialAudio.strings index f74d10db8..15825ad28 100644 --- a/macOS/APIExample/Examples/Advanced/SpatialAudio/zh-Hans.lproj/SpatialAudio.strings +++ b/macOS/APIExample/Examples/Advanced/SpatialAudio/zh-Hans.lproj/SpatialAudio.strings @@ -1,60 +1,59 @@ +/* Element = "textFieldCell"; title = "Please move the red icon to experience the 3D audio effect."; ObjectID = "0XP-YK-KuI"; */ +"0XP-YK-KuI.title" = "请移动红色图标体验 3D 音频效果"; -/* Class = "NSTextFieldCell"; title = "Move the red icon to experience 3D audio effects"; ObjectID = "0XP-YK-KuI"; */ -"0XP-YK-KuI.title" = "Move the red icon to experience 3D audio effects"; +/* Element = "buttonCell"; title = "Join Channel"; ObjectID = "55m-Re-Kgb"; */ +"55m-Re-Kgb.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Join Channel"; ObjectID = "55m-Re-Kgb"; */ -"55m-Re-Kgb.title" = "Join Channel"; +/* Element = "textFieldCell"; title = "Channel"; ObjectID = "8kK-yU-WUO"; */ +"8kK-yU-WUO.title" = "频道号"; -/* Class = "NSTextFieldCell"; title = "Channel ID"; ObjectID = "8kK-yU-WUO"; */ -"8kK-yU-WUO.title" = "Channel ID"; +/* Element = "textFieldCell"; title = "barrier"; ObjectID = "DuM-M6-we2"; */ +"DuM-M6-we2.title" = "音障"; -/* Class = "NSTextFieldCell"; title = "Sound Barrier"; ObjectID = "DuM-M6-we2"; */ -"DuM-M6-we2.title" = "Sound Barrier"; - -/* Class = "NSBox"; title = "Box"; ObjectID = "F9J-RG-ORp"; */ +/* Element = "box"; title = "Box"; ObjectID = "F9J-RG-ORp"; */ "F9J-RG-ORp.title" = "Box"; -/* Class = "NSTextFieldCell"; title = "Attenuation"; ObjectID = "Olg-lx-iDF"; */ -"Olg-lx-iDF.title" = "Attenuation"; +/* Element = "textFieldCell"; title = "Attenuatuin"; ObjectID = "Olg-lx-iDF"; */ +"Olg-lx-iDF.title" = "衰减"; -/* Class = "NSTextFieldCell"; title = "Attenuation"; ObjectID = "Rvd-Eg-yDs"; */ -"Rvd-Eg-yDs.title" = "Attenuation"; +/* Element = "textFieldCell"; title = "Attenuatuin"; ObjectID = "Rvd-Eg-yDs"; */ +"Rvd-Eg-yDs.title" = "衰减"; -/* Class = "NSTextFieldCell"; title = "Mute"; ObjectID = "SPa-zU-xrL"; */ -"SPa-zU-xrL.title" = "Mute"; +/* Element = "textFieldCell"; title = "Mute"; ObjectID = "SPa-zU-xrL"; */ +"SPa-zU-xrL.title" = "静音"; -/* Class = "NSTextFieldCell"; placeholderString = "Enter Channel ID"; ObjectID = "Seb-dp-fRr"; */ -"Seb-dp-fRr.placeholderString" = "Enter Channel ID"; +/* Element = "textFieldCell"; placeholderString = "Please Input Channel"; ObjectID = "Seb-dp-fRr"; */ +"Seb-dp-fRr.placeholderString" = "请输入频道名"; -/* Class = "NSBox"; title = "Box"; ObjectID = "UVp-jP-AeX"; */ +/* Element = "box"; title = "Box"; ObjectID = "UVp-jP-AeX"; */ "UVp-jP-AeX.title" = "Box"; -/* Class = "NSTextFieldCell"; title = "Airborne Simulation"; ObjectID = "XQd-ik-fGe"; */ -"XQd-ik-fGe.title" = "Airborne Simulation"; +/* Element = "textFieldCell"; title = "Airborne Simulation"; ObjectID = "XQd-ik-fGe"; */ +"XQd-ik-fGe.title" = "空气传播模拟"; -/* Class = "NSBox"; title = "Box"; ObjectID = "Zpd-F6-3hM"; */ +/* Element = "box"; title = "Box"; ObjectID = "Zpd-F6-3hM"; */ "Zpd-F6-3hM.title" = "Box"; -/* Class = "NSTextFieldCell"; title = "Microphone"; ObjectID = "dgP-7k-2RR"; */ -"dgP-7k-2RR.title" = "Microphone"; +/* Element = "textFieldCell"; title = "Mic"; ObjectID = "dgP-7k-2RR"; */ +"dgP-7k-2RR.title" = "麦克风"; -/* Class = "NSTextFieldCell"; title = "Voice Blur"; ObjectID = "dps-1x-WRS"; */ -"dps-1x-WRS.title" = "Voice Blur"; +/* Element = "textFieldCell"; title = "Voice Blur"; ObjectID = "dps-1x-WRS"; */ +"dps-1x-WRS.title" = "声音模糊"; -/* Class = "NSTextFieldCell"; title = "Mute"; ObjectID = "e6q-8k-QYN"; */ -"e6q-8k-QYN.title" = "Mute"; +/* Element = "textFieldCell"; title = "Mute"; ObjectID = "e6q-8k-QYN"; */ +"e6q-8k-QYN.title" = "静音"; -/* Class = "NSTextFieldCell"; title = "Voice Blur"; ObjectID = "i8Q-uY-ZPE"; */ -"i8Q-uY-ZPE.title" = "Voice Blur"; +/* Element = "textFieldCell"; title = "Voice Blur"; ObjectID = "i8Q-uY-ZPE"; */ +"i8Q-uY-ZPE.title" = "声音模糊"; -/* Class = "NSButtonCell"; title = "123"; ObjectID = "q1C-6O-FWQ"; */ +/* Element = "buttonCell"; title = "123"; ObjectID = "q1C-6O-FWQ"; */ "q1C-6O-FWQ.title" = "123"; -/* Class = "NSButtonCell"; title = "123"; ObjectID = "ujV-mS-k6K"; */ +/* Element = "buttonCell"; title = "123"; ObjectID = "ujV-mS-k6K"; */ "ujV-mS-k6K.title" = "123"; -/* Class = "NSTextFieldCell"; title = "Airborne Simulation"; ObjectID = "w1T-YB-klu"; */ -"w1T-YB-klu.title" = "Airborne Simulation"; +/* Element = "textFieldCell"; title = "Airborne Simulation"; ObjectID = "w1T-YB-klu"; */ +"w1T-YB-klu.title" = "空气传播模拟"; -/* Class = "NSTextFieldCell"; title = "Room"; ObjectID = "wB6-0F-DcM"; */ -"wB6-0F-DcM.title" = "Room"; +/* Element = "textFieldCell"; title = "Room"; ObjectID = "wB6-0F-DcM"; */ +"wB6-0F-DcM.title" = "房间"; 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/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings b/macOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings index c4d5facfc..f32c9c868 100644 --- a/macOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings +++ b/macOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings @@ -1,27 +1,8 @@ +/* Element = "viewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "码流加密"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "1ik-om-mWj"; */ -"1ik-om-mWj.title" = "Join Channel"; +/* Element = "box"; title = "Box"; ObjectID = "Wra-rY-FWh"; */ +"Wra-rY-FWh.title" = "Box"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "6f9-0B-egB"; */ -"6f9-0B-egB.title" = "1V1"; - -/* Class = "NSViewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ -"Gwp-vd-c2J.title" = "Stream Encryption"; - -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Owt-vb-7U9"; */ -"Owt-vb-7U9.title" = "Leave Channel"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "S4i-eh-YzK"; */ -"S4i-eh-YzK.title" = "1V3"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "aj5-Fn-je9"; */ -"aj5-Fn-je9.placeholderString" = "Enter Channel Name"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "cxo-X2-S8L"; */ -"cxo-X2-S8L.title" = "1V15"; - -/* Class = "NSTextFieldCell"; placeholderString = "Encryption Secret"; ObjectID = "sOM-VA-bwW"; */ -"sOM-VA-bwW.placeholderString" = "Encryption Secret"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "zu1-vg-leG"; */ -"zu1-vg-leG.title" = "1V8"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "oLq-PI-YGA"; */ +"oLq-PI-YGA.title" = "加入频道"; 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/VideoProcess/zh-Hans.lproj/VideoProcess.strings b/macOS/APIExample/Examples/Advanced/VideoProcess/zh-Hans.lproj/VideoProcess.strings index 6aaa2d5ff..a78e7c6ed 100644 --- a/macOS/APIExample/Examples/Advanced/VideoProcess/zh-Hans.lproj/VideoProcess.strings +++ b/macOS/APIExample/Examples/Advanced/VideoProcess/zh-Hans.lproj/VideoProcess.strings @@ -1,47 +1,44 @@ +/* Element = "textFieldCell"; title = "Low light Enhancement"; ObjectID = "11E-tR-tFb"; */ +"11E-tR-tFb.title" = "暗光增强"; -/* Class = "NSTextFieldCell"; title = "Low light Enhancement"; ObjectID = "11E-tR-tFb"; */ -"11E-tR-tFb.title" = "Low light Enhancement"; +/* Element = "textFieldCell"; title = "Beauty"; ObjectID = "4WO-Em-0Qq"; */ +"4WO-Em-0Qq.title" = "美颜"; -/* Class = "NSTextFieldCell"; title = "Beauty"; ObjectID = "4WO-Em-0Qq"; */ -"4WO-Em-0Qq.title" = "Beauty"; +/* Element = "textFieldCell"; title = "Strength"; ObjectID = "7PH-4u-nrD"; */ +"7PH-4u-nrD.title" = "混响持续强度"; -/* Class = "NSTextFieldCell"; title = "Strength"; ObjectID = "7PH-4u-nrD"; */ -"7PH-4u-nrD.title" = "Strength"; +/* Element = "textFieldCell"; title = "rosy"; ObjectID = "85x-Bt-HuU"; */ +"85x-Bt-HuU.title" = "红润"; -/* Class = "NSTextFieldCell"; title = "Ruddy"; ObjectID = "85x-Bt-HuU"; */ -"85x-Bt-HuU.title" = "Ruddy"; +/* Element = "textFieldCell"; title = "Color Enhancement"; ObjectID = "97m-MF-3to"; */ +"97m-MF-3to.title" = "色彩增强"; -/* Class = "NSTextFieldCell"; title = "Color Enhancement"; ObjectID = "97m-MF-3to"; */ -"97m-MF-3to.title" = "Color Enhancement"; +/* Element = "buttonCell"; title = "Face Shape"; ObjectID = "9VS-Ox-yEZ"; */ +"9VS-Ox-yEZ.title" = "脸型"; -/* Class = "NSBox"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ "BP9-4w-AfJ.title" = "Box"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ -"Lhu-U1-6qh.title" = "Join"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSViewController"; title = "Video Process"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Video Process"; +/* Element = "viewController"; title = "Video Process"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "视频增强"; -/* Class = "NSTextFieldCell"; title = "Sharp"; ObjectID = "ZOR-gO-Ji2"; */ -"ZOR-gO-Ji2.title" = "Sharp"; +/* Element = "textFieldCell"; title = "sharp"; ObjectID = "ZOR-gO-Ji2"; */ +"ZOR-gO-Ji2.title" = "锐化"; -/* Class = "NSTextFieldCell"; title = "Skin Protect"; ObjectID = "ozL-f1-AXZ"; */ -"ozL-f1-AXZ.title" = "Skin Protect"; +/* Element = "textFieldCell"; title = "Brighten"; ObjectID = "ky2-qK-YAJ"; */ +"ky2-qK-YAJ.title" = "提亮"; -/* Class = "NSTextFieldCell"; title = "Video Denoise"; ObjectID = "plA-5C-vDg"; */ -"plA-5C-vDg.title" = "Video Denoise"; +/* Element = "textFieldCell"; title = "Skin Protect"; ObjectID = "ozL-f1-AXZ"; */ +"ozL-f1-AXZ.title" = "肤色保护"; -/* Class = "NSTextFieldCell"; title = "Whitening"; ObjectID = "q1I-U0-llK"; */ -"q1I-U0-llK.title" = "Whitening"; +/* Element = "textFieldCell"; title = "Video Denoise"; ObjectID = "plA-5C-vDg"; */ +"plA-5C-vDg.title" = "视频降噪"; -/* Class = "NSTextFieldCell"; title = "Smoothing"; ObjectID = "sFK-pV-vaj"; */ -"sFK-pV-vaj.title" = "Smoothing"; +/* Element = "textFieldCell"; title = "Whitening"; ObjectID = "q1I-U0-llK"; */ +"q1I-U0-llK.title" = "美白"; -"AAl-Un-v63.title" = "Lightening"; - -/* Class = "NSButtonCell"; normalTitle = "Face Shape"; ObjectID = "9VS-Ox-yEZ"; */ -"9VS-Ox-yEZ.title" = "Face Shape"; - -/* Class = "NSButtonCell"; normalTitle = "Make Up"; ObjectID = "8aS-7m-YP7"; */ -"8aS-7m-YP7.title" = "Make Up"; +/* Element = "textFieldCell"; title = "smooth"; ObjectID = "sFK-pV-vaj"; */ +"sFK-pV-vaj.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 d2d5b0d58..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} @@ -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/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings index 836a4c2dc..579a8177d 100644 --- a/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings @@ -1,63 +1,47 @@ - -/* Class = "NSTextFieldCell"; title = "1Khz"; ObjectID = "5nb-04-vbe"; */ +/* Element = "textFieldCell"; title = "1Khz"; ObjectID = "5nb-04-vbe"; */ "5nb-04-vbe.title" = "1Khz"; -/* Class = "NSBox"; title = "Equalization Reverb"; ObjectID = "5z4-pq-KKl"; */ -"5z4-pq-KKl.title" = "Equalization Reverb"; +/* Element = "box"; title = "Equalization Reverb"; ObjectID = "5z4-pq-KKl"; */ +"5z4-pq-KKl.title" = "混响调整"; -/* Class = "NSTextFieldCell"; title = "2Khz"; ObjectID = "6ME-Zv-Hpv"; */ +/* Element = "textFieldCell"; title = "2Khz"; ObjectID = "6ME-Zv-Hpv"; */ "6ME-Zv-Hpv.title" = "2Khz"; -/* Class = "NSTextFieldCell"; title = "250hz"; ObjectID = "8JZ-5R-nCU"; */ +/* Element = "textFieldCell"; title = "250hz"; ObjectID = "8JZ-5R-nCU"; */ "8JZ-5R-nCU.title" = "250hz"; -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "8bV-OK-zbc"; */ -"8bV-OK-zbc.title" = "1V15"; - -/* Class = "NSTextFieldCell"; title = "31hz"; ObjectID = "8fd-8t-Irz"; */ +/* Element = "textFieldCell"; title = "31hz"; ObjectID = "8fd-8t-Irz"; */ "8fd-8t-Irz.title" = "31hz"; -/* Class = "NSTextFieldCell"; title = "16Khz"; ObjectID = "ClO-mY-jZW"; */ +/* Element = "textFieldCell"; title = "16Khz"; ObjectID = "ClO-mY-jZW"; */ "ClO-mY-jZW.title" = "16Khz"; -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "EhX-UJ-wov"; */ -"EhX-UJ-wov.placeholderString" = "Enter Channel Name"; - -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "J6a-ul-c2H"; */ -"J6a-ul-c2H.title" = "1V3"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "P4E-oB-5Di"; */ -"P4E-oB-5Di.title" = "Join Channel"; +/* Element = "textFieldCell"; title = "62hz"; ObjectID = "UAW-B9-951"; */ +"UAW-B9-951.title" = "62hz"; -/* Class = "NSBox"; title = "Equalization Band"; ObjectID = "bBW-s5-1yz"; */ -"bBW-s5-1yz.title" = "Equalization Band"; +/* Element = "buttonCell"; title = "Set Audio Effect Params"; ObjectID = "Xtr-ei-jBf"; */ +"Xtr-ei-jBf.title" = "设置参数"; -/* Class = "NSTextFieldCell"; title = "62hz"; ObjectID = "UAW-B9-951"; */ -"UAW-B9-951.title" = "62hz"; +/* Element = "box"; title = "Equalization Band"; ObjectID = "bBW-s5-1yz"; */ +"bBW-s5-1yz.title" = "波段增益"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "ch0-OR-L16"; */ -"ch0-OR-L16.title" = "1V1"; +/* Element = "box"; title = "Box"; ObjectID = "cBk-j3-gGn"; */ +"cBk-j3-gGn.title" = "Box"; -/* Class = "NSTextFieldCell"; title = "4Khz"; ObjectID = "fUn-bY-2Ur"; */ +/* Element = "textFieldCell"; title = "4Khz"; ObjectID = "fUn-bY-2Ur"; */ "fUn-bY-2Ur.title" = "4Khz"; -/* Class = "NSTextFieldCell"; title = "500hz"; ObjectID = "gNS-nM-8eg"; */ +/* Element = "textFieldCell"; title = "500hz"; ObjectID = "gNS-nM-8eg"; */ "gNS-nM-8eg.title" = "500hz"; -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "gWk-wf-hPu"; */ -"gWk-wf-hPu.title" = "1V8"; - -/* Class = "NSTextFieldCell"; title = "125hz"; ObjectID = "iEy-1i-vf4"; */ +/* Element = "textFieldCell"; title = "125hz"; ObjectID = "iEy-1i-vf4"; */ "iEy-1i-vf4.title" = "125hz"; -/* Class = "NSTextFieldCell"; title = "Voice Pitch"; ObjectID = "j8U-Er-3Ry"; */ -"j8U-Er-3Ry.title" = "Voice Pitch"; - -/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "Join Channel Audio"; +/* Element = "viewController"; title = "Voice Effects"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "美声/音效"; -/* Class = "NSTextFieldCell"; title = "8Khz"; ObjectID = "k68-jy-Jcs"; */ +/* Element = "textFieldCell"; title = "8Khz"; ObjectID = "k68-jy-Jcs"; */ "k68-jy-Jcs.title" = "8Khz"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "szu-uz-G6W"; */ -"szu-uz-G6W.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "xSc-z3-5Il"; */ +"xSc-z3-5Il.title" = "加入频道"; 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/JoinChannelAudio.swift b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift index 3961ea597..645641602 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 @@ -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/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings b/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings index d390b78a8..be0b8697e 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings @@ -1,21 +1,23 @@ - -/* Class = "NSTextFieldCell"; title = "NO"; ObjectID = "3mR-iP-I85"; */ +/* Element = "textFieldCell"; title = "NO"; ObjectID = "3mR-iP-I85"; */ "3mR-iP-I85.title" = "NO"; -/* Class = "NSTextFieldCell"; title = "Local user speaking status:"; ObjectID = "6jk-Ev-4bY"; */ -"6jk-Ev-4bY.title" = "Is speaking:"; +/* Element = "textFieldCell"; title = "Local user speaking status:"; ObjectID = "6jk-Ev-4bY"; */ +"6jk-Ev-4bY.title" = "是否说话:"; + +/* Element = "buttonCell"; title = "Join"; ObjectID = "B4L-Fw-EuA"; */ +"B4L-Fw-EuA.title" = "加入频道"; -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "B4L-Fw-EuA"; */ -"B4L-Fw-EuA.title" = "Join"; +/* Element = "buttonCell"; title = "Button"; ObjectID = "BVq-Zn-j5s"; */ +"BVq-Zn-j5s.title" = "Button"; -/* Class = "NSTextFieldCell"; title = "NO"; ObjectID = "QrQ-dw-wre"; */ +/* Element = "textFieldCell"; title = "NO"; ObjectID = "QrQ-dw-wre"; */ "QrQ-dw-wre.title" = "NO"; -/* Class = "NSTextFieldCell"; title = "Active Remote Speaker:"; ObjectID = "hda-m2-IVQ"; */ -"hda-m2-IVQ.title" = "Active Users:"; +/* Element = "textFieldCell"; title = "Active Remote Speaker:"; ObjectID = "hda-m2-IVQ"; */ +"hda-m2-IVQ.title" = "活跃用户:"; -/* Class = "NSBox"; title = "Box"; ObjectID = "j41-op-nLI"; */ +/* Element = "box"; title = "Box"; ObjectID = "j41-op-nLI"; */ "j41-op-nLI.title" = "Box"; -/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ -"jAv-ZA-ecf.title" = "Join Channel Audio"; +/* 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)/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(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Recorder)/zh-Hans.lproj/JoinChannelVideoRecorder.strings index f4a6903e6..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 @@ -1,24 +1,8 @@ +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +"BP9-4w-AfJ.title" = "Box"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "4rc-r1-Ay6"; */ -"4rc-r1-Ay6.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "Iws-j3-l2h"; */ -"Iws-j3-l2h.title" = "1V1"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Mmi-d8-vOm"; */ -"Mmi-d8-vOm.title" = "1V15"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "PtD-n2-sEW"; */ -"PtD-n2-sEW.placeholderString" = "Enter Channel Name"; - -/* 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" = "Real-time Video Call/Live Streaming"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ -"cH4-ft-u77.title" = "1V8"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "guU-jX-Wkg"; */ -"guU-jX-Wkg.title" = "Join Channel"; +/* 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)/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(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo(Token)/zh-Hans.lproj/JoinChannelVideoToken.strings index 7d6e06e51..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 @@ -1,28 +1,20 @@ +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +"BP9-4w-AfJ.title" = "Box"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "4rc-r1-Ay6"; */ -"4rc-r1-Ay6.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "Iws-j3-l2h"; */ -"Iws-j3-l2h.title" = "1V1"; +/* Element = "textFieldCell"; placeholderString = "Please the input AppID"; ObjectID = "PUf-pH-zWX"; */ +"PUf-pH-zWX.placeholderString" = "请输入 App ID"; -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Mmi-d8-vOm"; */ -"Mmi-d8-vOm.title" = "1V15"; +/* Element = "textFieldCell"; title = "Token"; ObjectID = "TSJ-u1-PfO"; */ +"TSJ-u1-PfO.title" = "Token"; -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "PtD-n2-sEW"; */ -"PtD-n2-sEW.placeholderString" = "Enter Channel Name"; +/* Element = "textFieldCell"; title = "AppID"; ObjectID = "UCG-Zk-hiI"; */ +"UCG-Zk-hiI.title" = "AppID"; -/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "VNU-so-ajb"; */ -"VNU-so-ajb.title" = "1V3"; +/* Element = "viewController"; title = "Join a channel (Token)"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "实时视频通话/直播(Token验证)"; -/* Class = "NSViewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ -"YjT-yy-DnJ.title" = "Real-time Video Call/Live Streaming"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ -"cH4-ft-u77.title" = "1V8"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "guU-jX-Wkg"; */ -"guU-jX-Wkg.title" = "Join Channel"; - -"JGsy-2x-qyo.placeholderString" = "Enter AppID"; - -"JMT-BK-gML.placeholderString" = "Enter 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/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/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings index f4a6903e6..f04530f99 100644 --- a/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings @@ -1,24 +1,8 @@ +/* Element = "box"; title = "Box"; ObjectID = "BP9-4w-AfJ"; */ +"BP9-4w-AfJ.title" = "Box"; -/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "4rc-r1-Ay6"; */ -"4rc-r1-Ay6.title" = "Leave Channel"; +/* Element = "buttonCell"; title = "Join"; ObjectID = "Lhu-U1-6qh"; */ +"Lhu-U1-6qh.title" = "加入频道"; -/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "Iws-j3-l2h"; */ -"Iws-j3-l2h.title" = "1V1"; - -/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Mmi-d8-vOm"; */ -"Mmi-d8-vOm.title" = "1V15"; - -/* Class = "NSTextFieldCell"; placeholderString = "Join Channel"; ObjectID = "PtD-n2-sEW"; */ -"PtD-n2-sEW.placeholderString" = "Enter Channel Name"; - -/* 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" = "Real-time Video Call/Live Streaming"; - -/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ -"cH4-ft-u77.title" = "1V8"; - -/* Class = "NSButtonCell"; title = "Join"; ObjectID = "guU-jX-Wkg"; */ -"guU-jX-Wkg.title" = "Join Channel"; +/* Element = "viewController"; title = "Join a channel (Video)"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "实时视频通话/直播"; diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/config.json deleted file mode 100644 index fc1b02889..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "engine_option": { - "identifier": "agora_beauty_material_100", - "version": "1.0.0", - "aspect": "16:9", - "preferred_fps": 24 - }, - "user_interface_option": { - "模板女": "muban_female/", - "模板男": "muban_male/", - "学姐妆": "xuejie/", - "学妹妆": "xuemei/", - "原生": "yuansheng/", - "冷白": "lengbai/", - "嫩白": "nenbai/" - }, - "beauty_config" : "模板男", - "makeup_config" : "学姐妆", - "filter_config" : "冷白" -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/lengbai/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/lengbai/config.json deleted file mode 100644 index 036b67a4b..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/lengbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/lengbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_female/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_female/config.json deleted file mode 100644 index c21c243f6..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_female/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.5, - "lightness": 0.7, - "redness": 0.5, - "contrast": 2, - "sharpness": 0.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 0, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 400, - "strength": 20 - } - ] -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_male/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_male/config.json deleted file mode 100644 index 0def94a5a..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/muban_male/config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.5, - "lightness": 0.7, - "redness": 0.5, - "contrast": 1, - "sharpness": 0.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 1, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 400, - "strength": 20 - } - ] -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/nenbai/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/nenbai/config.json deleted file mode 100644 index 6ddbae760..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/nenbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/nenbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuejie/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuejie/config.json deleted file mode 100644 index b561c7c41..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuejie/config.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 1, - "browColor": 1, - "browStrength": 1.0, - "lashStyle": 1, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 1, - "shadowStrength": 1.0, - "pupilStyle": 1, - "pupilStrength": 1.0, - "blushStyle": 1, - "blushColor": 1, - "blushStrength": 1.0, - "lipStyle": 1, - "lipColor": 1, - "lipStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuejie.png", - "strength": 0.5 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuemei/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuemei/config.json deleted file mode 100644 index 4ee5c17d0..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/xuemei/config.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 2, - "browColor": 2, - "browStrength": 1.0, - "lashStyle": 5, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 6, - "shadowStrength": 1.0, - "pupilStyle": 2, - "pupilStrength": 1.0, - "blushStyle": 2, - "blushColor": 5, - "blushStrength": 1.0, - "lipStyle": 2, - "lipColor": 5, - "lipStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuemei.png", - "strength": 0.5 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/yuansheng/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/yuansheng/config.json deleted file mode 100644 index 75b424363..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v1.0.0/yuansheng/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/yuansheng32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/config.json deleted file mode 100644 index 99991247f..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "engine_option": { - "identifier": "agora_beauty_material_200", - "version": "2.0.0", - "aspect": "16:9", - "preferred_fps": 24 - }, - "user_interface_option": { - "模板女": "muban_female/", - "模板男": "muban_male/", - "模板自然": "muban_natural/", - "学姐妆": "xuejie/", - "学妹妆": "xuemei/", - "原生": "yuansheng/", - "冷白": "lengbai/", - "嫩白": "nenbai/" - }, - "beauty_config" : "模板女", - "makeup_config" : "学妹妆", - "filter_config" : "原生" -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/lengbai/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/lengbai/config.json deleted file mode 100644 index 036b67a4b..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/lengbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/lengbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_female/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_female/config.json deleted file mode 100644 index fddcdd89d..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_female/config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.9, - "lightness": 0.9, - "redness": 1.0, - "sharpness": 1.0, - "contrast_strength": 1.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 0, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 302, - "strength": 10 - }, - { - "area": 304, - "strength": 50 - }, - { - "area": 305, - "strength": 50 - }, - { - "area": 306, - "strength": 30 - }, - { - "area": 400, - "strength": 20 - }, - { - "area": 402, - "strength": 30 - } - ], - "face_buffing_option": { - "eye_pouch": 0.5, - "brighten_eye": 0.9, - "nasolabial_fold": 0.7, - "whiten_teeth": 0.7 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_male/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_male/config.json deleted file mode 100644 index bd7f9419c..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_male/config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.6, - "lightness": 0.3, - "redness": 0.2, - "sharpness": 0.5, - "contrast_strength": 1.0 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 1, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 302, - "strength": 10 - }, - { - "area": 304, - "strength": 50 - }, - { - "area": 305, - "strength": 50 - }, - { - "area": 306, - "strength": 30 - }, - { - "area": 400, - "strength": 20 - }, - { - "area": 402, - "strength": 30 - } - ], - "face_buffing_option": { - "eye_pouch": 0.7, - "brighten_eye": 0.9, - "nasolabial_fold": 0.0, - "whiten_teeth": 0.5 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_natural/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_natural/config.json deleted file mode 100644 index 92c68bc0c..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/muban_natural/config.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "beauty_effect_option": { - "enable": true, - "smoothness": 0.5, - "lightness": 0.7, - "redness": 0.5, - "contrast": 1, - "sharpness": 0.0, - "contrast_strength": 0.5 - }, - "face_shape_beauty_option": { - "enable": true, - "style": 2, - "intensity": 50 - }, - "face_shape_area_option": [ - { - "area": 100, - "strength": 50 - }, - { - "area": 200, - "strength": 50 - }, - { - "area": 302, - "strength": 10 - }, - { - "area": 304, - "strength": 50 - }, - { - "area": 305, - "strength": 50 - }, - { - "area": 306, - "strength": 30 - }, - { - "area": 400, - "strength": 20 - }, - { - "area": 402, - "strength": 30 - } - ], - "face_buffing_option": { - "eye_pouch": 0.5, - "brighten_eye": 0.9, - "nasolabial_fold": 0.5, - "whiten_teeth": 0.5 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/nenbai/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/nenbai/config.json deleted file mode 100644 index 6ddbae760..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/nenbai/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/nenbai32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuejie/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuejie/config.json deleted file mode 100644 index 8fdde0cae..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuejie/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 1, - "browColor": 1, - "browStrength": 1.0, - "lashStyle": 1, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 1, - "shadowStrength": 1.0, - "pupilStyle": 1, - "pupilStrength": 1.0, - "blushStyle": 1, - "blushColor": 1, - "blushStrength": 1.0, - "lipStyle": 1, - "lipColor": 1, - "lipStrength": 1.0, - "wocanStyle": 1, - "wocanStrength": 1.0, - "facialStyle": 4, - "facialStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuejie.png", - "strength": 0.5 - }, - "style_makeup_option" : { - "styleIntensity" : 0.95 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuemei/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuemei/config.json deleted file mode 100644 index 5b80c816f..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/xuemei/config.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "makeup_options": { - "enable_mu": true, - "resPath": "../../resource", - "browStyle": 2, - "browColor": 2, - "browStrength": 1.0, - "lashStyle": 5, - "lashColor": 1, - "lashStrength": 1.0, - "shadowStyle": 6, - "shadowStrength": 1.0, - "pupilStyle": 2, - "pupilStrength": 1.0, - "blushStyle": 2, - "blushColor": 5, - "blushStrength": 1.0, - "lipStyle": 2, - "lipColor": 5, - "lipStrength": 1.0, - "wocanStyle": 3, - "wocanStrength": 1.0, - "facialStyle": 5, - "facialStrength": 1.0 - }, - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/xuemei.png", - "strength": 0.5 - }, - "style_makeup_option" : { - "styleIntensity" : 0.95 - } -} diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/yuansheng/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/yuansheng/config.json deleted file mode 100644 index 75b424363..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0/yuansheng/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "filter_effect_option": { - "enable": true, - "path": "../../resource/filters/yuansheng32.png", - "strength": 0.5 - } -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/config.json deleted file mode 100644 index 38de795ee..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/config.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "engine_option": { - "identifier": "agora_beauty_material_200", - "version": "2.0.0", - "aspect": "16:9", - "preferred_fps": 24, - "encrypted": true - }, - "user_interface_option": { - "模板女": "muban_female/", - "模板男": "muban_male/", - "模板自然": "muban_natural/", - "学姐妆": "xuejie/", - "学妹妆": "xuemei/", - "原生": "yuansheng/", - "冷白": "lengbai/", - "嫩白": "nenbai/" - }, - "beauty_config" : "模板女", - "makeup_config" : "学妹妆", - "filter_config" : "原生" -} \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/lengbai/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/lengbai/config.json deleted file mode 100644 index c589fa40f..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/lengbai/config.json +++ /dev/null @@ -1,2 +0,0 @@ -÷«�êÓ__ nסT s³��@ôܰÍ‘¥¼D ´ú¤Iޤ ¬膗™„ -{¾Шê\'Ql.ù÷¶]Io¢ø¤­¾TJpQwÜ\ÔIÃq<ºE†༊ 2ý̓Àϊ-ӳî‘“b7Cõ|ŸÆ \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_female/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_female/config.json deleted file mode 100644 index 3ea30a93b..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_female/config.json and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_male/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_male/config.json deleted file mode 100644 index 4e0c6a9d4..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_male/config.json and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_natural/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_natural/config.json deleted file mode 100644 index 9930f8b85..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/muban_natural/config.json and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/nenbai/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/nenbai/config.json deleted file mode 100644 index 06062b263..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/nenbai/config.json +++ /dev/null @@ -1,2 +0,0 @@ -÷«�êÓ__ nסT s³��@ôܰÍ‘¥¼D ´ú¤Iޤ ¬膗™„ -{¾Шê\'Ql.ù÷¶]Ioé]:V®z†ÕöXá‚ ܑ7ñWÓ óVÍê3ë�$­Gð·’Ös՝¯šF \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuejie/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuejie/config.json deleted file mode 100644 index f9522caf5..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuejie/config.json and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuemei/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuemei/config.json deleted file mode 100644 index ab3f807dc..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/xuemei/config.json and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/yuansheng/config.json b/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/yuansheng/config.json deleted file mode 100644 index 02c750cec..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/beauty_material_v2.0.0_encrypted/yuansheng/config.json +++ /dev/null @@ -1,2 +0,0 @@ -÷«�êÓ__ nסT s³��@ôܰÍ‘¥¼D ´ú¤Iޤ ¬膗™„ -{¾Шê\'Ql.ù÷¶]Io‡8³Â¾£ÜVê J™ˆڏP×è®ß̏Ä!ç`nÚÊrYŠôÛìø ��ò \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.png deleted file mode 100644 index f32018f8f..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush001.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.png deleted file mode 100644 index fda6f2845..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush002.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.png deleted file mode 100644 index f1135bf0c..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush004.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.png deleted file mode 100644 index 45a1e8b9e..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/blush/blush009.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.png deleted file mode 100644 index 08b3adfb3..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.txt deleted file mode 100644 index ff7914b65..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow001.txt +++ /dev/null @@ -1,26 +0,0 @@ -30.8540 54.4375 -45.0110 38.4755 -63.9685 28.0135 -85.7750 25.6785 -107.477 27.7120 -128.663 31.9345 -149.564 38.2160 -49.9845 50.6535 -69.6945 47.2265 -89.8265 46.6565 -109.794 48.3315 -129.602 51.2215 -149.033 54.8515 -146.121 54.4940 -133.676 38.9200 -116.063 28.5060 -95.5600 25.7185 -74.7805 26.9705 -54.3160 30.3035 -34.4040 35.6200 -128.359 50.6790 -109.779 47.2090 -90.8395 46.3015 -71.8655 47.1395 -52.9890 49.3300 -34.3750 52.0495 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.png deleted file mode 100644 index db0be064d..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.txt deleted file mode 100644 index ff7914b65..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow002.txt +++ /dev/null @@ -1,26 +0,0 @@ -30.8540 54.4375 -45.0110 38.4755 -63.9685 28.0135 -85.7750 25.6785 -107.477 27.7120 -128.663 31.9345 -149.564 38.2160 -49.9845 50.6535 -69.6945 47.2265 -89.8265 46.6565 -109.794 48.3315 -129.602 51.2215 -149.033 54.8515 -146.121 54.4940 -133.676 38.9200 -116.063 28.5060 -95.5600 25.7185 -74.7805 26.9705 -54.3160 30.3035 -34.4040 35.6200 -128.359 50.6790 -109.779 47.2090 -90.8395 46.3015 -71.8655 47.1395 -52.9890 49.3300 -34.3750 52.0495 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.png deleted file mode 100644 index c4a4103ff..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.txt deleted file mode 100644 index ff7914b65..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyebrow/eyebrow003.txt +++ /dev/null @@ -1,26 +0,0 @@ -30.8540 54.4375 -45.0110 38.4755 -63.9685 28.0135 -85.7750 25.6785 -107.477 27.7120 -128.663 31.9345 -149.564 38.2160 -49.9845 50.6535 -69.6945 47.2265 -89.8265 46.6565 -109.794 48.3315 -129.602 51.2215 -149.033 54.8515 -146.121 54.4940 -133.676 38.9200 -116.063 28.5060 -95.5600 25.7185 -74.7805 26.9705 -54.3160 30.3035 -34.4040 35.6200 -128.359 50.6790 -109.779 47.2090 -90.8395 46.3015 -71.8655 47.1395 -52.9890 49.3300 -34.3750 52.0495 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.png deleted file mode 100644 index d726fd35b..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash003.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.png deleted file mode 100644 index 5d94417e5..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyelash/eyelash005.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.png deleted file mode 100644 index 398757f71..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow001.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.png deleted file mode 100644 index aa638875e..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadow006.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadowscreen006.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadowscreen006.png deleted file mode 100644 index cb18c3fbd..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/eyeshadow/eyeshadowscreen006.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.png deleted file mode 100644 index 93abbc15b..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial002.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.png deleted file mode 100644 index 97ba1d83e..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial004.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.png deleted file mode 100644 index d0c86269f..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial005.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.png deleted file mode 100644 index 7d13c2af8..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.txt deleted file mode 100644 index 03bd13173..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/facial/facial006.txt +++ /dev/null @@ -1,10 +0,0 @@ -22.884 52.644 -42.604 162.736 -131.217 232.67 -221.472 164.036 -241.163 53.709 -208.808 34.915 -131.454 65.088 -88.441 35.399 -50.758 24.064 -131.113 104.094 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/lengbai32.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/lengbai32.png deleted file mode 100644 index cfa92e494..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/lengbai32.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/nenbai32.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/nenbai32.png deleted file mode 100644 index dcadb4af9..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/nenbai32.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/xuejie.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/xuejie.png deleted file mode 100644 index e55c1e367..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/xuejie.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/xuemei.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/xuemei.png deleted file mode 100644 index 5554af2c1..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/xuemei.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/yuansheng32.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/yuansheng32.png deleted file mode 100644 index 6b9714cb7..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/filters/yuansheng32.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.png deleted file mode 100644 index dfe1b80e8..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.txt deleted file mode 100644 index f697d5f34..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip001.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.png deleted file mode 100644 index 0b6321931..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.txt deleted file mode 100644 index f697d5f34..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip002.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.png deleted file mode 100644 index 93e1f83fd..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.txt deleted file mode 100644 index f697d5f34..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip003.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.png deleted file mode 100644 index 6a620aaca..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.txt deleted file mode 100644 index f697d5f34..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/lip/lip006.txt +++ /dev/null @@ -1,16 +0,0 @@ -54.1429 44.7807 -76.3764 35.6497 -99.4509 27.5741 -123.220 22.8059 -147.092 27.2459 -171.178 22.0762 -194.742 25.1468 -217.482 33.1426 -242.618 41.6146 -227.822 71.1710 -205.574 94.5960 -176.950 109.514 -146.193 113.620 -115.560 108.665 -88.4023 94.1990 -67.6135 71.5944 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.png deleted file mode 100644 index b20527b07..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan001.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.png deleted file mode 100644 index bfad3f3db..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan002.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.png deleted file mode 100644 index 24a48252c..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan003.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.png b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.png deleted file mode 100644 index 1580a77fa..000000000 Binary files a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.png and /dev/null differ diff --git a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.txt b/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.txt deleted file mode 100644 index d244d17c1..000000000 --- a/macOS/APIExample/Resources/beauty_material.bundle/resource/wocan/wocan004.txt +++ /dev/null @@ -1,44 +0,0 @@ -35.00 37.75 -37.91 35.25 -41.80 32.00 -45.69 30.50 -50.06 29.50 -54.92 29.50 -59.30 30.00 -63.67 31.00 -67.56 33.50 -70.96 36.00 -73.88 40.00 -75.82 43.50 -72.42 45.00 -68.53 46.00 -64.16 47.00 -60.27 47.50 -55.90 47.50 -51.52 47.00 -47.63 46.00 -43.75 45.00 -40.34 42.50 -36.94 40.75 -83.12 36.00 -80.20 33.50 -76.80 31.50 -72.91 30.00 -68.53 29.00 -64.16 29.00 -59.78 29.50 -54.44 31.00 -49.09 33.00 -44.72 35.50 -40.83 39.50 -37.91 43.50 -42.29 44.50 -47.15 45.50 -52.49 46.50 -57.35 47.00 -62.70 47.00 -66.59 47.00 -70.96 46.00 -74.37 44.00 -77.77 42.00 -81.17 39.50 \ No newline at end of file diff --git a/macOS/APIExample/ViewController.swift b/macOS/APIExample/ViewController.swift index f9ee332c3..ae523281e 100644 --- a/macOS/APIExample/ViewController.swift +++ b/macOS/APIExample/ViewController.swift @@ -25,36 +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: "Face Capture".localized, identifier: "menuCell", controller: "FaceCapture", storyboard: "FaceCapture"), - 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"), ] @@ -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 5b22a9537..a2f061bc5 100644 --- a/macOS/APIExample/zh-Hans.lproj/Localizable.strings +++ b/macOS/APIExample/zh-Hans.lproj/Localizable.strings @@ -6,297 +6,308 @@ Copyright © 2020 Agora Corp. All rights reserved. */ -"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"; -"RTMP Streaming" = "RTMP Streaming"; -"Media Injection" = "Media Injection"; -"Video Metadata" = "SEI Message"; -"Voice Changer" = "Voice Effects/Audio Effects"; -"Custom Audio Source" = "Custom Audio Source"; -"Custom Audio Render" = "Custom Audio Render"; -"Custom Video Source(MediaIO)" = "Custom Video Source (MediaIO)"; -"Custom Video Source(Push)" = "Custom Video Source (Push)"; -"Custom Video Source(Multi)" = "Multiple Custom Video Sources"; -"Create Video Track" = "Create Capture Track"; -"Destory Track" = "Destroy Track"; -"Custom Video Render" = "Custom Video Render (Metal)"; -"Quick Switch Channel" = "Quick Channel Switch"; -"Switch Channel" = "Switch Channel"; -"Join Multiple Channels" = "Join Multiple Channels"; -"Stream Encryption" = "Media Stream Encryption"; -"Audio Mixing" = "Audio File Mixing"; -"Raw Media Data" = "Raw Audio/Video Data"; -"Precall Test" = "Pre-call Network/Device Test"; -"Media Player" = "Media Player"; -"Screen Share" = "Screen Sharing"; -"Media Channel Relay" = "Cross-Channel Media Relay"; -"URL Streaming(RTE Player)" = "URL Streaming (RTE Player)"; +"Join a channel (Token)" = "实时视频通话/直播(Token验证)"; +"Join a channel (Video)" = "实时视频通话/直播"; +"Join a channel (Audio)" = "实时语音通话/直播"; +"Local or remote recording" = "本地/远端录制"; +"Live Streaming" = "RTC实时直播/主播/观众"; +"RTMP Streaming" = "RTMP旁路推流"; +"Media Metadata" = "SEI消息"; +"Voice Effects" = "美声/音效"; +"Custom Audio Source (PCM)" = "音频自采集(PCM)"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source" = "视频自采集"; +"Multiple Video Sources" = "多路视频自采集"; +"Create Video Track" = "创建采集Track"; +"Destory Track" = "销毁Track"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Switch Channel" = "切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Pre-call Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Media Channel Relay" = "跨频道流转发"; +"URL Streaming (RTE Player)" = "URL拉流(RTE Player)"; -"Super Resolution" = "Super Resolution"; -"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" = "Standard Music"; -"Music Standard Stereo" = "Standard Stereo Music"; -"Music High Quality" = "High Quality Music"; -"Music High Quality Stereo" = "High Quality Stereo Music"; -"Speech Standard" = "Standard Speech"; -"Chat Room Gaming" = "Entertainment Chat Room"; -"Education" = "Education"; -"Game Streaming" = "High Quality Chat Room"; -"Chat Room Entertainment" = "Gaming Chat"; -"Show Room" = "Show Room"; -"Cancel" = "Cancel"; -"Sure" = "Confirm"; -"Off" = "Original"; -"FemaleFresh" = "Chat Beautifier: Fresh (Female)"; -"FemaleVitality" = "Chat Beautifier: Vitality (Female)"; -"MaleMagnetic" = "Chat Beautifier: Magnetic (Male)"; -"Vigorous" = "Full-bodied"; -"Deep" = "Deep"; -"Mellow" = "Mellow"; -"Falsetto" = "Falsetto"; -"Full" = "Full"; -"Clear" = "Clear"; -"Resounding" = "Resounding"; -"Ringing" = "Ringing"; -"Spacial" = "Spacious"; -"Ethereal" = "Ethereal"; -"Old Man" = "Old Man"; -"Baby Boy" = "Baby Boy"; -"Baby Girl" = "Baby Girl"; -"ZhuBaJie" = "Zhu Bajie"; -"Hulk" = "Hulk"; -"FxUncle" = "Uncle"; -"FxSister" = "Sister"; -"Pop" = "Pop"; -"Pop(Old Version)" = "Pop (Old Version)"; +"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" = "高音质语聊房"; +"Chat Room" = "语聊房"; +"Chorus" = "实时合唱"; +"Meeting" = "会议场景"; +"AI Client" = "对话式 AI 客户端"; +"Chat Room Entertainment" = "游戏开黑"; +"Show Room" = "秀场"; +"Cancel" = "取消"; +"Sure" = "确定"; +"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" = "Hip Hop"; -"Vocal Concert" = "Concert"; -"Vocal Concert(Old Version)" = "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" = "Original Sound Level"; -"Wet Level" = "Early Reflection Level"; -"Room Size" = "Room Size"; -"Wet Delay" = "Early Reflection Delay"; -"Strength" = "Reverb Strength"; -"Broadcaster" = "Host"; -"Audience" = "Audience"; -"Global settings" = "Global Settings"; +"KTV(Old Version)" = "KTV(旧版)"; +"Studio" = "录音棚"; +"Studio(Old Version)" = "录音棚(旧版)"; +"Phonograph" = "留声机"; +"Virtual Stereo" = "虚拟立体声"; +"Dry Level" = "原始声音强度"; +"Wet Level" = "早期反射信号强度"; +"Room Size" = "房间尺寸"; +"Wet Delay" = "早期反射信号延迟"; +"Strength" = "强度"; +"Broadcaster" = "主播"; +"Audience" = "观众"; +"Global settings" = "全局设置"; -"Resolution" = "Resolution"; -"Enable Cloud Proxy" = "Enable Cloud Proxy"; -"Frame Rate" = "Frame Rate"; -"Camera" = "Camera"; -"Microphone" = "Microphone"; -"Layout" = "Layout"; -"Role" = "Role"; -"Channel" = "Channel"; -"Channel Name" = "Enter Channel Name"; -"Join Channel" = "Join Channel"; -"Leave Channel" = "Leave Channel"; -"Join ChannelEx" = "Join Channel Ex"; -"Leave ChannelEx" = "Leave Channel Ex"; -"Audio Profile" = "Audio Profile"; -"Audio Scenario" = "Audio Scenario"; -"Device Recording Volume" = "Device Recording Volume"; -"SDK Recording Volume" = "SDK Recording Volume"; -"Device Playout Volume" = "Device Playback Volume"; -"SDK Playout Volume" = "SDK Playback Volume"; -"User Playback Volume" = "First Remote User Volume"; -"Encryption Mode" = "Encryption Mode"; -"Encryption Secret" = "Encryption Password"; -"Input Encryption Secret" = "Enter Encryption Password"; -"Relay Channel" = "Relay Channel"; -"Start Relay" = "Start Relay"; -"Pause Relay" = "Pause Relay"; -"Resume Relay" = "Resume Relay"; -"Relay Channnel Name" = "Target Relay Channel Name"; -"Stop Relay" = "Stop Relay"; -"Display Share" = "Screen Share"; -"Window Share" = "Window Share"; -"Stop Share" = "Stop Share"; -"Share Half Screen" = "Share Partial Screen"; -"Publish" = "Publish"; -"Unpublish" = "Stop Publishing"; -"Mixing Volume" = "Mixing Volume"; -"Loopback Recording Volume" = "System Audio Mix Volume"; -"Mixing Playback Volume" = "Mix Playback Volume"; -"Mixing Publish Volume" = "Mix Publishing Volume"; -"Overall Effect Volume" = "Effect Volume"; -"Chat Beautifier" = "Chat Beautifier"; -"Timbre Transformation" = "Timbre Transformation"; -"Style Transformation" = "Style Transformation"; -"Room Acoustics" = "Room Acoustics"; -"Pitch Correction" = "Pitch Correction"; -"Cycle(0-60)" = "Cycle (0-60) seconds"; -"Tonic Mode(1-3)" = "Tonic Mode (1-3)"; -"Tonic Pitch(1-12)" = "Tonic Pitch (1-12)"; -"Voice Pitch" = "Voice Pitch"; -"Off" = "Off"; -"Set Audio Effect Params" = "Set Parameters"; -"Equalization Band" = "Band Gain"; -"Create Data Stream" = "Create Data Stream"; -"Send Message" = "Send Message"; -"Input Message" = "Enter Message"; -"Send" = "Send"; -"Sending" = "Sending"; -"Raw Audio Data" = "Raw Audio Data"; -"Raw Video Data" = "Raw Video Data"; -"SnapShot" = "Screenshot"; -"Video Source" = "Video Source Selection"; -"Simple Filter" = "Plugin System"; -"Enable Grey" = "Enable Grayscale"; -"Video Process" = "Video Enhancement"; -"Agora Beauty" = "Agora Beauty"; -"Low light Enhancement" = "Low Light Enhancement"; -"Video Denoise" = "Video Denoising"; -"Color Enhancement" = "Color Enhancement"; -"Strength" = "Strength"; -"Skin Protect" = "Skin Protection"; -"Colored Background" = "Solid Color Background"; -"Image Background" = "Image Background"; -"Virtual Background" = "Virtual Background"; -"Blur Background" = "Background Blur"; -"Video Background" = "Video"; -"Spatial Audio" = "Spatial Audio"; -"Start" = "Click to Start"; -"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 Moderation"; -"Video Neutral" = "Video Normal"; -"Video Sexy" = "Video Suggestive"; -"Video Porn" = "Video Adult"; -"You can use the camera point to picture and check content inspect effect" = "You can point the camera at pictures to test content moderation effect"; -"Save successfully" = "Save Successful"; -"Save failed" = "Save Failed"; -"Please the input AppId" = "Please enter AppID"; -"Please the input token" = "Please enter Token"; -"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"; -"Recording" = "Start Recording"; -"Stop Recording" = "Stop Recording"; -"Local Video Transcoding" = "Local Video Transcoding"; -"Exclude Window" = "Exclude Window"; -"Opening" = "Enabled"; -"(Default: flow)" = "(Default: High Stream)"; -"Red" = "Red"; -"Blue" = "Blue"; -"Pink" = "Pink"; -"Purple" = "Purple"; -"Yellow" = "Yellow"; -"Multi Camera Sourece" = "Multiple Camera Sources"; -"Start Capture" = "Start Capture"; -"Start Publish" = "Start Publishing"; -"Stop Capture" = "Stop Capture"; -"Stop Publish" = "Stop Publishing"; -"preload Channel" = "Preload"; -"cancel preload" = "Cancel Preload"; -"Set AINS Mode" = "Set AI Noise Suppression Mode"; +"Resolution" = "分辨率"; +"Enable Cloud Proxy" = "开启云代理"; +"Frame Rate" = "帧率"; +"Camera" = "摄像头"; +"Microphone" = "麦克风"; +"Layout" = "布局"; +"Role" = "角色"; +"Channel" = "频道号"; +"Channel Name" = "输入频道号"; +"Join Channel" = "加入频道"; +"Leave Channel" = "离开频道"; +"Join ChannelEx" = "加入Ex频道"; +"Leave ChannelEx" = "离开Ex频道"; +"Audio Profile" = "音频音质参数"; +"Audio Scenario" = "音频使用场景"; +"Device Recording Volume" = "设备录制音量"; +"SDK Recording Volume" = "SDK录制音量"; +"Device Playout Volume" = "设备播放音量"; +"SDK Playout Volume" = "SDK播放音量"; +"User Playback Volume" = "首位远端用户音量"; +"Encryption Mode" = "加密模式"; +"Encryption Secret" = "加密密码"; +"Input Encryption Secret" = "输入加密密码"; +"Relay Channel" = "转发频道"; +"Start Relay" = "开始转发"; +"Pause Relay" = "暂停转发"; +"Resume Relay" = "恢复转发"; +"Relay Channnel Name" = "目标转发频道名"; +"Stop Relay" = "停止转发"; +"Display Share" = "屏幕共享"; +"Window Share" = "窗口共享"; +"Stop Share" = "停止共享"; +"Share Half Screen" = "分享部分区域"; +"Publish" = "发流"; +"Unpublish" = "停止发流"; +"Mixing Volume" = "混音音量"; +"Loopback Recording Volume" = "系统音频混音音量"; +"Mixing Playback Volume" = "混音播放音量"; +"Mixing Publish Volume" = "混音发布音量"; +"Overall Effect Volume" = "音效音量"; +"Chat Beautifier" = "语聊美声"; +"Timbre Transformation" = "音色转换"; +"Style Transformation" = "风格转换"; +"Room Acoustics" = "室内声学"; +"Pitch Correction" = "音高修正"; +"Cycle(0-60)" = "循环周期(0-60)秒"; +"Tonic Mode(1-3)" = "主音模式(1-3)"; +"Tonic Pitch(1-12)" = "主音音高(1-12)"; +"Voice Pitch" = "声调"; +"Set Audio Effect Params" = "设置参数"; +"Equalization Band" = "波段增益"; +"Send Data Stream" = "发送数据流"; +"Send Message" = "发送消息"; +"Input Message" = "输入消息"; +"Send" = "发送"; +"Sending" = "发送中"; +"Raw Audio Data" = "音频裸数据"; +"Raw Video Data" = "视频裸数据"; +"SnapShot" = "截图"; +"Video Source" = "视频源选择"; +"Simple Filter Extension" = "插件系统"; +"Enable Grey" = "开启去色"; +"Video Process" = "视频增强"; +"Beauty 2.0" = "美颜 2.0"; +"Low light Enhancement" = "暗光增强"; +"Video Denoise" = "视频降噪"; +"Color Enhancement" = "色彩增强"; +"Skin Protect" = "肤色保护"; +"Colored Background" = "纯色背景"; +"Image Background" = "图片背景"; +"Virtual Background" = "虚拟背景"; +"Blur Background" = "背景虚化"; +"Video Background" = "视频"; +"Spatial Audio" = "空间音效"; +"Start" = "点击开始"; +"Please move the red icon to experience the 3D audio effect" = "请移动红色图标体验3D音频效果"; +"Content Inspect" = "视频鉴黄"; +"Video Neutral" = "视频正常"; +"Video Sexy" = "视频性感"; +"Video Porn" = "视频色情"; +"You can use the camera point to picture and check content inspect effect" = "你可以用摄像头对准图片检验鉴黄效果"; +"Save successfully" = "保存成功"; +"Save failed" = "保存失败"; +"Please the input AppId" = "请输入AppID"; +"Please the input token" = "请输入Token"; +"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" = "停止录制"; +"Local Video Transcoding" = "本地合图"; +"Exclude Window" = "屏蔽窗口"; +"Opening" = "已开启"; +"(Default: flow)" = "(默认: 大流)"; +"Red" = "红色"; +"Blue" = "蓝色"; +"Pink" = "粉色"; +"Purple" = "紫色"; +"Yellow" = "黄色"; +"Multi Camera" = "多路摄像头"; +"Start Capture" = "开始采集"; +"Start Publish" = "开始推流"; +"Stop Capture" = "停止采集"; +"Stop Publish" = "停止推流"; +"preload Channel" = "预加载"; +"cancel preload" = "取消预加载"; +"Set AINS Mode" = "设置是否开启 AI 降噪功能并设置降噪模式。"; -"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 customer service to obtain a face capture certificate"; -"Start Video Echo Test" = "Start Video Echo Test"; -"Stop Video Echo Test" = "Stop Video Echo Test"; +"AGGRESSIVE" = "强降噪模式"; +"BALANCED" = "(默认)均衡降噪模式"; +"ULTRALOWLATENCY" = "低延时强降噪模式"; +"Start Video Echo Test" = "开始视频回路测试"; +"Stop Video Echo Test" = "停止视频回路测试"; -"Set Voice AI Tuner" = "Set Voice AI Tuner"; -"Mature Male Voice" = "Mature Male Voice"; -"Fresh Male Voice" = "Fresh Male Voice"; -"Elegant Female Voice" = "Elegant Female Voice"; -"Sweet Female Voice" = "Sweet Female Voice"; -"Warm Male Singing Voice" = "Warm Male Singing Voice"; -"Gentle Female Singing Voice" = "Gentle Female Singing Voice"; -"Husky Male Singing Voice" = "Husky Male Singing Voice"; -"Warm Elegant Female Singing Voice" = "Warm Elegant Female Singing Voice"; -"Powerful Male Singing Voice" = "Powerful Male Singing Voice"; -"Dreamy Female Singing Voice" = "Dreamy Female Singing Voice"; +"Set Voice AI Tuner" = "设置AI调音器"; +"Mature Male Voice" = "大叔声"; +"Fresh Male Voice" = "清新男音"; +"Elegant Female Voice" = "御姐音"; +"Sweet Female Voice" = "萝莉音"; +"Warm Male Singing Voice" = "暖男歌声"; +"Gentle Female Singing Voice" = "温柔女歌声"; +"Husky Male Singing Voice" = "烟嗓叔音歌声"; +"Warm Elegant Female Singing Voice" = "温暖御姐歌声"; +"Powerful Male Singing Voice" = "力量男歌声"; +"Dreamy Female Singing Voice" = "梦幻女歌声"; -"Start Audio Mixing" = "Start Audio Forwarding"; -"Stop Audio Mixing" = "Stop Audio Forwarding"; +"Start Audio Mixing" = "开启音频转发"; +"Stop Audio Mixing" = "停止音频转发"; -"Makeup Enable" = "Makeup Toggle"; -"Eyebrow Style" = "Eyebrow Style"; -"Eyebrow Color" = "Eyebrow Color"; -"Eyebrow Strength" = "Eyebrow Intensity"; -"Eyelash Style" = "Eyelash Style"; -"Eyelash Color" = "Eyelash Color"; -"Eyelash Strength" = "Eyelash Intensity"; -"Eyeshadow Style" = "Eyeshadow Style"; -"Eyeshadow Color" = "Eyeshadow Color"; -"Eyeshadow Strength" = "Eyeshadow Intensity"; -"Pupil Style" = "Pupil Style"; -"Pupil Color" = "Pupil Color"; -"Pupil Strength" = "Pupil Intensity"; -"Blush Style" = "Blush Style"; -"Blush Color" = "Blush Color"; -"Blush Strength" = "Blush Intensity"; -"Lip Style" = "Lipstick Style"; -"Lip Color" = "Lipstick Color"; -"Lip Strength" = "Lipstick Intensity"; +"Makeup Enable" = "美妆开关"; +"Eyebrow Style" = "眉毛类型"; +"Eyebrow Color" = "眉毛颜色"; +"Eyebrow Strength" = "眉毛强度"; +"Eyelash Style" = "眼睫毛类型"; +"Eyelash Color" = "眼睫毛颜色"; +"Eyelash Strength" = "眼睫毛强度"; +"Eyeshadow Style" = "眼影类型"; +"Eyeshadow Color" = "眼影颜色"; +"Eyeshadow Strength" = "眼影强度"; +"Pupil Style" = "瞳孔类型"; +"Pupil Color" = "瞳孔颜色"; +"Pupil Strength" = "瞳孔强度"; +"Blush Style" = "腮红类型"; +"Blush Color" = "腮红颜色"; +"Blush Strength" = "腮红强度"; +"Lip Style" = "唇彩类型"; +"Lip Color" = "唇彩颜色"; +"Lip Strength" = "唇彩强度"; -"Eyebrow Style Off" = "Off"; -"Eyebrow Style Type 1" = "Type 1"; -"Eyebrow Style Type 2" = "Type 2"; +"Eyebrow Style Off" = "关闭"; +"Eyebrow Style Type 1" = "类型1"; +"Eyebrow Style Type 2" = "类型2"; -"Eyebrow Color None" = "None"; -"Eyebrow Color Black" = "Black"; -"Eyebrow Color Brown" = "Brown"; +"Eyebrow Color None" = "无"; +"Eyebrow Color Black" = "黑色"; +"Eyebrow Color Brown" = "棕色"; -"Blush Color None" = "None"; -"Blush Color Shade 1" = "Shade 1"; -"Blush Color Shade 2" = "Shade 2"; -"Blush Color Shade 3" = "Shade 3"; -"Blush Color Shade 4" = "Shade 4"; -"Blush Color Shade 5" = "Shade 5"; +"Blush Color None" = "无"; +"Blush Color Shade 1" = "1号色"; +"Blush Color Shade 2" = "2号色"; +"Blush Color Shade 3" = "3号色"; +"Blush Color Shade 4" = "4号色"; +"Blush Color Shade 5" = "5号色"; -"Face Shape Enable" = "Face Shape Toggle"; -"Face Shape Gender" = "Gender"; -"Face Shape Gender Male" = "Male"; -"Face Shape Gender Female" = "Female"; -"Face Shape Intensity" = "Intensity"; +"Face Shape Enable" = "美型开关"; +"Face Shape Gender" = "性别"; +"Face Shape Gender Male" = "男性"; +"Face Shape Gender Female" = "女性"; +"Face Shape Intensity" = "强度"; -"Face Shape Area Head Scale" = "Head Scale"; -"Face Shape Area Forehead" = "Forehead"; -"Face Shape Area Face Contour" = "Face Contour"; -"Face Shape Area Face Length" = "Face Length"; -"Face Shape Area Face Width" = "Face Width"; -"Face Shape Area Cheek Bone" = "Cheekbone"; -"Face Shape Area Cheek" = "Cheek"; -"Face Shape Area Chin" = "Chin"; -"Face Shape Area Eye Scale" = "Eye Scale"; -"Face Shape Area Nose Length" = "Nose Length"; -"Face Shape Area Nose Width" = "Nose Width"; -"Face Shape Area Mouth Scale" = "Mouth Scale"; +"Face Shape Area Head Scale" = "头部比例"; +"Face Shape Area Forehead" = "额头"; +"Face Shape Area Face Contour" = "脸型轮廓"; +"Face Shape Area Face Length" = "脸长"; +"Face Shape Area Face Width" = "脸宽"; +"Face Shape Area Cheek Bone" = "颧骨"; +"Face Shape Area Cheek" = "脸颊"; +"Face Shape Area Chin" = "下巴"; +"Face Shape Area Eye Scale" = "眼睛比例"; +"Face Shape Area Nose Length" = "鼻子长度"; +"Face Shape Area Nose Width" = "鼻子宽度"; +"Face Shape Area Mouth Scale" = "嘴巴比例"; -"Live Channel Name" = "Live Channel Name"; +"Live Channel Name" = "直播频道名"; -"Play" = "Play"; -"Stop" = "Stop"; +"Play" = "播放"; +"Stop" = "停止"; + +"Multipath" = "网络传输高级功能"; +"Simulcast" = "同源多流"; +"default makeup style" = "学妹妆"; +"Eyebrow Style Type 3" = "类型3"; +"Eyebrow Style Type 4" = "类型4"; +"Wocan" = "卧蚕"; +"Wocan Strength" = "卧蚕强度"; + +"3D Voice" = "3D 人声"; +"Additional Effect Volume" = "附加音效音量"; +"Center Stage" = "人物居中"; +"Mode" = "模式"; +"Audience Video Layer" = "观众端视频层"; +"Maxmum 3 layers can be selected" = "最多可选择 3 层"; diff --git a/macOS/ARCHITECTURE.md b/macOS/ARCHITECTURE.md index 61149da5b..cbfd2078b 100644 --- a/macOS/ARCHITECTURE.md +++ b/macOS/ARCHITECTURE.md @@ -25,7 +25,8 @@ macOS/ │ │ └── SKILL.md # Per-example agent guide (present or forthcoming) │ ├── Common/ # Shared utilities (KeyCenter, GlobalSettings, LogUtils, Util) │ ├── Resources/ -│ ├── Base.lproj/ # Storyboard and localization +│ ├── Base.lproj/ # Base storyboards +│ ├── zh-Hans.lproj/ # Simplified Chinese project localization │ ├── AppDelegate.swift │ └── ViewController.swift # Main window controller ├── SimpleFilter/ # Specialized filter example @@ -33,10 +34,9 @@ macOS/ ├── APIExample.xcworkspace/ # Xcode workspace ├── libs/ # SDK libraries ├── Pods/ # CocoaPods dependencies -├── .agent/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 ``` @@ -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 @@ -78,12 +78,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/` | `setBeautyEffectOptions()`, `setVideoEncoderConfiguration()` | Beauty filter and enhancement effects | -| AudioMixing | `Examples/Advanced/AudioMixing/` | `startAudioMixing()`, `stopAudioMixing()`, `pauseAudioMixing()`, `resumeAudioMixing()` | Audio file mixing and playback control | +| AgoraBeauty | `Examples/Advanced/AgoraBeauty/` | `createVideoEffectObject()`, `setVideoEffectStringParam()`, `enableVirtualBackground()` | Beauty filter and enhancement effects | +| 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 | @@ -93,10 +93,9 @@ 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 | -| LocalVideoTranscoding | `Examples/Advanced/LocalVideoTranscoding/` | `startLocalVideoTranscoding()`, `updateLocalTranscodingConfig()`, `stopLocalVideoTranscoding()` | Local video transcoding and composition | +| LiveStreaming | `Examples/Advanced/LiveStreaming/` | `setClientRole()`, `addVideoWatermark()` with `WatermarkConfig`, `startRtmpStreamWithTranscoding()`, `stopRtmpStream()` | Live streaming with watermark configuration and RTMP transcoding | +| 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 | @@ -108,7 +107,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 | @@ -117,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/macOS/ExportOptions.plist b/macOS/ExportOptions.plist index d85797e0a..614d0fc61 100644 --- a/macOS/ExportOptions.plist +++ b/macOS/ExportOptions.plist @@ -5,17 +5,10 @@ destination export method - developer - provisioningProfiles - - io.agora.api.examples - AgoraLab2020 - - signingCertificate - Apple Development + debugging signingStyle - manual + automatic teamID - JDPG69R49Z + YS397FG5PA diff --git a/macOS/Podfile b/macOS/Podfile index 8d96c544b..f0e61fc21 100644 --- a/macOS/Podfile +++ b/macOS/Podfile @@ -1,8 +1,9 @@ -# 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 - pod 'AgoraRtcEngine_macOS', '4.6.2' + pod 'AgoraRtcEngine_macOS', '4.7.0' # pod 'sdk', :path => 'sdk.podspec' end @@ -21,10 +22,13 @@ 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' + 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 18db0efa9..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 @@ -15,14 +16,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 ``` @@ -64,16 +61,33 @@ To build and run the sample application, get an App Id: Then you can compile and run the project using `APIExample.xcworkspace`. +### Agora Beauty 2.0 Resources + +The Agora Beauty 2.0 material package is not included in this repository. Before building locally, obtain `AgoraBeautyMaterial.bundle.zip` from Agora technical support and extract it to `macOS/APIExample/Resources/`. + +The extracted resources should have the following structure: + +```text +macOS/APIExample/Resources/ +└── AgoraBeautyMaterial.bundle/ + ├── beauty_material_functional/ + │ ├── config.json + │ └── ... + └── ... +``` + +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) - 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/macOS/README.zh.md b/macOS/README.zh.md new file mode 100644 index 000000000..2863bc596 --- /dev/null +++ b/macOS/README.zh.md @@ -0,0 +1,72 @@ +# API Example macOS + +*[English](README.md) | 中文* + +本项目提供一组 Agora RTC API 示例,帮助你了解各项 macOS API 的使用方式。 + +## 环境准备 + +- macOS 12.0 或更高版本 +- Xcode 13.1 或更高版本 + +## 快速开始 + +以下步骤说明如何安装依赖、配置项目并运行示例。 + +### 安装依赖 + +进入当前示例目录,然后安装 CocoaPods 依赖。CocoaPods 的安装方式请参考[官方指南](https://guides.cocoapods.org/using/getting-started.html)。 + +```bash +cd macOS +pod install +``` + +确认 `APIExample.xcworkspace` 已成功生成。 + +### 获取 App ID + +1. 登录 [Agora Console](https://console.agora.io/) 并创建项目。 +2. 在项目详情页复制 App ID。 +3. 使用 Xcode 打开 `APIExample.xcworkspace`。 +4. 编辑 `APIExample/Common/KeyCenter.swift`,填入 App ID: + + ```swift + static let AppId: String = "" + static let Certificate: String? = nil + ``` + +如果项目未启用 App Certificate,请保持 `Certificate` 为 `nil`。App Certificate 属于敏感信息,生产环境应在服务端生成 Token,不应将证书写入客户端或提交到 Git。 + +配置完成后,即可在 Xcode 中构建并运行项目。 + +### Agora 美颜 2.0 资源 + +Agora 美颜 2.0 资源包不包含在仓库中。本地构建前,从 Agora 技术支持获取 `AgoraBeautyMaterial.bundle.zip`,并解压到 `macOS/APIExample/Resources/`。 + +解压后的资源目录结构如下: + +```text +macOS/APIExample/Resources/ +└── AgoraBeautyMaterial.bundle/ + ├── beauty_material_functional/ + │ ├── config.json + │ └── ... + └── ... +``` + +解压后的 bundle 已被 Git 忽略。 + +## 联系我们 + +- 常见问题请查看 [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) +- 完整 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/macOS/cloud_build.sh b/macOS/cloud_build.sh index 097081cae..2559ad5fd 100755 --- a/macOS/cloud_build.sh +++ b/macOS/cloud_build.sh @@ -1,76 +1,54 @@ #!/usr/bin/env sh +set -eu + export LANG=en_US.UTF-8 export PATH=$PATH:/opt/homebrew/bin PROJECT_PATH=$PWD +WORKSPACE=${WORKSPACE:-$PWD} +SIGNING_TEAM="YS397FG5PA" -if [ "$WORKSPACE" = "" ]; then - WORKSPACE=$PWD -fi +: "${BUILD_NUMBER:?BUILD_NUMBER is required}" +: "${APP_ID:?APP_ID is required}" +: "${JFROG_API_KEY:?JFROG_API_KEY is required}" + +cd "${PROJECT_PATH}" +curl --fail --location -H "X-JFrog-Art-Api:${JFROG_API_KEY}" -o AgoraBeautyMaterial.bundle.zip "https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/AgoraBeautyMaterial.bundle.zip" +rm -rf APIExample/Resources/AgoraBeautyMaterial.bundle +unzip -q AgoraBeautyMaterial.bundle.zip -d APIExample/Resources +rm -f AgoraBeautyMaterial.bundle.zip +test -f APIExample/Resources/AgoraBeautyMaterial.bundle/beauty_material_functional/config.json -cd ${PROJECT_PATH} && pod install || exit 1 +pod install # Build environment CONFIGURATION="Debug" # Project file path -APP_PATH="$(ls | grep xcworkspace)" +APP_PATH=$(find . -maxdepth 1 -type d -name '*.xcworkspace' -print -quit) +if [ -z "${APP_PATH}" ]; then + echo "Error: No Xcode workspace found in ${PROJECT_PATH}" + exit 1 +fi +APP_PATH=${APP_PATH#./} # Project target name -TARGET_NAME=${APP_PATH%%.*} +TARGET_NAME=${APP_PATH%%.*} KEYCENTER_PATH=$TARGET_NAME/Common/KeyCenter.swift -# Project configuration path -PBXPROJ_PATH=${TARGET_NAME}.xcodeproj/project.pbxproj - -# Main project configuration -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:CODE_SIGN_IDENTITY 'Developer ID Application'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:CODE_SIGN_IDENTITY 'Developer ID Application'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:DEVELOPMENT_TEAM 'GM72UGLGZW'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:PROVISIONING_PROFILE_SPECIFIER 'App'" $PBXPROJ_PATH - -# SimpleFilter -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:8BD4AE7E272518D600E95B87:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8BD4AE7E272518D600E95B87:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8BD4AE7E272518D600E95B87:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:8BD4AE7F272518D600E95B87:buildSettings:CODE_SIGN_STYLE 'Manual'" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8BD4AE7F272518D600E95B87:buildSettings:DEVELOPMENT_TEAM ''" $PBXPROJ_PATH -/usr/libexec/PlistBuddy -c "Set :objects:8BD4AE7F272518D600E95B87:buildSettings:PROVISIONING_PROFILE_SPECIFIER ''" $PBXPROJ_PATH - -# Modify build number -# Debug -/usr/libexec/PlistBuddy -c "Set :objects:03896D5324F8A011008593CD:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH -# Release -/usr/libexec/PlistBuddy -c "Set :objects:03896D5424F8A011008593CD:buildSettings:CURRENT_PROJECT_VERSION ${BUILD_NUMBER}" $PBXPROJ_PATH - - # Read APPID environment variable -echo AGORA_APP_ID: $APP_ID +echo "AGORA_APP_ID is configured" -echo PROJECT_PATH: $PROJECT_PATH -echo TARGET_NAME: $TARGET_NAME -echo KEYCENTER_PATH: $KEYCENTER_PATH -echo APP_PATH: $APP_PATH +echo PROJECT_PATH: "$PROJECT_PATH" +echo TARGET_NAME: "$TARGET_NAME" +echo KEYCENTER_PATH: "$KEYCENTER_PATH" +echo APP_PATH: "$APP_PATH" # Modify Keycenter file -sed -i -e "s#<\#YOUR AppId\#>#\"$APP_ID\"#g" $KEYCENTER_PATH -rm -f ${KEYCENTER_PATH}-e - -# Xcode clean -xcodebuild clean -workspace "${APP_PATH}" -configuration "${CONFIGURATION}" -scheme "${TARGET_NAME}" - -# Timestamp -CURRENT_TIME=$(date "+%Y-%m-%d %H-%M-%S") +sed -i -e "s#<\#YOUR AppId\#>#\"$APP_ID\"#g" "${KEYCENTER_PATH}" +rm -f "${KEYCENTER_PATH}-e" # Archive path ARCHIVE_PATH="${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}.xcarchive" @@ -79,27 +57,53 @@ ARCHIVE_PATH="${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}.xcarchive" # Plist path PLIST_PATH="${PROJECT_PATH}/ExportOptions.plist" +EXPORT_PATH="${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}_export" + +echo PLIST_PATH: "$PLIST_PATH" + +# Archive with Xcode-managed development signing for the configured team. +rm -rf "${ARCHIVE_PATH}" "${EXPORT_PATH}" +xcodebuild \ + -workspace "${APP_PATH}" \ + -scheme "${TARGET_NAME}" \ + -configuration "${CONFIGURATION}" \ + -destination 'generic/platform=macOS' \ + -archivePath "${ARCHIVE_PATH}" \ + -allowProvisioningUpdates \ + CODE_SIGN_STYLE=Automatic \ + CODE_SIGN_IDENTITY="Apple Development" \ + DEVELOPMENT_TEAM="${SIGNING_TEAM}" \ + PROVISIONING_PROFILE_SPECIFIER= \ + CURRENT_PROJECT_VERSION="${BUILD_NUMBER}" \ + clean archive + +if [ ! -d "${ARCHIVE_PATH}/Products/Applications/${TARGET_NAME}.app" ]; then + echo "Error: Archive does not contain ${TARGET_NAME}.app" + exit 1 +fi -echo PLIST_PATH: $PLIST_PATH - -# Archive using workspace (can also use project) -xcodebuild archive -workspace "${APP_PATH}" -scheme "${TARGET_NAME}" -configuration "${CONFIGURATION}" -archivePath "${ARCHIVE_PATH}" - -cd ${WORKSPACE} - -# Compress archive -7za a -tzip "${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" "${ARCHIVE_PATH}" - -echo "start sign..." - -# Sign -sh sign "${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}.xcarchive.zip" --type xcarchive --plist "${PLIST_PATH}" --application macApp - +mkdir -p "${EXPORT_PATH}" +xcodebuild -exportArchive \ + -archivePath "${ARCHIVE_PATH}" \ + -exportPath "${EXPORT_PATH}" \ + -exportOptionsPlist "${PLIST_PATH}" \ + -allowProvisioningUpdates + +EXPORTED_APP="${EXPORT_PATH}/${TARGET_NAME}.app" +if [ ! -d "${EXPORTED_APP}" ]; then + echo "Error: Xcode export did not produce ${TARGET_NAME}.app" + exit 1 +fi +codesign --verify --deep --strict --verbose=2 "${EXPORTED_APP}" -SDK_VERSION=$(echo $sdk_url | cut -d "/" -f 5) +SDK_VERSION=$(echo "${sdk_url:-unknown}" | cut -d "/" -f 5) OUTPUT_FILE=${WORKSPACE}/${TARGET_NAME}_${BUILD_NUMBER}_${SDK_VERSION}_$(date "+%Y%m%d%H%M%S").app.zip -mv ${TARGET_NAME}_${BUILD_NUMBER}.app.zip $OUTPUT_FILE +rm -f "${OUTPUT_FILE}" +ditto -c -k --sequesterRsrc --keepParent "${EXPORTED_APP}" "${OUTPUT_FILE}" +if [ ! -s "${OUTPUT_FILE}" ]; then + echo "Error: App package was not created" + exit 1 +fi -rm -rf *.xcarchive -rm -rf *.xcarchive.zip -echo OUTPUT_FILE: $OUTPUT_FILE +rm -rf "${ARCHIVE_PATH}" "${EXPORT_PATH}" +echo OUTPUT_FILE: "$OUTPUT_FILE" 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 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/.agent/skills/review-case/SKILL.md b/windows/.agent/skills/review-case/SKILL.md deleted file mode 100644 index ead1ccdfa..000000000 --- a/windows/.agent/skills/review-case/SKILL.md +++ /dev/null @@ -1,461 +0,0 @@ ---- -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 - version: 1.0.0 - 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) diff --git a/windows/.agent/skills/review-case/references/correct-lifecycle.cpp b/windows/.agent/skills/review-case/references/correct-lifecycle.cpp deleted file mode 100644 index 1e2b464ac..000000000 --- a/windows/.agent/skills/review-case/references/correct-lifecycle.cpp +++ /dev/null @@ -1,27 +0,0 @@ -// Correct Engine Lifecycle Pattern - -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(); -} diff --git a/windows/.agent/skills/upsert-case/references/example-template.cpp b/windows/.agent/skills/upsert-case/references/example-template.cpp deleted file mode 100644 index 4d53ce765..000000000 --- a/windows/.agent/skills/upsert-case/references/example-template.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "stdafx.h" -#include "APIExample.h" -#include "CDlg.h" -#include "CConfig.h" - -IMPLEMENT_DYNAMIC(CDlg, CDialogEx) - -// Event Handler Implementation -void CRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) { - if (m_hMsgHandler) { - ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOIN_CHANNEL_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) { - if (m_hMsgHandler) { - ::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; - } -} - -void CDlg::DoDataExchange(CDataExchange* pDX) { - CDialogEx::DoDataExchange(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) -END_MESSAGE_MAP() - -BOOL CDlg::OnInitDialog() { - CDialogEx::OnInitDialog(); - InitializeAgoraEngine(); - return TRUE; -} - -void CDlg::PostNcDestroy() { - LeaveChannel(); - CDialogEx::PostNcDestroy(); - delete this; -} - -void CDlg::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); - m_rtcEngine->enableVideo(); - m_rtcEngine->enableAudio(); -} - -void CDlg::JoinChannel() { - if (!m_rtcEngine) return; - - const char* token = CConfig::GetToken("test"); - m_rtcEngine->joinChannel(token, "test", "", 0); -} - -void CDlg::LeaveChannel() { - if (!m_rtcEngine) return; - - m_rtcEngine->leaveChannel(); - m_isJoined = false; -} - -void CDlg::OnBnClickedButtonJoin() { - JoinChannel(); -} - -void CDlg::OnBnClickedButtonLeave() { - LeaveChannel(); -} - -LRESULT CDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { - // Handle engine events - return 0; -} diff --git a/windows/.agent/skills/upsert-case/references/example-template.h b/windows/.agent/skills/upsert-case/references/example-template.h deleted file mode 100644 index d84095b53..000000000 --- a/windows/.agent/skills/upsert-case/references/example-template.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include "AGVideoWnd.h" -#include -#include - -// Event handler for Agora engine callbacks -class CRtcEngineEventHandler : public IRtcEngineEventHandler { -public: - 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; - -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_ }; - -protected: - virtual BOOL OnInitDialog(); - virtual void DoDataExchange(CDataExchange* pDX); - virtual void PostNcDestroy(); - - afx_msg void OnBnClickedButtonJoin(); - afx_msg void OnBnClickedButtonLeave(); - afx_msg LRESULT OnMsgEngineEvent(WPARAM wParam, LPARAM lParam); - - DECLARE_MESSAGE_MAP() - -private: - void InitializeAgoraEngine(); - void JoinChannel(); - void LeaveChannel(); - void UpdateUI(); - - IRtcEngine* m_rtcEngine = nullptr; - CRtcEngineEventHandler m_eventHandler; - uid_t m_remoteUid = 0; - bool m_isJoined = false; -}; diff --git a/windows/.agent/skills/upsert-case/references/lifecycle-pattern.cpp b/windows/.agent/skills/upsert-case/references/lifecycle-pattern.cpp deleted file mode 100644 index 506c9a003..000000000 --- a/windows/.agent/skills/upsert-case/references/lifecycle-pattern.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// Proper Engine Lifecycle Pattern - -// Create -m_rtcEngine = createAgoraRtcEngine(); - -// Initialize -RtcEngineContext context; -context.appId = CConfig::GetAppId(); -context.eventHandler = &m_eventHandler; -m_rtcEngine->initialize(context); - -// Use -m_rtcEngine->joinChannel(token, channelName, "", 0); - -// Cleanup -m_rtcEngine->leaveChannel(); -m_rtcEngine->release(); -m_rtcEngine = nullptr; diff --git a/windows/.agent/skills/upsert-case/references/message-map-pattern.cpp b/windows/.agent/skills/upsert-case/references/message-map-pattern.cpp deleted file mode 100644 index 211e774c9..000000000 --- a/windows/.agent/skills/upsert-case/references/message-map-pattern.cpp +++ /dev/null @@ -1,17 +0,0 @@ -// Message Map Pattern - Thread-safe UI updates - -// Header -BEGIN_MESSAGE_MAP(CExampleDlg, CDialogEx) - ON_BN_CLICKED(IDC_BUTTON_JOIN, &CExampleDlg::OnBnClickedButtonJoin) - ON_MESSAGE(WM_MSGID(EID_JOIN_CHANNEL_SUCCESS), &CExampleDlg::OnMsgEngineEvent) -END_MESSAGE_MAP() - -// Implementation -void CExampleDlg::OnBnClickedButtonJoin() { - JoinChannel(); -} - -LRESULT CExampleDlg::OnMsgEngineEvent(WPARAM wParam, LPARAM lParam) { - // Handle engine events on main thread - return 0; -} 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/skills/review-case/SKILL.md b/windows/.agents/skills/review-case/SKILL.md new file mode 100644 index 000000000..2122619aa --- /dev/null +++ b/windows/.agents/skills/review-case/SKILL.md @@ -0,0 +1,107 @@ +--- +name: review-case +description: > + Code review for API examples. Ensures examples follow project conventions, + handle lifecycle correctly, manage threads safely, and use APIs properly. +license: MIT +metadata: + author: APIExample Team + version: 1.0.0 + platform: Windows +--- + +# 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 new file mode 100644 index 000000000..e610c89f3 --- /dev/null +++ b/windows/.agents/skills/review-case/references/correct-lifecycle.cpp @@ -0,0 +1,20 @@ +// 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(); + // Initialize controls only. + return TRUE; +} + +// ReleaseScene() must call this BEFORE ShowWindow(SW_HIDE). +void CExampleDlg::UnInitAgora() { + // Invalidate pending Token/permission requests first. + if (m_rtcEngine) { + 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; + } + m_eventHandler.SetMsgReceiver(nullptr); + DiscardEngineMessages(); // See the upsert-case template for the owned message IDs. +} 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 63% rename from windows/.agent/skills/review-case/references/correct-thread-safety.cpp rename to windows/.agents/skills/review-case/references/correct-thread-safety.cpp index 487cc5b7c..46a94aa26 100644 --- a/windows/.agent/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/.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 89% rename from windows/.agent/skills/upsert-case/SKILL.md rename to windows/.agents/skills/upsert-case/SKILL.md index f809c4b4a..85da7cd32 100644 --- a/windows/.agent/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/.agent/skills/upsert-case/references/event-handler-pattern.cpp b/windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp similarity index 53% rename from windows/.agent/skills/upsert-case/references/event-handler-pattern.cpp rename to windows/.agents/skills/upsert-case/references/event-handler-pattern.cpp index e4dc26afc..04c8c4a0c 100644 --- a/windows/.agent/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 new file mode 100644 index 000000000..661e6ba93 --- /dev/null +++ b/windows/.agents/skills/upsert-case/references/example-template.cpp @@ -0,0 +1,134 @@ +#include "stdafx.h" +#include "APIExample.h" +#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) + +void CRtcEngineEventHandler::onJoinChannelSuccess( + const char* channel, agora::rtc::uid_t uid, int elapsed) { + if (m_hMsgHandler) { + ::PostMessage(m_hMsgHandler, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} + +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); + } +} + +CDlg::CDlg(CWnd* pParent) + : CDialogEx(IDD_DIALOG_, pParent) { +} + +CDlg::~CDlg() { + UnInitAgora(); // Fallback; ReleaseScene must clean up before hiding this window. +} + +void CDlg::DoDataExchange(CDataExchange* pDX) { + CDialogEx::DoDataExchange(pDX); +} + +BEGIN_MESSAGE_MAP(CDlg, CDialogEx) + 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(); + // UI only: the host precreates all scenes, including inactive ones. + return TRUE; +} + +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 false; + + m_eventHandler.SetMsgReceiver(m_hWnd); + RtcEngineContext context; + context.appId = m_appId.c_str(); + context.eventHandler = &m_eventHandler; + 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; + 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; + int result = m_rtcEngine->leaveChannel(); + if (result != 0) TRACE(_T("leaveChannel failed: %d\n"), result); +} + +void CDlg::OnBnClickedButtonJoin() { JoinChannel(); } +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::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 new file mode 100644 index 000000000..44fac10d1 --- /dev/null +++ b/windows/.agents/skills/upsert-case/references/example-template.h @@ -0,0 +1,46 @@ +#pragma once +#include + +// 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; } + 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; +}; + +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: + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; + afx_msg void OnBnClickedButtonJoin(); + afx_msg void OnBnClickedButtonLeave(); + afx_msg LRESULT OnMsgJoinChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnMsgError(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() + +private: + void JoinChannel(); + void LeaveChannel(); + void DiscardEngineMessages(); + + IRtcEngine* m_rtcEngine = nullptr; + CRtcEngineEventHandler m_eventHandler; + 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 new file mode 100644 index 000000000..2c09c6772 --- /dev/null +++ b/windows/.agents/skills/upsert-case/references/lifecycle-pattern.cpp @@ -0,0 +1,30 @@ +// Scene lifecycle fragments; see example-template.h/.cpp for the complete skeleton. + +// OnInitDialog(): UI only. APIExampleDlg precreates inactive dialogs. + +// 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 = m_appId.c_str(); +context.eventHandler = &m_eventHandler; +if (m_rtcEngine->initialize(context) != 0) { + UnInitAgora(); + return false; +} + +// 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. + +// 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 new file mode 100644 index 000000000..80919c419 --- /dev/null +++ b/windows/.agents/skills/upsert-case/references/message-map-pattern.cpp @@ -0,0 +1,17 @@ +// 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() + +// At file scope in the .cpp, not inside the class declaration: +BEGIN_MESSAGE_MAP(CExampleDlg, CDialogEx) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CExampleDlg::OnMsgJoinChannel) + ON_MESSAGE(WM_MSGID(EID_ERROR), &CExampleDlg::OnMsgError) +END_MESSAGE_MAP() + +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 5e3601839..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 @@ -62,9 +61,10 @@ 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 | +| 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/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..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 @@ -295,7 +292,6 @@ if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIEx - @@ -354,7 +350,6 @@ if exist $(SolutionDir)APIExample\res\hdr_1280_720.yuv (copy $(SolutionDir)APIEx - @@ -460,4 +455,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..765f7cb69 100755 --- a/windows/APIExample/APIExample/APIExampleDlg.cpp +++ b/windows/APIExample/APIExample/APIExampleDlg.cpp @@ -225,43 +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(advancedFaceCapture); - 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); @@ -312,10 +309,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); @@ -444,7 +437,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); } @@ -596,10 +589,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 +734,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..f4379a758 100755 --- a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp +++ b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp @@ -516,8 +516,8 @@ 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); + CString audioUrl = GetExePath() + _T("\\res\\ID_MUSIC_01.m4a"); + 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/Beauty2.0/CDlgBeauty2.0.cpp b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.cpp index 667316d5b..2a21dc705 100644 --- a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.cpp +++ b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.cpp @@ -190,6 +190,10 @@ bool CDlgBeauty2::InitAgora() m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("agora_video_filters_clear_vision")); m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("clear_vision")); + if (ret == 0) { + m_rtcEngine->setParameters("{\"rtc.video.yuvconverter_enable_hardware_buffer\":true}"); + InitializeBeautyResources(); + } //enable video in the engine. m_rtcEngine->enableVideo(); @@ -256,9 +260,11 @@ void CDlgBeauty2::UnInitAgora() void CDlgBeauty2::CleanupBeautyResources() { if (m_videoEffectObject && m_rtcEngine) { + mBeautyDlgEx->SetVideoEffectObject(nullptr); m_rtcEngine->destroyVideoEffectObject(m_videoEffectObject); - m_videoEffectObject = nullptr; - + m_videoEffectObject.reset(); + } + if (m_rtcEngine) { m_rtcEngine->enableExtension( "agora_video_filters_clear_vision", "clear_vision", @@ -268,6 +274,33 @@ void CDlgBeauty2::CleanupBeautyResources() } } +void CDlgBeauty2::InitializeBeautyResources() +{ + if (!m_rtcEngine || !m_initialize || m_videoEffectObject) { + return; + } + + CString strModelPath = GetExePath() + _T("\\beauty_agora\\beauty_material_functional"); + if (!PathFileExists(strModelPath)) { + CString strMsg; + strMsg.Format(_T("Beauty resource path not exist: %s"), strModelPath); + AfxMessageBox(strMsg); + return; + } + + std::string modelPath = cs2utf8(strModelPath); + m_videoEffectObject = m_rtcEngine->createVideoEffectObject( + modelPath.c_str(), + agora::media::PRIMARY_CAMERA_SOURCE + ); + if (!m_videoEffectObject) { + AfxMessageBox(_T("Create VideoEffectObject failed")); + return; + } + + mBeautyDlgEx->SetVideoEffectObject(m_videoEffectObject.get()); +} + //Initialize the Ctrl Text. void CDlgBeauty2::InitCtrlText() { @@ -347,7 +380,7 @@ BOOL CDlgBeauty2::OnInitDialog() { CDialogEx::OnInitDialog(); mBeautyDlgEx->Create(CDlgBeautyEx2::IDD); - mBeautyDlgEx->initData(&m_initialize, &m_rtcEngine, this); + mBeautyDlgEx->initData(this); mBeautyDlgEx->InitCtrlText(); InitCtrlText(); for (int i = 0; i < 2; ++i) { @@ -366,22 +399,27 @@ void CDlgBeauty2::SetBeauty() { if (!m_rtcEngine || !m_initialize) return; - BeautyOptions option; - option.rednessLevel = m_sldRedness.GetPos() / 100.0f; - option.lighteningLevel = m_sdlLightening.GetPos() / 100.0f; - option.smoothnessLevel = m_sldSmoothness.GetPos() / 100.0f; - option.lighteningContrastLevel = (agora::rtc::BeautyOptions::LIGHTENING_CONTRAST_LEVEL)m_cmbContrast.GetCurSel(); - int ret = m_rtcEngine->setBeautyEffectOptions(m_chkBeauty.GetCheck() != 0, option); - if (ret < 0) { - CString strInfo; - strInfo.Format(_T("setBeautyEffectOptions: %d"), ret); - m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + if (m_videoEffectObject) { + bool enabled = m_chkBeauty.GetCheck() != 0; + if (enabled) { + m_videoEffectObject->addOrUpdateVideoEffect( + static_cast(agora::rtc::IVideoEffectObject::VIDEO_EFFECT_NODE_ID::BEAUTY), + "" + ); + m_videoEffectObject->setVideoEffectBoolParam("face_shape_beauty_option", "enable", false); + } + m_videoEffectObject->setVideoEffectBoolParam("beauty_effect_option", "enable", enabled); + m_videoEffectObject->setVideoEffectFloatParam("beauty_effect_option", "redness", m_sldRedness.GetPos() / 100.0f); + m_videoEffectObject->setVideoEffectStringParam("beauty_effect_option", "whiten_lut_path", ""); + m_videoEffectObject->setVideoEffectFloatParam("beauty_effect_option", "lightness", m_sdlLightening.GetPos() / 100.0f); + m_videoEffectObject->setVideoEffectFloatParam("beauty_effect_option", "smoothness", m_sldSmoothness.GetPos() / 100.0f); + m_videoEffectObject->setVideoEffectIntParam("beauty_effect_option", "contrast", m_cmbContrast.GetCurSel()); } FilterEffectOptions optionFilter; optionFilter.path = "built_in_whiten_filter"; optionFilter.strength = mSliderBright.GetPos() / 100.0f; - ret = m_rtcEngine->setFilterEffectOptions(m_chkBeauty.GetCheck() != 0, optionFilter); + int ret = m_rtcEngine->setFilterEffectOptions(m_chkBeauty.GetCheck() != 0, optionFilter); if (ret < 0) { CString strInfo; strInfo.Format(_T("setFilterEffectOptions: %d"), ret); diff --git a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.h b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.h index 40648d179..b4575f75d 100644 --- a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.h +++ b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeauty2.0.h @@ -79,7 +79,7 @@ class CDlgBeauty2 : public CDialogEx bool m_initialize = false; bool m_joinChannel = false; agora::rtc::IRtcEngine* m_rtcEngine = nullptr; - agora::rtc::IVideoEffectObject* m_videoEffectObject = nullptr; + agora_refptr m_videoEffectObject; CBeautyEventHandler2 m_eventHandler; CAGVideoWnd m_videoWnds[2]; CDlgBeautyEx2* mBeautyDlgEx; diff --git a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.cpp b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.cpp index 00f70541c..b1b818af3 100644 --- a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.cpp +++ b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.cpp @@ -74,92 +74,21 @@ BOOL CDlgBeautyEx2::OnInitDialog() HideFaceShapeControls(); - if (m_initialize && *m_initialize && m_rtcEngine && *m_rtcEngine) { - InitializeBeautyResources(); - } - return TRUE; } -void CDlgBeautyEx2::initData(bool* initialize, agora::rtc::IRtcEngine** engine, CDlgBeauty2* beautyDlg) +void CDlgBeautyEx2::initData(CDlgBeauty2* beautyDlg) { - m_initialize = initialize; - m_rtcEngine = engine; m_beautyDlg = beautyDlg; } -void CDlgBeautyEx2::InitializeBeautyResources() +void CDlgBeautyEx2::SetVideoEffectObject(agora::rtc::IVideoEffectObject* videoEffectObject) { - if (!m_initialize || !*m_initialize || !m_rtcEngine || !*m_rtcEngine) { - return; - } - - if (m_videoEffectObjectRef) { - return; - } - - int ret = (*m_rtcEngine)->enableExtension("agora_video_filters_clear_vision", "clear_vision", true); - if (ret != 0) { - CString strMsg; - strMsg.Format(_T("Enable beauty extension failed: %d"), ret); - AfxMessageBox(strMsg); - return; - } - - (*m_rtcEngine)->setParameters("{\"rtc.video.yuvconverter_enable_hardware_buffer\":true}"); - - CString strExePath = GetExePath(); - CString strModelPath = strExePath + _T("\\beauty_agora\\beauty_material.bundle\\beauty_material_v2.0.0"); - - if (!PathFileExists(strModelPath)) { - CString strMsg; - strMsg.Format(_T("Beauty resource path not exist: %s"), strModelPath); - AfxMessageBox(strMsg); - return; - } - - std::string modelPath = cs2utf8(strModelPath); - m_videoEffectObjectRef = (*m_rtcEngine)->createVideoEffectObject( - modelPath.c_str(), - agora::media::PRIMARY_CAMERA_SOURCE - ); - - if (m_videoEffectObjectRef) { - CString strInfo; - strInfo.Format(_T("Beauty resource loaded successfully: %s"), strModelPath); - AfxMessageBox(strInfo); - } else { - AfxMessageBox(_T("Create VideoEffectObject failed")); - } -} - -CString CDlgBeautyEx2::GetExePath() -{ - TCHAR szPath[MAX_PATH]; - GetModuleFileName(NULL, szPath, MAX_PATH); - CString strExePath = szPath; - int nPos = strExePath.ReverseFind('\\'); - return strExePath.Left(nPos); -} - -void CDlgBeautyEx2::CleanupBeautyResources() -{ - if (m_videoEffectObjectRef && m_rtcEngine && *m_rtcEngine) { - (*m_rtcEngine)->enableExtension( - "agora_video_filters_clear_vision", - "clear_vision", - false, - agora::media::PRIMARY_CAMERA_SOURCE - ); - - m_videoEffectObjectRef = nullptr; - } + m_videoEffectObjectRef = videoEffectObject; } void CDlgBeautyEx2::OnClose() { - CleanupBeautyResources(); - if (m_beautyDlg) { m_beautyDlg->UpdateExtentCbState(false); } @@ -324,10 +253,6 @@ void CDlgBeautyEx2::HideFaceShapeControls() void CDlgBeautyEx2::SetBeauty() { - if (!m_videoEffectObjectRef) { - InitializeBeautyResources(); - } - if (!m_videoEffectObjectRef) { AfxMessageBox(_T("VideoEffectObject not created successfully")); return; diff --git a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.h b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.h index 1e81ca851..89230d656 100644 --- a/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.h +++ b/windows/APIExample/APIExample/Advanced/Beauty2.0/CDlgBeautyEx2.0.h @@ -75,8 +75,6 @@ class CDlgBeautyEx2 : public CDialogEx IDD = IDD_DIALOG_BEAUTY_EX }; private: - bool* m_initialize = nullptr; - agora::rtc::IRtcEngine** m_rtcEngine = nullptr; CDlgBeauty2* m_beautyDlg = nullptr; MakeupOptions2 m_makeupOptions; FaceShapeAreaOptions m_faceShapeAreaOptions; @@ -86,10 +84,7 @@ class CDlgBeautyEx2 : public CDialogEx std::string GetFaceShapeAreaParamName(FaceShapeAreaOptions::FACE_SHAPE_AREA area); int MapUIToResourceId(const CString& resourceType, int uiIndex); - void InitializeBeautyResources(); - void CleanupBeautyResources(); void HideFaceShapeControls(); - CString GetExePath(); protected: virtual void DoDataExchange(CDataExchange* pDX); @@ -98,7 +93,8 @@ class CDlgBeautyEx2 : public CDialogEx DECLARE_MESSAGE_MAP() public: - void initData(bool* initialize, agora::rtc::IRtcEngine** engine, CDlgBeauty2* beautyDlg); + void initData(CDlgBeauty2* beautyDlg); + void SetVideoEffectObject(agora::rtc::IVideoEffectObject* videoEffectObject); void InitCtrlText(); void InitCtrlData(); void SetBeauty(); 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/PushExternalVideoYUV/PushExternalVideoYUV.cpp b/windows/APIExample/APIExample/Advanced/PushExternalVideoYUV/PushExternalVideoYUV.cpp index 55016324d..ee28d059f 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; @@ -267,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/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/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/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/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/APIExample/en.ini b/windows/APIExample/APIExample/en.ini index 4c1e7fc51..525c346ef 100755 --- a/windows/APIExample/APIExample/en.ini +++ b/windows/APIExample/APIExample/en.ini @@ -15,39 +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.FaceCapture=FaceCapture +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 @@ -282,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 @@ -308,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 @@ -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 24cf7c8af..3d02abe32 100644 --- a/windows/APIExample/APIExample/stdafx.h +++ b/windows/APIExample/APIExample/stdafx.h @@ -57,7 +57,6 @@ #pragma comment(lib, "agora_rtc_sdk.dll.lib") #pragma comment(lib, "libagora_segmentation_extension.dll.lib") //#pragma comment(lib, "libagora_video_process.dll.lib") -#pragma comment(lib, "libagora-ffmpeg.dll.lib") using namespace agora; using namespace agora::rtc; using namespace agora::media; @@ -112,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 old mode 100755 new mode 100644 index 4c1e7fc51..ce140ef2e Binary files a/windows/APIExample/APIExample/zh-cn.ini and b/windows/APIExample/APIExample/zh-cn.ini differ diff --git a/windows/APIExample/cloud_build.bat b/windows/APIExample/cloud_build.bat index 5831f0a35..4428052cf 100644 --- a/windows/APIExample/cloud_build.bat +++ b/windows/APIExample/cloud_build.bat @@ -4,7 +4,11 @@ setlocal enabledelayedexpansion echo "compile start..." REM --- 1. Install Dependencies --- -powershell.exe -ExecutionPolicy Bypass -File "%~dp0install.ps1" +powershell.exe -ExecutionPolicy Bypass -File "%~dp0install.ps1" -RequireBeautyMaterial +if %ERRORLEVEL% NEQ 0 ( + echo Dependency installation failed! + exit /b %ERRORLEVEL% +) REM --- 2. Find MSBuild --- set "MSBuildPath=" @@ -46,9 +50,43 @@ 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 +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\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 +) +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 +) xcopy /Y APIExample\*.ini Output\ >nul 2>&1 REM Copy SDK and ThirdParty DLLs (Agora uses sdk\x86 for Win32) diff --git a/windows/APIExample/install.ps1 b/windows/APIExample/install.ps1 index 64a545285..12bef924a 100644 --- a/windows/APIExample/install.ps1 +++ b/windows/APIExample/install.ps1 @@ -1,8 +1,30 @@ -$agora_sdk = 'https://download.agora.io/sdk/release/Agora_Native_SDK_for_Windows_v4.6.2_FULL.zip' +param( + [switch]$RequireBeautyMaterial +) + +$agora_sdk = 'https://download.agora.io/sdk/release/Agora_Native_SDK_for_Windows_v4.7.0_FULL.zip' $ThirdPartysrc = 'https://demo-app-download.agora.io/api_example/ThirdParty.zip' $ThirdPartydes = 'ThirdParty.zip' $agora_des = 'AgoraSdk.zip' $agora_local_sdk = '../../sdk' +$beauty_url = 'https://artifactory-api.bj2.agoralab.co/artifactory/qa_test_data/beauty/AgoraBeautyMaterial.zip' +$beauty_destination = Join-Path $PSScriptRoot 'Release\beauty_agora' + +if ($RequireBeautyMaterial) { + $beauty_zip = Join-Path $PSScriptRoot 'AgoraBeautyMaterial.zip' + Invoke-WebRequest -UseBasicParsing -Uri $beauty_url -Headers @{ + 'X-JFrog-Art-Api' = $env:JFROG_API_KEY + } -OutFile $beauty_zip + if (Test-Path $beauty_destination) { + Remove-Item $beauty_destination -Recurse -Force + } + Expand-Archive -Path $beauty_zip -DestinationPath $beauty_destination -Force + Remove-Item $beauty_zip -Force + $beauty_config = Join-Path $beauty_destination 'beauty_material_functional\config.json' + if (-not (Test-Path $beauty_config -PathType Leaf)) { + throw "Beauty material extraction did not create $beauty_config" + } +} if (-not (Test-Path ThirdParty)){ echo "download $ThirdPartydes" diff --git a/windows/ARCHITECTURE.md b/windows/ARCHITECTURE.md index e92efe599..90049abad 100644 --- a/windows/ARCHITECTURE.md +++ b/windows/ARCHITECTURE.md @@ -37,12 +37,10 @@ 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 -├── .agent/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 ``` @@ -61,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 @@ -77,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 @@ -101,13 +99,12 @@ All examples share utilities: | AudioProfile | `Advanced/AudioProfile/` | `setAudioProfile()`, `setAudioScenario()` | Audio profile and scenario configuration | | AudioVolume | `Advanced/AudioVolume/` | `adjustRecordingSignalVolume()`, `adjustPlaybackSignalVolume()`, `adjustUserPlaybackSignalVolume()` | Audio volume adjustment and control | | Beauty | `Advanced/Beauty/` | `setBeautyEffectOptions()`, `setVideoEncoderConfiguration()` | Beauty filter and enhancement effects | -| Beauty2.0 | `Advanced/Beauty2.0/` | `setBeautyEffectOptions()` with v2.0 API | Enhanced beauty effects with v2.0 API | +| 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 | @@ -134,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/README.md b/windows/README.md index 2cdb371b3..08aa260b8 100644 --- a/windows/README.md +++ b/windows/README.md @@ -38,6 +38,21 @@ You can directly run `APIExample/installThirdParty.bat` to automatically environ > If you run into permission or execution policy errors, try running as administrator, or adjust the local PowerShell execution policy and try again. +### Agora Beauty 2.0 Resources + +The Agora Beauty 2.0 material package is not included in this repository. Before building locally, obtain `AgoraBeautyMaterial.zip` from Agora technical support and extract it to `windows/APIExample/Release/beauty_agora/`. + +The extracted resources should have the following structure: + +```text +windows/APIExample/Release/beauty_agora/ +└── beauty_material_functional/ + ├── config.json + └── ... +``` + +The `Release` directory is ignored by Git. + ## Basic Scene @@ -143,13 +158,13 @@ You can directly run `APIExample/installThirdParty.bat` to automatically environ ## 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) - 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 8ee92750b..b6d87137f 100644 --- a/windows/README.zh.md +++ b/windows/README.zh.md @@ -44,6 +44,21 @@ > 若遇权限或执行策略报错,可尝试以管理员运行,或调整本机 PowerShell 执行策略后再试。 +### Agora 美颜 2.0 资源 + +Agora 美颜 2.0 资源包不包含在仓库中。本地构建前,从 Agora 技术支持获取 `AgoraBeautyMaterial.zip`,并解压到 `windows/APIExample/Release/beauty_agora/`。 + +解压后的资源目录结构如下: + +```text +windows/APIExample/Release/beauty_agora/ +└── beauty_material_functional/ + ├── config.json + └── ... +``` + +`Release` 目录已被 Git 忽略。 + ## 基础场景 ### 直播互动 @@ -166,13 +181,13 @@ ## 联系我们 -- 如果你遇到了困难,可以先参阅[常见问题](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) - 完整的 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) ## 代码许可 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 }} -