mirror of
https://github.com/zoriya/v10.git
synced 2026-08-15 18:34:22 +00:00
112 lines
2.7 KiB
Markdown
112 lines
2.7 KiB
Markdown
---
|
|
status: draft
|
|
date: 2025-01-27
|
|
---
|
|
|
|
# Feature Availability Design
|
|
|
|
## Problem
|
|
|
|
Features may target capabilities the platform doesn't support.
|
|
|
|
```ts
|
|
// iOS Safari doesn't allow programmatic volume control
|
|
request: {
|
|
setVolume: (vol, { target }) => {
|
|
target.volume = vol; // silently fails on iOS
|
|
},
|
|
}
|
|
```
|
|
|
|
UI needs to know: hide control? disable it? show it normally?
|
|
|
|
## Solution
|
|
|
|
Single availability state per feature:
|
|
|
|
```ts
|
|
type FeatureAvailability = 'available' | 'unavailable' | 'unsupported';
|
|
```
|
|
|
|
| Value | Meaning |
|
|
| --------------- | ------------------------------------------------------ |
|
|
| `'unsupported'` | Platform can never do this (e.g., iOS volume) |
|
|
| `'unavailable'` | Could work, not ready yet (e.g., waiting for manifest) |
|
|
| `'available'` | Ready to use |
|
|
|
|
---
|
|
|
|
## Related: Missing Feature vs Unavailable Capability
|
|
|
|
See [rfc/player-api](../../rfc/player-api/primitives.md) for feature access patterns.
|
|
|
|
| Concept | Cause | Detection |
|
|
| ---------------------- | ------------------- | --------------------------------- |
|
|
| Missing feature | Feature not included| `slice === undefined` |
|
|
| Unavailable capability | Platform limitation | `*Availability === 'unsupported'` |
|
|
|
|
---
|
|
|
|
## Implementation
|
|
|
|
### Naming Convention
|
|
|
|
Property: `{feature}Availability`
|
|
|
|
- `volumeAvailability`
|
|
- `qualityAvailability`
|
|
- `pipAvailability`
|
|
|
|
### Default Value
|
|
|
|
Always start `'unsupported'` (pessimistic). Must be proven otherwise.
|
|
|
|
### Async Capability Detection
|
|
|
|
Use module-level cache + `update()` pattern. No API changes needed.
|
|
|
|
```ts
|
|
let availability: FeatureAvailability = 'unsupported';
|
|
|
|
const volumeFeature = createFeature<HTMLMediaElement>()({
|
|
initialState: {
|
|
volume: 1,
|
|
volumeAvailability: 'unsupported',
|
|
},
|
|
|
|
getSnapshot: ({ target }) => ({
|
|
volume: target.volume,
|
|
volumeAvailability: availability,
|
|
}),
|
|
|
|
subscribe: ({ target, update, signal }) => {
|
|
listen(target, 'volumechange', update, { signal });
|
|
|
|
// Async detection
|
|
canChangeVolume().then((supported) => {
|
|
if (signal.aborted) return;
|
|
availability = supported ? 'available' : 'unsupported';
|
|
update();
|
|
});
|
|
},
|
|
|
|
request: {
|
|
setVolume: {
|
|
guard: () => availability === 'available',
|
|
handler: (vol, { target }) => {
|
|
target.volume = vol;
|
|
},
|
|
},
|
|
},
|
|
});
|
|
```
|
|
|
|
### Guards
|
|
|
|
Guards receive `{ target, signal }`, not state. Check capability on target directly.
|
|
|
|
## References
|
|
|
|
- Media Chrome uses similar pattern with `*Unavailable` properties
|
|
- Vidstack uses `canSetVolume`, `canSetQuality` computed properties
|