Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# airconsole-appengine/static/api

This subtree is the AppEngine-served copy of the public AirConsole JavaScript API bundle.

## Verification Entry Points

- Browser regression harness: serve this subtree statically and open a versioned runner in `tests/`.
- Playwright verification: in `ci/`, use `npm run server` and `npm test`.

## Local Invariants

- Keep versioned root bundles backward compatible.
- Stage upcoming releases in `beta/` before promotion.
- Do not remove `deprecated/` assets.
- Never call `rm`, use `safe-rm` instead (brew install safe-rm).

## Read Next

- `tests/AGENTS.md`
- `ci/AGENTS.md`

## Memory Management

Always use the **Kratos MCP** to manage memory across sessions:

- Store relevant context, decisions, and learnings via `kratos_memory_save` before ending a session.
- Retrieve prior context at the start of a new session using `kratos_memory_search` or `kratos_memory_get_recent`.
- Use `kratos_memory_ask` for natural language queries against accumulated memory.
- Never rely solely on in-context state for information that should persist across sessions.

## Code Access

Always use the **Serena MCP** for reading and writing code:

- Use `serena_find_symbol`, `serena_get_symbols_overview`, and `serena_search_for_pattern` to navigate and understand code.
- Use `serena_find_referencing_symbols` to find all callers/references before refactoring, and `serena_rename_symbol` to rename a symbol consistently across the codebase.
- Use `serena_replace_symbol_body`, `serena_replace_content`, `serena_insert_after_symbol`, and `serena_insert_before_symbol` to make code changes.
- Prefer symbol-level tools over raw text replacement when the target is a named code entity.
- Always call `serena_check_onboarding_performed` after activating a project.

## Syntax and API Verification

Always use the **Context7 MCP** to verify correct syntax and API usage before writing or modifying code that depends on external libraries:

- Call `context7_resolve-library-id` first to obtain the correct library ID for any framework or package.
- Call `context7_query-docs` with a specific query to retrieve up-to-date documentation and code examples.
- Use Context7 before writing code that depends on external library APIs to avoid outdated or hallucinated usage patterns.
99 changes: 99 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Media Permission Flow

## Actors

**Game** calls `getUserMedia(constraints)` and consumes the returned Promise.

**API** is the AirConsole JS SDK running on the controller. It validates input, tracks pending state, talks to the platform, and bridges browser level failures back to the game.

**Platform** decides whether the permission flow should run through a browser prompt, resolve natively, deny with a reason, or return an error.

**Browser** is `navigator.mediaDevices.getUserMedia`, the final source of stream success or browser level failure.

## Sequence

```mermaid
sequenceDiagram
autonumber
actor Game
participant API
participant Platform
participant Browser

Game->>API: getUserMedia(constraints)
alt Flow 1: early rejection
Note over API: Validate caller and constraints
Note over API: Reject early when device is SCREEN, device_id is undefined, media_permission_pending_ is true, or constraints are invalid
API-->>Game: resolve({ success: false, error })
else Request accepted
Note over API: mediaPermissionCallbacks_.set(instance, { resolve, reject })
Note over API: media_permission_pending_ = true
Note over API: start 30s timeout
API->>Platform: sendEvent_('requestUserMediaPermission', { constraints })

alt Flow 2: platform sends promptUserMediaPermission
Platform->>API: event promptUserMediaPermission
API->>Browser: navigator.mediaDevices.getUserMedia(constraints)
alt Browser succeeds
Browser-->>API: stream
API->>Platform: sendEvent_('userMediaPermissionGranted', { constraints })
API-->>Game: resolve({ success: true, stream })
else Flow 4: browser rejects after promptUserMediaPermission
Browser-->>API: error
API->>Platform: sendEvent_('userMediaPermissionDenied', { userPromptDuration, error })
Platform->>API: event userMediaPermissionDenied with data: { error }
API->>API: rejectMediaPermission_(error)
API-->>Game: Promise rejects with error
end

else Flow 3: platform sends userMediaPermissionGranted
Platform->>API: event userMediaPermissionGranted
API->>Browser: navigator.mediaDevices.getUserMedia(constraints)
alt Browser succeeds
Browser-->>API: stream
API->>Platform: sendEvent_('userMediaPermissionGranted', { constraints })
API-->>Game: resolve({ success: true, stream })
else Flow 5: browser rejects after userMediaPermissionGranted
Browser-->>API: error
API-->>Game: resolve({ success: false, error })
end

else Flow 6: platform denies with reason
Platform->>API: event userMediaPermissionDenied with data: { reason: 'temporary'|'permanent' }
API-->>Game: resolve({ success: false, reason })

else Flow 7: platform sends error directly
Platform->>API: event userMediaPermissionDenied with data: { error }
API->>API: rejectMediaPermission_(error)
API-->>Game: Promise rejects with error

else Flow 8: timeout
Note over API: 30s passes with no platform response
API-->>Game: resolve({ success: false, error: 'timeout' })
end

Note over API: Guard stale messages with media_permission_pending_
end

rect rgba(230, 230, 255, 0.35)
Note over Platform,Game: Flow 9: broadcast to other devices
Platform->>API: device update with _is_userMediaPermission_update: true
alt granted
API-->>Game: onUserMediaAccessGranted(device_id, constraints)
else denied
API-->>Game: onUserMediaAccessDenied(device_id, reason)
end
end
```

## Key Design Decisions

Platform denials resolve the Promise with `{ success: false, ... }`. Browser failures use Promise rejection when the platform echoes a browser error back through `userMediaPermissionDenied`, but native controller browser failures after `userMediaPermissionGranted` resolve with `{ success: false, error }` instead.

Browser prompt failures use a platform echo pattern. The API sends `sendEvent_('userMediaPermissionDenied', { userPromptDuration, error })`, waits for the platform to echo `userMediaPermissionDenied` with `data: { error }`, then calls `rejectMediaPermission_(error)`.

Callback storage lives in a `WeakMap`, which keeps resolve and reject handlers attached to the SDK instance without exposing them on public state.

The 30 second timeout is a safety net. It resolves `{ success: false, error: 'timeout' }` if the platform never answers.

`media_permission_pending_` blocks duplicate requests and ignores stale platform messages after cleanup.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ Release notes follow the [keep a changelog](https://keepachangelog.com/en/1.0.0/

## [Unreleased]

### Changed

- Changed `getUserMedia` browser-error rejection flow: controller now sends error to platform via `sendEvent_('userMediaPermissionDenied', { userPromptDuration, error })`; platform echoes back `userMediaPermissionDenied` with `data: { error }` which triggers `rejectMediaPermission_` locally, replacing the previous direct local rejection.

### Added

- Added `ARCHITECTURE.md` with mermaid sequence diagram documenting the full media permission flow.

## [1.10.0] - 2026-02-17

### Added
Expand Down
Loading