diff --git a/.claude/commands/docs-revise-pr.md b/.claude/commands/docs-revise-pr.md
new file mode 100644
index 0000000000..623d528963
--- /dev/null
+++ b/.claude/commands/docs-revise-pr.md
@@ -0,0 +1,227 @@
+# Revise Documentation PR
+
+Apply feedback to an existing docs-content PR.
+
+## Usage
+
+```
+/docs:revise-pr [DOCS_PR_NUMBER]
+```
+
+Then provide feedback when prompted, or include inline:
+
+```
+/docs:revise-pr 959 "Fix the code example in call-content.md - missing import"
+```
+
+## Standards Reference
+
+**IMPORTANT:** All revisions must follow the standards document:
+@.claude/docs-standards.md
+
+Key sections for revisions:
+- §2 Code Blocks - verify code compiles, explicit types
+- §3 Writing Style - terminology, no filler phrases
+- §12 Examples - good vs bad patterns
+
+---
+
+## Process
+
+### Step 1: Fetch PR and Branch
+
+```bash
+PR_NUMBER=$1
+
+# Get PR details including branch name
+gh pr view $PR_NUMBER --repo GetStream/docs-content --json headRefName,title,body,state,files
+
+# Clone and checkout the PR branch
+rm -rf /tmp/docs-content-sync
+mkdir -p /tmp/docs-content-sync
+cd /tmp/docs-content-sync
+git clone https://github.com/GetStream/docs-content.git .
+git checkout [branch-name-from-pr]
+```
+
+### Step 2: Get Feedback
+
+If feedback not provided as argument, ask:
+gs
+```
+What changes are needed?
+
+Common feedback types:
+- Code fixes: "The code example doesn't compile - StreamVideo should be StreamVideoBuilder"
+- Missing content: "Add a note about Android 14 compatibility"
+- Style issues: "Remove the filler phrases in the intro paragraph"
+- Revert: "Revert the changes to permissions.md"
+- Structure: "Move the parameters table before the example"
+```
+
+### Step 3: Read Affected Files
+
+Before making changes:
+1. Read the files mentioned in feedback
+2. Understand current state
+3. Identify exact locations to change
+
+### Step 4: Apply Changes
+
+Based on feedback type:
+
+#### Code Fixes
+
+When fixing code examples, verify against standards §2:
+
+```markdown
+**Checklist:**
+- [ ] Explicit types: `val client: StreamVideo = ...`
+- [ ] Compiles against SDK develop branch
+- [ ] No internal APIs (check `.api` files)
+- [ ] Correct language tag (kotlin, groovy, xml)
+- [ ] Imports included for full samples
+```
+
+#### Writing Fixes
+
+When fixing prose, verify against standards §3:
+
+```markdown
+**Checklist:**
+- [ ] No filler phrases (Basically, Simply, Just, Obviously)
+- [ ] Direct language ("The SDK provides" not "In terms of the SDK")
+- [ ] Correct terminology (customize not custom, participant not user)
+- [ ] Active voice for instructions
+```
+
+#### Structure Fixes
+
+When restructuring, verify against standards §1 and §7:
+
+```markdown
+**Checklist:**
+- [ ] Page follows intro → sections → examples pattern
+- [ ] Heading hierarchy (## for main, ### for sub)
+- [ ] Links use correct format (/video/docs/android/...)
+```
+
+#### Adding Content
+
+When adding new content:
+
+**For notes/warnings (§4):**
+```markdown
+> **Note:** Brief inline note.
+
+
+**Important:** Longer warning with details.
+
+```
+
+**For parameters (§2):**
+```markdown
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `name` | What it does | `value` |
+```
+
+**For API levels (§5):**
+```markdown
+This feature is available on Android 10 (API level 29) and above.
+```
+
+### Step 5: Commit and Push
+
+```bash
+cd /tmp/docs-content-sync
+
+git add .
+
+# Commit - NO AI attribution (§11)
+git commit -m "docs(android): address review feedback
+
+[Summary of changes made]"
+
+git push
+```
+
+### Step 6: Comment on PR
+
+```bash
+gh pr comment $PR_NUMBER --repo GetStream/docs-content --body "Applied feedback:
+
+- [Change 1]
+- [Change 2]
+
+Ready for another look."
+```
+
+### Step 7: Report
+
+```
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ REVISIONS APPLIED
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+PR: GetStream/docs-content#[PR_NUMBER]
+Branch: [branch-name]
+
+Feedback received:
+"[original feedback]"
+
+Changes made:
+- [File 1]: [what changed]
+- [File 2]: [what changed]
+
+Quality verified:
+✓ Code compiles
+✓ Writing style correct
+✓ No AI attribution
+
+Status: Pushed, commented on PR
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+PR URL: [link]
+```
+
+---
+
+## Multiple Revision Rounds
+
+Run this command as many times as needed. Each run:
+1. Fetches latest from the PR branch (gets any manual changes)
+2. Applies new feedback
+3. Pushes and comments
+4. Reports what changed
+
+When satisfied, dev marks PR ready for review manually in GitHub.
+
+---
+
+## Common Feedback Patterns
+
+| Feedback | How to handle |
+|----------|---------------|
+| "Code doesn't compile" | Read SDK source, fix syntax, verify types |
+| "Missing import" | Add import for full samples, or note it's a fragment |
+| "Too wordy" | Apply §3 rules, remove filler, use direct language |
+| "Add API level" | Use format: "Android X (API level Y)" |
+| "Wrong terminology" | Check §3 dictionary, use correct terms |
+| "Needs example" | Add code block with explicit types |
+| "Revert file X" | `git checkout origin/main -- video/android/path/to/file.md` |
+| "Link broken" | Fix to `/video/docs/android/section/page/` format |
+
+---
+
+## Verification Checklist
+
+Before pushing revisions, verify (§13):
+
+- [ ] Changes address the specific feedback
+- [ ] No new issues introduced
+- [ ] Code still compiles (if code was changed)
+- [ ] Writing style consistent with rest of doc
+- [ ] Commit message follows format
+- [ ] No AI attribution anywhere
diff --git a/.claude/commands/docs-sync-sdk-pr.md b/.claude/commands/docs-sync-sdk-pr.md
new file mode 100644
index 0000000000..d37ed26bd0
--- /dev/null
+++ b/.claude/commands/docs-sync-sdk-pr.md
@@ -0,0 +1,312 @@
+# Sync SDK PR to Documentation
+
+Automatically update docs-content when SDK changes require documentation updates.
+
+## Usage
+
+```
+/docs:sync-sdk-pr [PR_NUMBER]
+```
+
+## Standards Reference
+
+**IMPORTANT:** Read and follow all rules in the standards document:
+@.claude/docs-standards.md
+
+Key sections to reference during this workflow:
+- §2 Code Blocks - for code formatting rules
+- §3 Writing Style - for terminology and tone
+- §9 SDK to Docs Mapping - for finding affected files
+- §11 PR Standards - for commit/PR format
+
+---
+
+## Configuration
+
+| Setting | Value |
+|---------|-------|
+| Docs repo | `https://github.com/GetStream/docs-content.git` |
+| Docs path | `video/android/` |
+| Sidebar | `_sidebars/[video][android].json` |
+| Clone to | `/tmp/docs-content-sync/` |
+| Base branch | `main` |
+
+---
+
+## Process
+
+### Step 1: Validate Input
+
+```bash
+PR_NUMBER=$1
+
+if [ -z "$PR_NUMBER" ]; then
+ echo "Usage: /docs:sync-sdk-pr [PR_NUMBER]"
+ exit 1
+fi
+
+# Verify PR exists and get details
+gh pr view $PR_NUMBER --json number,title,body,state,labels,files
+```
+
+Check PR state - warn if not merged (docs should typically sync after merge).
+
+### Step 2: Analyze SDK Changes
+
+```bash
+# Get changed files
+gh pr diff $PR_NUMBER --name-only
+
+# Get full diff for .api files
+gh pr diff $PR_NUMBER -- "*.api" 2>/dev/null || gh pr diff $PR_NUMBER | grep -A50 "\.api"
+
+# Check for deprecation annotations
+gh pr diff $PR_NUMBER | grep -i "@Deprecated\|@deprecated"
+```
+
+**Classify each change (per §9 of standards):**
+
+| Change Type | Detection | Doc Action |
+|-------------|-----------|------------|
+| New public class | Lines added to `.api` file | New section or page |
+| New parameter | Function signature changed in `.api` | Update examples + add to parameters table |
+| Changed signature | Modified line in `.api` file | Update all code examples using it |
+| Deprecated API | `@Deprecated` in diff | Add deprecation notice (§8 format) |
+| Removed API | Lines removed from `.api` file | Remove from docs entirely |
+| Behavior change | PR description mentions it | Update descriptions |
+| New feature | PR labeled `pr:new-feature` | Likely needs new section/page |
+
+### Step 3: Clone docs-content
+
+```bash
+rm -rf /tmp/docs-content-sync
+mkdir -p /tmp/docs-content-sync
+cd /tmp/docs-content-sync
+git clone --depth 1 https://github.com/GetStream/docs-content.git .
+```
+
+### Step 4: Find Affected Doc Files
+
+**Use the mapping table from §9 of standards:**
+
+| SDK Area | Doc Location |
+|----------|--------------|
+| `StreamVideoBuilder` | `03-guides/01-client-auth.md` |
+| `StreamVideo` interface | `03-guides/01-client-auth.md` |
+| `Call` class | `03-guides/02-joining-creating-calls.md` |
+| `call.state.*` | `03-guides/03-call-and-participant-state.md` |
+| `call.camera`, `call.microphone` | `03-guides/04-camera-and-microphone.md` |
+| `call.screenShare` | `06-advanced/04-screen-sharing.md` |
+| `CallContent` | `04-ui-components/04-call/01-call-content.md` |
+| `ParticipantVideo` | `04-ui-components/05-participants/01-participant-video.md` |
+| `VideoTheme` | `04-ui-components/03-video-theme.md` |
+| Video filters | `06-advanced/05-apply-video-filters.md` |
+| Audio filters | `03-guides/05-noise-cancellation.md` |
+| Push notifications | `06-advanced/00-incoming-calls/03-push-notifications.md` |
+| Ringing calls | `06-advanced/00-incoming-calls/02-ringing.md` |
+| Recording | `06-advanced/09-recording.md` |
+| Livestreaming | `03-guides/12-livestreaming.md` |
+| Picture-in-Picture | `06-advanced/03-enable-picture-in-picture.md` |
+| Telecom | `06-advanced/12-telecom.md` |
+
+**Also search directly:**
+
+```bash
+cd /tmp/docs-content-sync
+
+# Search for class/function references
+grep -r "ClassName" video/android/ --include="*.md" -l
+grep -r "functionName" video/android/ --include="*.md" -l
+```
+
+### Step 5: Update Documentation
+
+For each affected file, apply these rules:
+
+#### Code Examples (§2)
+
+- **Explicit types required:** `val client: StreamVideo = ...` not `val client = ...`
+- **Full samples** for first example on page, **fragments** for subsequent
+- **Verify compilation** - code must work against SDK develop branch
+- **No internal APIs** - check `.api` files if unsure
+
+```bash
+# Check if API is public
+cat stream-video-android-*/api/*.api | grep "ClassName"
+```
+
+#### Writing Style (§3)
+
+- **Direct language:** "The SDK provides..." not "In terms of the SDK..."
+- **No filler phrases:** Remove "Basically", "Simply", "Just", "Obviously"
+- **Correct terminology:** "customize" not "custom", "participant" not "user"
+
+#### Parameters Tables
+
+When adding new parameters, use this format:
+
+```markdown
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `paramName` | What it does | `defaultValue` |
+```
+
+#### Deprecations (§8)
+
+Use admonition format:
+
+```markdown
+
+
+**Deprecated:** `oldMethod()` is deprecated and will be removed in version X.Y.Z. Use `newMethod()` instead.
+
+
+```
+
+#### Android-Specific (§5)
+
+- Document API level: "Available on Android 10 (API level 29) and above."
+- Document permissions if required
+- Include version-specific code with `Build.VERSION.SDK_INT` checks
+
+#### New Pages (§7)
+
+If creating a new page:
+1. Use correct number prefix (e.g., `14-new-feature.md`)
+2. Follow page structure from §1
+3. Update sidebar: `_sidebars/[video][android].json`
+
+### Step 6: Create Branch and Commit
+
+```bash
+cd /tmp/docs-content-sync
+
+BRANCH_NAME="docs/android-sdk-pr-${PR_NUMBER}"
+git checkout -b $BRANCH_NAME
+
+git add video/android/
+git add _sidebars/ # if sidebar changed
+
+# Commit - NO AI attribution (§11)
+git commit -m "docs(android): update for SDK PR #${PR_NUMBER}
+
+[Brief description of what changed]
+
+Related to GetStream/stream-video-android#${PR_NUMBER}"
+```
+
+### Step 7: Push and Create Draft PR
+
+```bash
+cd /tmp/docs-content-sync
+
+git push -u origin $BRANCH_NAME
+
+# Create draft PR with template from §11
+gh pr create --draft \
+ --title "docs(android): update for SDK PR #${PR_NUMBER}" \
+ --body "## Summary
+
+[Brief description of doc changes]
+
+## SDK Changes
+
+Related to GetStream/stream-video-android#${PR_NUMBER}
+
+- [Change 1 from SDK PR]
+- [Change 2 from SDK PR]
+
+## Doc Updates
+
+- [File 1]: [what changed]
+- [File 2]: [what changed]
+
+---
+
+**Status:** Draft - ready for dev review"
+```
+
+### Step 8: Report Results
+
+```
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+ DOCS SYNC COMPLETE
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+SDK PR: GetStream/stream-video-android#[PR_NUMBER]
+Docs PR: GetStream/docs-content#[NEW_PR_NUMBER]
+
+Changes detected:
+- [Type]: [Description]
+
+Files updated:
+- video/android/[file1].md - [what changed]
+- video/android/[file2].md - [what changed]
+
+Quality checks:
+✓ Code examples use explicit types
+✓ No internal APIs exposed
+✓ Writing style is direct
+✓ No AI attribution
+
+Status: Draft PR created
+
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Next steps:
+1. Review the draft PR: [PR_URL]
+2. Provide feedback: /docs:revise-pr [DOCS_PR_NUMBER] "feedback"
+3. When satisfied, mark PR ready for review
+```
+
+---
+
+## Edge Cases
+
+| Situation | Action |
+|-----------|--------|
+| No doc changes needed | Report "No documentation impact detected" with reasoning |
+| New feature, no existing docs | Create new page, update sidebar, link from related pages |
+| Major restructure needed | Create GitHub issue instead, flag for manual planning |
+| Can't determine impact | List potentially affected files, ask dev to confirm scope |
+| Deprecated without replacement | Document deprecation, note removal timeline |
+| Breaking change | Add migration guide (§8), update all affected examples |
+
+---
+
+## Verification Before Finishing
+
+Run through checklist from §13:
+
+### Content
+- [ ] All code blocks compile against SDK develop
+- [ ] Explicit types used (`val x: Type = ...`)
+- [ ] No internal APIs exposed
+- [ ] Correct language tags (kotlin, groovy, xml, bash)
+
+### Writing
+- [ ] Direct writing style (no "Basically", "Simply", etc.)
+- [ ] Consistent terminology (see §3 dictionary)
+- [ ] API levels documented for version-specific features
+- [ ] Required permissions documented
+
+### Structure
+- [ ] Page follows standard structure (§1)
+- [ ] Proper heading hierarchy (##, ###)
+- [ ] Links use correct format (`/video/docs/android/...`)
+
+### PR
+- [ ] Commit message: `docs(android): [description]`
+- [ ] PR description follows template
+- [ ] SDK PR referenced
+- [ ] NO "Generated with Claude" or "Co-Authored-By: Claude"
+
+---
+
+## Handling Feedback
+
+After dev reviews the draft PR, use:
+
+```
+/docs:revise-pr [DOCS_PR_NUMBER] "feedback here"
+```
diff --git a/.claude/docs-standards.md b/.claude/docs-standards.md
new file mode 100644
index 0000000000..e7934e5cb2
--- /dev/null
+++ b/.claude/docs-standards.md
@@ -0,0 +1,658 @@
+# Documentation Standards for Stream Video Android SDK
+
+Comprehensive standards for creating and updating documentation. Follow these rules to ensure consistent, high-quality documentation.
+
+---
+
+## Configuration
+
+```yaml
+docs_repo: https://github.com/GetStream/docs-content.git
+docs_path: video/android/
+sidebar_path: _sidebars/[video][android].json
+sdk_repo: /Users/aapostol/projects/stream-video-android
+sdk_branch: develop
+clone_to: /tmp/docs-content-sync/
+```
+
+---
+
+## 1. Page Structure
+
+Every documentation page follows this structure:
+
+```markdown
+[Introduction paragraph - what this page covers, 1-3 sentences]
+
+## Section Heading
+
+[Content]
+
+### Subsection (if needed)
+
+[Content with code examples]
+
+## Next Section
+
+[Continue pattern]
+```
+
+**Rules:**
+- **No frontmatter** - pages start directly with content (no `---` YAML blocks). Titles come from sidebar JSON, not from the markdown file. Frontmatter will be rendered as visible text.
+- Introduction paragraph before first heading (no `## Introduction` heading needed)
+- Use `##` for main sections, `###` for subsections
+- Keep hierarchy shallow (rarely go beyond `###`)
+- Each page should cover ONE topic thoroughly
+
+**Section ordering pattern:**
+1. Introduction/Overview (what it is)
+2. Prerequisites (if any)
+3. Basic usage (simplest case)
+4. Configuration/Options (parameters, customization)
+5. Advanced usage (complex scenarios)
+6. Related links (if relevant)
+
+---
+
+## 2. Code Blocks
+
+### Language Tags
+
+Always specify the language:
+
+```kotlin
+// Kotlin code (most common)
+```
+
+```groovy
+// Gradle Groovy DSL
+```
+
+```kotlin
+// Gradle Kotlin DSL (build.gradle.kts)
+```
+
+```xml
+// XML (layouts, strings, manifest)
+```
+
+```bash
+# Shell commands
+```
+
+### Full Samples vs Fragments
+
+**Full sample** - Include imports, show complete context:
+```kotlin
+import io.getstream.video.android.core.StreamVideoBuilder
+import io.getstream.video.android.core.GEO
+import io.getstream.video.android.model.User
+
+val client: StreamVideo = StreamVideoBuilder(
+ context = applicationContext,
+ apiKey = "your-api-key",
+ geo = GEO.GlobalEdgeNetwork,
+ user = User(id = "user-id"),
+ token = "user-token",
+).build()
+```
+
+**Fragment** - Omit imports, focus on the specific API:
+```kotlin
+// Enable background blur
+call.videoFilter = BlurredBackgroundVideoFilter()
+```
+
+**When to use which:**
+| Scenario | Use |
+|----------|-----|
+| First example on a page | Full sample |
+| Quickstart / Getting started | Full sample |
+| Showing a specific API call | Fragment |
+| Showing customization options | Fragment |
+| Complete working example | Full sample |
+
+### Code Verification Rules
+
+| Rule | Description |
+|------|-------------|
+| Must compile | All code blocks must compile against SDK develop branch |
+| Explicit types | Use `val client: StreamVideo = ...` not `val client = ...` |
+| Real values | Use realistic placeholder values, not `"xxx"` or `"???"` |
+| No internal APIs | Never use APIs not in `.api` files |
+
+**Checking if an API is internal:**
+```bash
+# If a class/function is NOT in the .api file, it's internal
+cat stream-video-android-*/api/*.api | grep "ClassName"
+```
+
+**Internal API alternatives:**
+| Don't use | Use instead |
+|-----------|-------------|
+| `{ DefaultOnlineIndicator(...) }` | `{ /* default indicator */ }` |
+| `{ LivestreamBackStage(call) }` | `{ /* default backstage UI */ }` |
+| `DefaultModerationVideoFilter()` | Omit or use public alternative |
+| Internal composables | Comment placeholder or public alternative |
+
+---
+
+## 3. Writing Style
+
+### Voice and Tone
+
+| Rule | Good | Bad |
+|------|------|-----|
+| Direct language | "The SDK provides..." | "In terms of the SDK..." |
+| Active voice | "Call `startScreenSharing()` to begin" | "Screen sharing can be started by calling..." |
+| User-centric | "You can customize..." | "The system allows..." |
+| Imperative for instructions | "Add the dependency" | "You should add the dependency" |
+
+### Banned Phrases
+
+Remove these filler words and phrases:
+
+- "Basically"
+- "Simply" / "Just"
+- "Obviously"
+- "Actually"
+- "In order to" → use "to"
+- "In terms of"
+- "It should be noted that"
+- "As you can see"
+- "Please note that" → use direct statement or Note callout
+
+### Common Corrections
+
+| Before | After |
+|--------|-------|
+| "the most easiest way" | "the easiest way" |
+| "you can available the" | "you can use the" |
+| "support to show" | "can display" |
+| "by your taste" | "as needed" |
+| "custom the video" | "customize the video" |
+| "allows to" | "allows you to" or rephrase |
+| "In case you want" | "To" or "If you want" |
+
+### Terminology Dictionary
+
+Use consistent terminology throughout:
+
+| Use | Don't use |
+|-----|-----------|
+| customize | custom (as verb) |
+| call | video call (unless distinguishing from audio) |
+| participant | user (in call context) |
+| local participant | current user / self |
+| remote participant | other user |
+| SDK | sdk / Sdk |
+| API | api / Api |
+| WebRTC | webrtc / WebRtc |
+| WebSocket | websocket / Websocket |
+| Jetpack Compose | jetpack compose |
+| Android | android (in prose) |
+
+---
+
+## 4. Notes, Warnings, and Callouts
+
+### Inline Notes
+
+For brief notes within flow:
+```markdown
+> **Note:** Screen audio sharing requires the microphone to be unmuted.
+```
+
+### Admonition Blocks
+
+For important callouts that need emphasis:
+
+```markdown
+
+This is informational content the reader should know.
+
+
+
+**Important:** This warns about potential issues or gotchas.
+
+
+
+This is a serious warning about destructive or dangerous operations.
+
+```
+
+**Do NOT use `:::note` syntax** - Android docs don't support it. The `:::` syntax will be rendered as literal text. Always use `` tags.
+
+**When to use which:**
+| Type | Use for |
+|------|---------|
+| `note` | Helpful information, tips, clarifications |
+| `caution` | Potential issues, common mistakes, gotchas |
+| `warning` | Data loss, security issues, breaking changes |
+
+---
+
+## 5. Android-Specific Conventions
+
+### API Level Notes
+
+Always specify minimum API level for features:
+```markdown
+Screen audio sharing is available on Android 10 (API level 29) and above.
+```
+
+Format: "Android X (API level Y)"
+
+### Permissions
+
+Document required permissions clearly:
+```markdown
+For Android 13+ (API level 33+), you need to request the `POST_NOTIFICATIONS` permission:
+
+```kotlin
+if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQUEST_CODE)
+}
+```
+```
+
+### Dependencies
+
+Show both Groovy and Kotlin DSL when relevant:
+```markdown
+Add the dependency to your app-level `build.gradle.kts`:
+
+```kotlin
+dependencies {
+ implementation("io.getstream:stream-video-android-ui-compose:$version")
+}
+```
+```
+
+Use `$version` or `x.x.x` as placeholder, link to version source.
+
+---
+
+## 6. Links and Navigation
+
+### Internal Links
+
+Use absolute paths from docs root:
+```markdown
+[CallContent](/video/docs/android/ui-components/call/call-content/)
+[Camera & Microphone guide](/video/docs/android/guides/camera-and-microphone/)
+```
+
+**Pattern:** `/video/docs/android/[section]/[page]/`
+
+### External Links
+
+```markdown
+[Android Media Projection API](https://developer.android.com/guide/topics/large-screens/media-projection)
+```
+
+### Cross-References
+
+When referencing related content:
+```markdown
+For more details, see the [Video Theme guide](/video/docs/android/ui-components/video-theme/).
+```
+
+---
+
+## 7. Sidebar and Navigation
+
+### Sidebar Structure
+
+The sidebar is defined in `_sidebars/[video][android].json`:
+
+```json
+{
+ "baseURL": "/video/docs/android",
+ "items": [
+ {
+ "label": "section-name",
+ "children": [
+ {
+ "title": "Page Title",
+ "slug": "/section/page-name/",
+ "markdown": "video/android/XX-section/YY-page-name.md"
+ }
+ ]
+ }
+ ]
+}
+```
+
+### File Naming
+
+Files use numbered prefixes for ordering:
+```
+01-basics/
+ 01-introduction.md
+ 02-installation.md
+ 03-quickstart.md
+03-guides/
+ 01-client-auth.md
+ 02-joining-creating-calls.md
+```
+
+### When to Create New Pages vs Update Existing
+
+| Scenario | Action |
+|----------|--------|
+| New major feature | Create new page |
+| New parameter to existing feature | Update existing page |
+| New optional capability | Update existing + consider new page if complex |
+| Bug fix / behavior change | Update existing page |
+| Deprecation | Update existing + add deprecation notice |
+
+**Creating new pages:**
+1. Create `.md` file with correct number prefix
+2. Add entry to sidebar JSON
+3. Follow existing section's page structure
+
+---
+
+## 8. Deprecations and Breaking Changes
+
+### Deprecation Notice Format
+
+```markdown
+
+
+**Deprecated:** `oldMethod()` is deprecated and will be removed in version X.Y.Z. Use `newMethod()` instead.
+
+
+```
+
+### Migration Guide Format
+
+For breaking changes, provide clear migration:
+
+```markdown
+## Migrating from X to Y
+
+### Before (deprecated)
+
+```kotlin
+// Old way - deprecated
+call.oldMethod(param)
+```
+
+### After
+
+```kotlin
+// New way
+call.newMethod(newParam)
+```
+
+### Changes
+- `oldMethod` → `newMethod`
+- Parameter `param` renamed to `newParam`
+- Return type changed from `Unit` to `Result`
+```
+
+### Removal Documentation
+
+When API is removed:
+1. Remove code examples using it
+2. Remove or update any references
+3. Update related pages that linked to it
+4. Do NOT leave "removed in version X" notices indefinitely
+
+---
+
+## 9. SDK to Docs Mapping
+
+### Detecting Changes
+
+| Change Type | Detection Method | Doc Impact |
+|-------------|------------------|------------|
+| New public class | Added to `.api` file | New section or page |
+| Changed signature | Modified in `.api` file | Update code examples |
+| New parameter | Function signature change | Update examples + parameters table |
+| Deprecated API | `@Deprecated` annotation | Add deprecation notice |
+| Removed API | Removed from `.api` file | Remove from docs |
+| Behavior change | PR description/commits | Update descriptions |
+
+### API Files to Check
+
+```bash
+stream-video-android-core/api/*.api
+stream-video-android-ui-core/api/*.api
+stream-video-android-ui-compose/api/*.api
+stream-video-android-filters-video/api/*.api
+```
+
+### Common Mappings
+
+| SDK Area | Doc Location |
+|----------|--------------|
+| `StreamVideoBuilder` | `01-basics/`, `03-guides/01-client-auth.md` |
+| `StreamVideo` interface | `03-guides/01-client-auth.md` |
+| `Call` class | `03-guides/02-joining-creating-calls.md` |
+| `call.state.*` | `03-guides/03-call-and-participant-state.md` |
+| `call.camera`, `call.microphone` | `03-guides/04-camera-and-microphone.md` |
+| `call.screenShare` | `06-advanced/04-screen-sharing.md` |
+| `CallContent` | `04-ui-components/04-call/01-call-content.md` |
+| `ParticipantVideo` | `04-ui-components/05-participants/01-participant-video.md` |
+| `VideoTheme` | `04-ui-components/03-video-theme.md` |
+| Video filters | `06-advanced/05-apply-video-filters.md` |
+| Audio filters | `03-guides/05-noise-cancellation.md` |
+| Push notifications | `06-advanced/00-incoming-calls/03-push-notifications.md` |
+| Ringing calls | `06-advanced/00-incoming-calls/02-ringing.md` |
+| Recording | `06-advanced/09-recording.md` |
+| Livestreaming | `03-guides/12-livestreaming.md` |
+| Picture-in-Picture | `06-advanced/03-enable-picture-in-picture.md` |
+| Telecom integration | `06-advanced/12-telecom.md` |
+
+### Search Strategy
+
+When mapping SDK changes to docs:
+
+1. **Grep for class/function names:**
+ ```bash
+ grep -r "ClassName\|functionName" video/android/ --include="*.md" -l
+ ```
+
+2. **Check the mapping table above**
+
+3. **Check sidebar for section structure**
+
+4. **Look at related pages** - changes often affect multiple files
+
+---
+
+## 10. Cross-Platform Awareness
+
+### Reference Other Platforms
+
+```
+React docs: video/react/
+iOS docs: video/ios/
+Flutter docs: video/flutter/
+```
+
+### When to Check Other Platforms
+
+| Scenario | Action |
+|----------|--------|
+| Writing new feature docs | Check if React/iOS has it, adapt good explanations |
+| Unclear how to explain concept | See how other platforms document it |
+| Android-specific behavior | Note the difference explicitly |
+| API differs from other platforms | Document the Android way clearly |
+
+### Documenting Platform Differences
+
+```markdown
+> **Note:** On Android, screen sharing requires explicit user permission via the Media Projection API. This differs from web browsers where permission is handled differently.
+```
+
+---
+
+## 11. PR and Commit Standards
+
+### Commit Messages
+
+```
+docs(android): [description]
+
+[Optional body with more details]
+```
+
+**Examples:**
+- `docs(android): add connectOnInit parameter to client auth guide`
+- `docs(android): update screen sharing with audio capture feature`
+- `docs(android): fix code example in video filters page`
+
+### PR Description Template
+
+```markdown
+## Summary
+
+[Brief description of doc changes]
+
+## SDK Changes
+
+Related to GetStream/stream-video-android#[PR_NUMBER]
+
+- [Change 1 from SDK PR]
+- [Change 2 from SDK PR]
+
+## Doc Updates
+
+- [File 1]: [what changed]
+- [File 2]: [what changed]
+```
+
+### What NOT to Include
+
+| Don't include | Why |
+|---------------|-----|
+| "Generated with Claude" | No AI attribution |
+| "Co-Authored-By: Claude" | No AI co-author |
+| Test checklists | Keep PR focused on changes |
+| Emoji in commits | Keep professional |
+
+---
+
+## 12. Examples: Good vs Bad
+
+### Example 1: Introduction Paragraph
+
+**Bad:**
+```markdown
+## Introduction
+
+In this document, we will basically be looking at how you can simply implement screen sharing in your Android application. As you probably know, screen sharing is a feature that allows users to share their screen.
+```
+
+**Good:**
+```markdown
+The Stream Video Android SDK supports screen sharing using the Android Media Projection API. Users with the `screenshare` capability can share their device screen with other call participants.
+```
+
+### Example 2: Code with Types
+
+**Bad:**
+```kotlin
+val client = StreamVideoBuilder(
+ context = context,
+ apiKey = apiKey,
+).build()
+```
+
+**Good:**
+```kotlin
+val client: StreamVideo = StreamVideoBuilder(
+ context = applicationContext,
+ apiKey = "mmhfdzb5evj2",
+ geo = GEO.GlobalEdgeNetwork,
+ user = user,
+ token = token,
+).build()
+```
+
+### Example 3: Parameter Documentation
+
+**Bad:**
+```markdown
+The function takes some parameters that you can use.
+```
+
+**Good:**
+```markdown
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `blurIntensity` | Intensity of the blur effect: `LIGHT`, `MEDIUM`, or `HEAVY` | `MEDIUM` |
+| `foregroundThreshold` | Confidence threshold for foreground detection (0.0 to 1.0) | `0.99999` |
+```
+
+### Example 4: API Level Documentation
+
+**Bad:**
+```markdown
+This feature requires a newer Android version.
+```
+
+**Good:**
+```markdown
+Screen audio sharing is available on Android 10 (API level 29) and above. On older devices, only screen video is captured.
+```
+
+### Example 5: Note vs Inline
+
+**Bad (overusing notes):**
+```markdown
+> **Note:** Call `startScreenSharing()` to begin.
+
+> **Note:** Pass the intent data from the permission result.
+
+> **Note:** The user must have the screenshare capability.
+```
+
+**Good (notes for important info only):**
+```markdown
+Call `startScreenSharing()` with the intent data from the Media Projection permission result. The user must have the `screenshare` capability.
+
+> **Note:** Screen audio sharing requires the microphone to be unmuted. When the microphone is muted, both microphone and screen audio are silenced.
+```
+
+---
+
+## 13. Verification Checklist
+
+Before creating or updating a docs PR:
+
+### Content
+- [ ] All code blocks compile against SDK develop
+- [ ] Explicit types used in code examples
+- [ ] No internal APIs exposed
+- [ ] Correct language tags on code blocks
+
+### Writing
+- [ ] Direct writing style (no filler phrases)
+- [ ] Consistent terminology
+- [ ] API levels documented for version-specific features
+- [ ] Required permissions documented
+
+### Structure
+- [ ] Page follows standard structure
+- [ ] Proper heading hierarchy
+- [ ] Links use correct format
+
+### PR
+- [ ] Commit message follows format
+- [ ] PR description follows template
+- [ ] SDK PR referenced
+- [ ] No AI attribution anywhere
+
+### Navigation (if new page)
+- [ ] File has correct number prefix
+- [ ] Sidebar JSON updated
+- [ ] Related pages link to new page
+
+---
+
+*Comprehensive standards derived from Android Video Documentation Audit (v1.0, v1.1) and cross-platform analysis.*
diff --git a/.gitignore b/.gitignore
index b21be5dca2..9fde93456d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -124,3 +124,6 @@ demo-app/src/production/play/
video-buddy-server.log
video-buddy-console.log
video-buddy-session.json
+
+# GSD workflow files
+.planning/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000..59367cde0e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,60 @@
+# Claude Instructions for Stream Video Android SDK
+
+## Documentation Commands
+
+When SDK changes require documentation updates:
+
+| Command | Purpose |
+|---------|---------|
+| `/docs:sync-sdk-pr [PR#]` | Create docs PR for SDK changes |
+| `/docs:revise-pr [PR#]` | Apply feedback to docs PR |
+
+### Workflow
+
+1. Dev creates SDK PR
+2. Dev runs `/docs:sync-sdk-pr 1234`
+3. Claude analyzes changes, creates draft docs PR
+4. Dev reviews, provides feedback
+5. Dev runs `/docs:revise-pr 567 "feedback"` as needed
+6. Dev marks ready for review
+
+### Standards
+
+All documentation work must follow:
+- `.claude/docs-standards.md` — Quality rules for docs
+
+Key rules:
+- Code must compile against develop branch
+- No internal APIs (check `.api` files)
+- Direct writing style, no filler phrases
+- No AI attribution in commits/PRs
+
+## Project Planning
+
+GSD workflow files are in `.planning/`:
+- `PROJECT.md` — Project context
+- `STANDARDS.md` — Audit standards
+- `ROADMAP.md` — Phase structure
+- `STATE.md` — Current position
+
+## Repository Structure
+
+```
+stream-video-android/ # SDK source
+├── stream-video-android-core/
+├── stream-video-android-ui-compose/
+├── stream-video-android-ui-core/
+├── stream-video-android-filters-video/
+└── .claude/
+ ├── docs-standards.md # Doc quality rules
+ └── commands/
+ ├── docs-sync-sdk-pr.md
+ └── docs-revise-pr.md
+```
+
+## Related Repos
+
+| Repo | Purpose | Path |
+|------|---------|------|
+| docs-content | Documentation source | Clone fresh to /tmp for edits |
+| docs | Parent repo (don't edit) | /Users/aapostol/projects/docs |
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
index 4b0ed353ae..4101d84203 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
@@ -87,8 +87,6 @@ import io.getstream.video.android.core.model.VideoTrack
import io.getstream.video.android.core.model.toIceServer
import io.getstream.video.android.core.notifications.internal.telecom.TelecomCallController
import io.getstream.video.android.core.recording.RecordingType
-import io.getstream.video.android.core.socket.common.scope.ClientScope
-import io.getstream.video.android.core.socket.common.scope.UserScope
import io.getstream.video.android.core.utils.AtomicUnitCall
import io.getstream.video.android.core.utils.RampValueUpAndDownHelper
import io.getstream.video.android.core.utils.StreamSingleFlightProcessorImpl
@@ -110,6 +108,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withTimeoutOrNull
import org.threeten.bp.OffsetDateTime
import org.webrtc.EglBase
import org.webrtc.PeerConnection
@@ -154,17 +153,18 @@ public class Call(
internal var reconnectAttepmts = 0
internal val clientImpl = client as StreamVideoClient
- internal val scopeProvider: ScopeProvider = ScopeProviderImpl(clientImpl.scope)
+ internal var scopeProvider: ScopeProvider = ScopeProviderImpl(clientImpl.scope)
// Atomic controls
private var atomicLeave = AtomicUnitCall()
private val logger by taggedLogger("Call:$type:$id")
- private val supervisorJob = SupervisorJob()
+ private var supervisorJob = SupervisorJob()
private var callStatsReportingJob: Job? = null
+ private var cleanupJob: Job? = null
private var powerManager: PowerManager? = null
- internal val scope = CoroutineScope(clientImpl.scope.coroutineContext + supervisorJob)
+ internal var scope = CoroutineScope(clientImpl.scope.coroutineContext + supervisorJob)
/** The call state contains all state such as the participant list, reactions etc */
val state = CallState(client, this, user, scope)
@@ -226,15 +226,22 @@ public class Call(
*/
private var sfuSocketReconnectionTime: Long? = null
+ /**
+ * Lock for synchronizing join/leave lifecycle operations.
+ * Protects access to isDestroyed, cleanupJob, and scope re-initialization.
+ */
+ private val lifecycleLock = Any()
+
/**
* Call has been left and the object is cleaned up and destroyed.
+ * Must be accessed under [lifecycleLock].
*/
private var isDestroyed = false
/** Session handles all real time communication for video and audio */
internal var session: RtcSession? = null
var sessionId = UUID.randomUUID().toString()
- internal val unifiedSessionId = UUID.randomUUID().toString()
+ internal var unifiedSessionId = UUID.randomUUID().toString()
internal var connectStartTime = 0L
internal var reconnectStartTime = 0L
@@ -343,11 +350,10 @@ public class Call(
testInstanceProvider.mediaManagerCreator!!.invoke()
} else {
MediaManagerImpl(
- clientImpl.context,
- this,
- scope,
- eglBase.eglBaseContext,
- clientImpl.callServiceConfigRegistry.get(type).audioUsage,
+ context = clientImpl.context,
+ call = this,
+ eglBaseContext = eglBase.eglBaseContext,
+ audioUsage = clientImpl.callServiceConfigRegistry.get(type).audioUsage,
) { clientImpl.callServiceConfigRegistry.get(type).audioUsage }
}
}
@@ -532,10 +538,13 @@ public class Call(
var result: Result
- atomicLeave = AtomicUnitCall()
while (retryCount < 3) {
result = _join(create, createOptions, ring, notify)
if (result is Success) {
+ // Reset atomicLeave AFTER successful join (after cleanup is complete)
+ // This prevents race conditions where leave() is called during the join process
+ atomicLeave = AtomicUnitCall()
+
// we initialise the camera, mic and other according to local + backend settings
// only when the call is joined to make sure we don't switch and override
// the settings during a call.
@@ -604,6 +613,26 @@ public class Call(
ring: Boolean = false,
notify: Boolean = false,
): Result {
+ // Wait for any pending cleanup to complete before rejoining.
+ // Use a loop to handle the edge case where resetScopes() clears cleanupJob
+ // but a new leave() sets it again before we can proceed.
+ // Timeout after ~10 seconds to prevent hanging forever if cleanup is stuck.
+ var cleanupWaitAttempts = 0
+ while (true) {
+ val pendingCleanup = synchronized(lifecycleLock) { cleanupJob }
+ if (pendingCleanup == null) break
+
+ if (cleanupWaitAttempts++ >= 10) {
+ logger.e { "[_join] Cleanup taking too long after ${cleanupWaitAttempts}s, failing join" }
+ return Failure(Error.GenericError("Join failed: cleanup timed out"))
+ }
+
+ logger.d {
+ "[_join] Waiting for cleanup to complete before rejoining (attempt $cleanupWaitAttempts)"
+ }
+ withTimeoutOrNull(1000) { pendingCleanup.join() }
+ }
+
reconnectAttepmts = 0
sfuEvents?.cancel()
sfuListener?.cancel()
@@ -947,11 +976,38 @@ public class Call(
sfuEvents?.cancel()
state._connection.value = RealtimeConnection.Disconnected
logger.v { "[leave] #ringing; disconnectionReason: $disconnectionReason, call_id = $id" }
- if (isDestroyed) {
- logger.w { "[leave] #ringing; Call already destroyed, ignoring" }
- return@atomicLeave
+
+ // Synchronize access to isDestroyed, cleanupJob to prevent race conditions with join()
+ // IMPORTANT: cleanupJob must be set inside this synchronized block to eliminate the race
+ // window where join() could read cleanupJob as null before it's assigned.
+ synchronized(lifecycleLock) {
+ if (isDestroyed) {
+ logger.w { "[leave] #ringing; Call already destroyed, ignoring" }
+ return@atomicLeave
+ }
+ isDestroyed = true
+
+ // Guard against overwriting cleanupJob if cleanup is already in progress
+ if (cleanupJob?.isActive == true) {
+ logger.w { "[leave] Cleanup already in progress, skipping duplicate cleanup" }
+ return@atomicLeave
+ }
+
+ // Set cleanupJob INSIDE this synchronized block so join() will always see it
+ // and wait for cleanup to complete before proceeding
+ cleanupJob = clientImpl.scope.launch {
+ safeCall {
+ session?.sfuTracer?.trace(
+ "leave-call",
+ "[reason=$reason, error=${disconnectionReason?.message}]",
+ )
+ val stats = collectStats()
+ session?.sendCallStats(stats)
+ }
+ cleanup()
+ resetScopes()
+ }
}
- isDestroyed = true
sfuSocketReconnectionTime = null
@@ -974,18 +1030,6 @@ public class Call(
.leaveCall(this)
(client as StreamVideoClient).onCallCleanUp(this)
-
- clientImpl.scope.launch {
- safeCall {
- session?.sfuTracer?.trace(
- "leave-call",
- "[reason=$reason, error=${disconnectionReason?.message}]",
- )
- val stats = collectStats()
- session?.sendCallStats(stats)
- }
- cleanup()
- }
}
/** ends the call for yourself as well as other users */
@@ -1512,7 +1556,6 @@ public class Call(
fun cleanup() {
// monitor.stop()
session?.cleanup()
- shutDownJobsGracefully()
callStatsReportingJob?.cancel()
mediaManager.cleanup() // TODO Rahul, Verify Later: need to check which call has owned the media at the moment(probably use active call)
session = null
@@ -1520,13 +1563,43 @@ public class Call(
scopeProvider.cleanup()
}
- // This will allow the Rest APIs to be executed which are in queue before leave
- private fun shutDownJobsGracefully() {
- UserScope(ClientScope()).launch {
- supervisorJob.children.forEach { it.join() }
- supervisorJob.cancel()
+ /**
+ * Resets state to allow the Call to be reusable after leave().
+ * Generates new session IDs, resets the scopeProvider, clears participants, and resets device statuses.
+ *
+ * IMPORTANT: We do NOT recreate [scope] or [supervisorJob] because [CallState] and its
+ * StateFlows depend on the original scope. The scope lives for the entire lifetime of
+ * the Call object.
+ */
+ private fun resetScopes() {
+ logger.d { "[resetScopes] Resetting state to make Call reusable" }
+
+ // Reset the destroyed flag and clear cleanupJob under lock to prevent race conditions
+ synchronized(lifecycleLock) {
+ isDestroyed = false
+ cleanupJob = null
}
- scope.cancel()
+
+ // Generate new session IDs for fresh connection
+ sessionId = UUID.randomUUID().toString()
+ unifiedSessionId = UUID.randomUUID().toString()
+ logger.d { "[resetScopes] New sessionId: $sessionId, unifiedSessionId: $unifiedSessionId" }
+
+ // Clear participants to remove stale video/audio tracks from previous session
+ state.clearParticipants()
+
+ // Reset device statuses to NotSelected so they get re-initialized on next join
+ mediaManager.reset()
+
+ // Reset the scope provider to allow reuse
+ scopeProvider.reset()
+
+ // NOTE: We intentionally do NOT recreate supervisorJob or scope here.
+ // CallState's StateFlows (duration, participants, etc.) use stateIn(scope, ...)
+ // which captures the scope at initialization. If we recreated scope, those
+ // StateFlows would become dead and never emit again.
+
+ logger.d { "[resetScopes] State reset successfully" }
}
suspend fun ring(): Result {
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
index 84ce23a431..eeeb41f2d8 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/MediaManager.kt
@@ -284,7 +284,7 @@ class ScreenShareManager(
private val logger by taggedLogger("Media:ScreenShareManager")
- private val _status = MutableStateFlow(DeviceStatus.NotSelected)
+ internal val _status = MutableStateFlow(DeviceStatus.NotSelected)
val status: StateFlow = _status
public val isEnabled: StateFlow = _status.mapState { it is DeviceStatus.Enabled }
@@ -577,7 +577,7 @@ class MicrophoneManager(
val selectedUsbDevice: StateFlow = _selectedUsbDevice
// Exposed state
- private val _status = MutableStateFlow(DeviceStatus.NotSelected)
+ internal val _status = MutableStateFlow(DeviceStatus.NotSelected)
/** The status of the audio */
val status: StateFlow = _status
@@ -885,6 +885,13 @@ class MicrophoneManager(
setupCompleted = false
}
+ /**
+ * Resets the microphone status to NotSelected to allow re-initialization on next join.
+ */
+ internal fun reset() {
+ _status.value = DeviceStatus.NotSelected
+ }
+
fun canHandleDeviceSwitch() = audioUsageProvider.invoke() != AudioAttributes.USAGE_MEDIA
// Internal logic
@@ -907,40 +914,47 @@ class MicrophoneManager(
setupUsbDeviceDetection()
}
- if (canHandleDeviceSwitch() && !::audioHandler.isInitialized) {
- audioHandler = AudioSwitchHandler(
- context = mediaManager.context,
- preferredDeviceList = listOf(
- AudioDevice.BluetoothHeadset::class.java,
- AudioDevice.WiredHeadset::class.java,
- ) + if (preferSpeaker) {
- listOf(
- AudioDevice.Speakerphone::class.java,
- AudioDevice.Earpiece::class.java,
- )
- } else {
- listOf(
- AudioDevice.Earpiece::class.java,
- AudioDevice.Speakerphone::class.java,
- )
- },
- audioDeviceChangeListener = { devices, selected ->
- logger.i { "[audioSwitch] audio devices. selected $selected, available devices are $devices" }
-
- _devices.value = devices.map { it.fromAudio() }
- _selectedDevice.value = selected?.fromAudio()
-
- setupCompleted = true
-
- capturedOnAudioDevicesUpdate?.invoke()
- capturedOnAudioDevicesUpdate = null
- },
- )
+ if (canHandleDeviceSwitch()) {
+ if (!::audioHandler.isInitialized) {
+ // First time initialization
+ audioHandler = AudioSwitchHandler(
+ context = mediaManager.context,
+ preferredDeviceList = listOf(
+ AudioDevice.BluetoothHeadset::class.java,
+ AudioDevice.WiredHeadset::class.java,
+ ) + if (preferSpeaker) {
+ listOf(
+ AudioDevice.Speakerphone::class.java,
+ AudioDevice.Earpiece::class.java,
+ )
+ } else {
+ listOf(
+ AudioDevice.Earpiece::class.java,
+ AudioDevice.Speakerphone::class.java,
+ )
+ },
+ audioDeviceChangeListener = { devices, selected ->
+ logger.i { "[audioSwitch] audio devices. selected $selected, available devices are $devices" }
+
+ _devices.value = devices.map { it.fromAudio() }
+ _selectedDevice.value = selected?.fromAudio()
+
+ setupCompleted = true
+
+ capturedOnAudioDevicesUpdate?.invoke()
+ capturedOnAudioDevicesUpdate = null
+ },
+ )
- logger.d { "[setup] Calling start on instance $audioHandler" }
- audioHandler.start()
+ logger.d { "[setup] Calling start on instance $audioHandler" }
+ audioHandler.start()
+ } else {
+ // audioHandler exists but was stopped (cleanup was called), restart it
+ logger.d { "[setup] Restarting audioHandler after cleanup" }
+ audioHandler.start()
+ }
} else {
- logger.d { "[MediaManager#setup] Usage is MEDIA or audioHandle is already initialized" }
+ logger.d { "[MediaManager#setup] Usage is MEDIA" }
capturedOnAudioDevicesUpdate?.invoke()
}
}
@@ -998,7 +1012,7 @@ class CameraManager(
private val logger by taggedLogger("Media:CameraManager")
/** The status of the camera. enabled or disabled */
- private val _status = MutableStateFlow(DeviceStatus.NotSelected)
+ internal val _status = MutableStateFlow(DeviceStatus.NotSelected)
public val status: StateFlow = _status
/** Represents whether the camera is enabled */
@@ -1343,6 +1357,13 @@ class CameraManager(
setupCompleted = false
}
+ /**
+ * Resets the camera status to NotSelected to allow re-initialization on next join.
+ */
+ internal fun reset() {
+ _status.value = DeviceStatus.NotSelected
+ }
+
private fun createCameraDeviceWrapper(
id: String,
cameraManager: CameraManager?,
@@ -1415,15 +1436,21 @@ class CameraManager(
* @see AudioSwitch
* @see BluetoothHeadsetManager
*/
+@Suppress("UNUSED_PARAMETER")
class MediaManagerImpl(
val context: Context,
val call: Call,
- val scope: CoroutineScope,
+ // Deprecated: This parameter is no longer used. Scope is now obtained dynamically from call.scope
+ scope: CoroutineScope? = null,
val eglBaseContext: EglBase.Context,
@Deprecated("Use audioUsageProvider instead", replaceWith = ReplaceWith("audioUsageProvider"))
val audioUsage: Int = defaultAudioUsage,
val audioUsageProvider: (() -> Int) = { audioUsage },
) {
+ // Use call.scope dynamically to support scope recreation after leave()
+ val scope: CoroutineScope
+ get() = call.scope
+
internal val camera =
CameraManager(this, eglBaseContext, DefaultCameraCharacteristicsValidator())
internal val microphone = MicrophoneManager(this, audioUsage, audioUsageProvider)
@@ -1546,6 +1573,17 @@ class MediaManagerImpl(
camera.cleanup()
microphone.cleanup()
}
+
+ /**
+ * Resets device statuses to NotSelected to allow re-initialization on next join.
+ * Should be called after cleanup when preparing for rejoin.
+ */
+ internal fun reset() {
+ camera._status.value = DeviceStatus.NotSelected
+ microphone._status.value = DeviceStatus.NotSelected
+ speaker._status.value = DeviceStatus.NotSelected
+ screenShare._status.value = DeviceStatus.NotSelected
+ }
}
fun MediaStreamTrack.trySetEnabled(enabled: Boolean) = safeCall { setEnabled(enabled) }
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
index bc85d2ac77..f0940758b1 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
@@ -90,7 +90,7 @@ import java.net.ConnectException
* @property audioProcessing The audio processor used for custom modifications to audio data within WebRTC.
* @property callServiceConfigRegistry The audio processor used for custom modifications to audio data within WebRTC.
* @property leaveAfterDisconnectSeconds The number of seconds to wait before leaving the call after the connection is disconnected.
- * @property callUpdatesAfterLeave Whether to update the call state after leaving the call.
+ * @property callUpdatesAfterLeave [Deprecated] This parameter is no longer needed. Call updates are now always enabled after leave().
* @property connectOnInit Determines whether the socket should automatically connect as soon as a user is set.
* If `false`, the connection is established only when explicitly requested or when core SDK features
* (such as audio or video calls) are used.
@@ -147,6 +147,10 @@ public class StreamVideoBuilder @JvmOverloads constructor(
private val appName: String? = null,
private val audioProcessing: ManagedAudioProcessingFactory? = null,
private val leaveAfterDisconnectSeconds: Long = 30,
+ @Deprecated(
+ message = "This parameter is no longer needed. Call updates are now always enabled after leave() to support call reusability.",
+ level = DeprecationLevel.WARNING,
+ )
private val callUpdatesAfterLeave: Boolean = false,
private val enableStatsReporting: Boolean = true,
@InternalStreamVideoApi
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
index a11aeabeaf..1eb7d9adee 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.kt
@@ -18,7 +18,6 @@ package io.getstream.video.android.core
import android.content.Context
import android.media.AudioAttributes
-import androidx.collection.LruCache
import androidx.lifecycle.Lifecycle
import io.getstream.android.push.PushDevice
import io.getstream.android.video.generated.models.AcceptCallResponse
@@ -177,6 +176,10 @@ internal class StreamVideoClient internal constructor(
internal val audioProcessing: ManagedAudioProcessingFactory? = null,
internal val leaveAfterDisconnectSeconds: Long = 30,
internal val appVersion: String? = null,
+ @Deprecated(
+ message = "This parameter is no longer needed. Call updates are now always enabled after leave() to support call reusability.",
+ level = DeprecationLevel.WARNING,
+ )
internal val enableCallUpdatesAfterLeave: Boolean = false,
internal val enableStatsCollection: Boolean = true,
internal val enableStereoForSubscriber: Boolean = true,
@@ -203,24 +206,18 @@ internal class StreamVideoClient internal constructor(
private val logger by taggedLogger("Call:StreamVideo")
private var subscriptions = mutableSetOf()
private var calls = mutableMapOf()
- private val destroyedCalls = LruCache(maxSize = 100)
internal val callSoundAndVibrationPlayer = CallSoundAndVibrationPlayer(context)
val socketImpl = coordinatorConnectionModule.socketConnection
fun onCallCleanUp(call: Call) {
- if (enableCallUpdatesAfterLeave) {
- logger.d { "[cleanup] Call updates are required, preserve the instance: ${call.cid}" }
- destroyedCalls.put(call.hashCode(), call)
- }
- logger.d { "[cleanup] Removing call from cache: ${call.cid}" }
- calls.remove(call.cid)
+ logger.d { "[cleanup] Call cleaned up but kept in cache for reuse: ${call.cid}" }
+ // Call remains in the 'calls' map to allow rejoin and continue receiving updates
}
override fun cleanup() {
// remove all cached calls
calls.clear()
- destroyedCalls.evictAll()
// stop all running coroutines
scope.cancel()
// call cleanup on the active call
@@ -561,7 +558,7 @@ internal class StreamVideoClient internal constructor(
// call level subscriptions
if (selectedCid.isNotEmpty()) {
calls[selectedCid]?.fireEvent(event)
- notifyDestroyedCalls(event)
+ // No need to notify destroyed calls - calls remain in map after leave()
}
if (selectedCid.isNotEmpty()) {
@@ -585,37 +582,7 @@ internal class StreamVideoClient internal constructor(
it.session?.handleEvent(event)
it.handleEvent(event)
}
- deliverIntentToDestroyedCalls(event)
- }
- }
-
- private fun shouldProcessDestroyedCall(event: VideoEvent, callCid: String): Boolean {
- return when (event) {
- is WSCallEvent -> event.getCallCID() == callCid
- else -> true
- }
- }
-
- private fun deliverIntentToDestroyedCalls(event: VideoEvent) {
- safeCall {
- destroyedCalls.snapshot().forEach { (_, call) ->
- call.let {
- if (shouldProcessDestroyedCall(event, call.cid)) {
- it.state.handleEvent(event)
- it.handleEvent(event)
- }
- }
- }
- }
- }
-
- private fun notifyDestroyedCalls(event: VideoEvent) {
- safeCall {
- destroyedCalls.snapshot().forEach { (_, call) ->
- if (shouldProcessDestroyedCall(event, call.cid)) {
- call.fireEvent(event)
- }
- }
+ // No need to deliver to destroyed calls - calls remain in map after leave()
}
}
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
index 7c4604d0ac..e24f946c8d 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/audio/AudioHandler.kt
@@ -76,11 +76,15 @@ public class AudioSwitchHandler(
}
override fun stop() {
- logger.d { "[stop] no args" }
- mainThreadHandler.removeCallbacksAndMessages(null)
- mainThreadHandler.post {
- audioSwitch?.stop()
- audioSwitch = null
+ synchronized(this) {
+ logger.d { "[stop] no args" }
+ mainThreadHandler.removeCallbacksAndMessages(null)
+ mainThreadHandler.post {
+ audioSwitch?.stop()
+ audioSwitch = null
+ }
+ // Reset flag to allow restart after stop
+ isAudioSwitchInitScheduled = false
}
}
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProvider.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProvider.kt
index 28d7b31944..0bc011aecb 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProvider.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProvider.kt
@@ -40,4 +40,10 @@ internal interface ScopeProvider {
* Cleans up resources when the provider is no longer needed.
*/
fun cleanup()
+
+ /**
+ * Resets the provider to allow reuse after cleanup.
+ * This clears the cleanup flag and allows executors to be recreated.
+ */
+ fun reset()
}
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProviderImpl.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProviderImpl.kt
index 50a80c90ee..8f39f9829a 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProviderImpl.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/scope/ScopeProviderImpl.kt
@@ -97,4 +97,10 @@ internal class ScopeProviderImpl(
executor?.shutdown()
executor = null
}
+
+ override fun reset() {
+ logger.d { "Resetting ScopeProvider to allow reuse" }
+ isCleanedUp = false
+ // executor is already null after cleanup, will be recreated on next use
+ }
}