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 + } }