@livx.cc/native-kit 0.35.0 → 0.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/NativeKit.ts +20 -4
- package/src/core/web-adapter.ts +5 -1
- package/src/index.ts +1 -1
- package/src/modules/media.ts +39 -0
- package/src/modules/motion.ts +4 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Isomorphic native-capabilities kit for PWAs — the **same API in the browser and inside an [appwrap](https://github.com/Livshitz/appwrap) native shell**. Zero dependencies.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
bun add @livx.cc/native-kit
|
|
6
|
+
bun add @livx.cc/native-kit
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livx.cc/native-kit",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.2",
|
|
4
4
|
"description": "Isomorphic native-capabilities kit for PWAs \u2014 same API in browser and in an appwrap native shell. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Elya Livshitz",
|
package/src/core/NativeKit.ts
CHANGED
|
@@ -122,10 +122,26 @@ export class NativeKit {
|
|
|
122
122
|
ready(): Promise<Handshake> {
|
|
123
123
|
if (!this.readyPromise) {
|
|
124
124
|
this.readyPromise = (async () => {
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
const candidates = this.options.adapters.filter((a) => a.detect());
|
|
126
|
+
if (!candidates.length) throw new KitError('NOT_READY', 'No adapter detected this environment');
|
|
127
|
+
// Try each detected adapter in priority order. A transport can LOOK native yet refuse
|
|
128
|
+
// the handshake — e.g. a host shell exposing a capability-GATED `appwrap` message
|
|
129
|
+
// handler to an embedded/mini-app page (CAP_DENIED, or no handshake handler at all).
|
|
130
|
+
// Falling through to the next adapter (web) lets the app degrade to standard web
|
|
131
|
+
// APIs instead of dying with no transport at all.
|
|
132
|
+
let handshake: Handshake | null = null;
|
|
133
|
+
let lastError: unknown = null;
|
|
134
|
+
for (const adapter of candidates) {
|
|
135
|
+
try {
|
|
136
|
+
handshake = await adapter.handshake(this.options.handshakeTimeoutMs);
|
|
137
|
+
this.adapter = adapter;
|
|
138
|
+
break;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
lastError = e;
|
|
141
|
+
console.warn(`[native-kit] ${adapter.kind} adapter handshake failed (${(e as Error)?.message ?? e}) — trying next adapter`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!handshake) throw lastError ?? new KitError('NOT_READY', 'All adapters failed the handshake');
|
|
129
145
|
// Version-skew safety net: a native shell from an older `appwrap init` may speak a
|
|
130
146
|
// different protocol. Fail loud rather than silently mis-degrade. The web adapter
|
|
131
147
|
// always reports the kit's own protocol, so this only ever fires against a stale shell.
|
package/src/core/web-adapter.ts
CHANGED
|
@@ -316,10 +316,14 @@ export class WebAdapter implements NativeKitAdapter {
|
|
|
316
316
|
if (state !== 'granted') throw new KitError('DENIED', 'Motion permission not granted');
|
|
317
317
|
}
|
|
318
318
|
if (!this.motionHandler) {
|
|
319
|
+
// Honor the requested rate (default 10 Hz, clamped 5–60 — mirrors the native cap);
|
|
320
|
+
// a tilt game asks 60 and 10 Hz steering is unplayably laggy.
|
|
321
|
+
const hz = Math.min(60, Math.max(5, Number(p.hz) || 10));
|
|
322
|
+
const minMs = 1000 / hz;
|
|
319
323
|
let last = 0;
|
|
320
324
|
this.motionHandler = (e) => {
|
|
321
325
|
const now = performance.now();
|
|
322
|
-
if (now - last <
|
|
326
|
+
if (now - last < minMs) return;
|
|
323
327
|
last = now;
|
|
324
328
|
const a = e.accelerationIncludingGravity;
|
|
325
329
|
const r = e.rotationRate;
|
package/src/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type { ScheduleOptions } from './modules/notifications';
|
|
|
29
29
|
export type { PushMessage, PushPlatform, PushToken } from './modules/push';
|
|
30
30
|
export type { GeoPosition } from './modules/geo';
|
|
31
31
|
export type { PickedPhoto, PickPhotoOptions } from './modules/photos';
|
|
32
|
-
export type { AudioMode, MediaDeviceLite } from './modules/media';
|
|
32
|
+
export type { AudioMode, MediaDeviceLite, AudioState } from './modules/media';
|
|
33
33
|
export type { NetworkStatus } from './modules/network';
|
|
34
34
|
export type { ActionOptions, AlertOptions, ConfirmOptions, SafeAreaInsets } from './modules/ui';
|
|
35
35
|
export type { MotionSample } from './modules/motion';
|
package/src/modules/media.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
import type { Unsubscribe } from '../core/types';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Live media bridge — mic / camera / speaker. The streams themselves are plain
|
|
@@ -9,6 +10,14 @@ import type { NativeKit } from '../core/NativeKit';
|
|
|
9
10
|
*/
|
|
10
11
|
export type AudioMode = 'playback' | 'playAndRecord' | 'voiceChat' | 'default';
|
|
11
12
|
|
|
13
|
+
/** Whether the device would actually play game/media sound right now, plus the media volume (0–1). */
|
|
14
|
+
export interface AudioState {
|
|
15
|
+
/** True when output is effectively silenced — iOS mute switch OR zero media volume; Android: zero media volume. */
|
|
16
|
+
silent: boolean;
|
|
17
|
+
/** Media output volume, 0–1 (iOS AVAudioSession.outputVolume / Android STREAM_MUSIC). */
|
|
18
|
+
volume: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export interface MediaDeviceLite {
|
|
13
22
|
kind: MediaDeviceKind;
|
|
14
23
|
label: string;
|
|
@@ -68,6 +77,36 @@ export class MediaModule {
|
|
|
68
77
|
return this.kit.invoke('media.configureAudio', { mode });
|
|
69
78
|
}
|
|
70
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Watch whether sound would actually be heard — for reflecting the OS mute/volume state in UI
|
|
82
|
+
* (e.g. disabling an in-app speaker toggle while the device is silenced). Fires `cb` with the
|
|
83
|
+
* current {@link AudioState} immediately and again whenever it changes (~1s granularity). Native
|
|
84
|
+
* only — on web (no OS silent concept) this is a no-op returning a noop unsubscribe.
|
|
85
|
+
*/
|
|
86
|
+
async watchAudio(cb: (state: AudioState) => void): Promise<Unsubscribe> {
|
|
87
|
+
// Wait for the handshake so `platform` is resolved (it defaults to 'web' until ready — callers
|
|
88
|
+
// often subscribe during early app setup, before the bridge handshake lands).
|
|
89
|
+
await this.kit.ready().catch(() => {});
|
|
90
|
+
// Gate on PLATFORM, not the 'media' capability: the audio-state handler is always registered in
|
|
91
|
+
// the native shell, so this works even for apps that don't opt into the full media module. Only
|
|
92
|
+
// the browser (no OS silent/volume concept) is a genuine no-op.
|
|
93
|
+
if (this.kit.platform === 'web') return () => {};
|
|
94
|
+
const off = this.kit.on('media.audioState', (p) => cb(p as AudioState));
|
|
95
|
+
try {
|
|
96
|
+
await this.kit.invoke('media.audioWatch.start');
|
|
97
|
+
} catch (e) {
|
|
98
|
+
off();
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
let stopped = false;
|
|
102
|
+
return () => {
|
|
103
|
+
if (stopped) return;
|
|
104
|
+
stopped = true;
|
|
105
|
+
off();
|
|
106
|
+
this.kit.invoke('media.audioWatch.stop').catch((e) => console.warn('[native-kit] audioWatch.stop failed', e));
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
/** Stop every track on a stream — convenience to release the camera/mic LED. */
|
|
72
111
|
stop(stream: MediaStream | null | undefined): void {
|
|
73
112
|
stream?.getTracks().forEach((t) => t.stop());
|
package/src/modules/motion.ts
CHANGED
|
@@ -19,11 +19,12 @@ export class MotionModule {
|
|
|
19
19
|
return this.kit.capability('motion');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Stream motion samples
|
|
23
|
-
|
|
22
|
+
/** Stream motion samples; resolves an unsubscribe once streaming starts. `opts.hz` sets the emit
|
|
23
|
+
* rate (default 10, clamped 5–60 native-side) — bump it for crisp tilt (e.g. a game asks 60). */
|
|
24
|
+
async watch(cb: (sample: MotionSample) => void, opts?: { hz?: number }): Promise<Unsubscribe> {
|
|
24
25
|
const off = this.kit.on('motion.data', (p) => cb(p as MotionSample));
|
|
25
26
|
try {
|
|
26
|
-
await this.kit.invoke('motion.start');
|
|
27
|
+
await this.kit.invoke('motion.start', opts?.hz ? { hz: opts.hz } : undefined);
|
|
27
28
|
} catch (e) {
|
|
28
29
|
off();
|
|
29
30
|
throw e;
|