@shenora/react 0.11.0 → 0.12.0
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/dist/bridge.d.ts +28 -47
- package/dist/bridge.js +37 -70
- package/dist/clipboard.d.ts +5 -10
- package/dist/clipboard.js +11 -18
- package/dist/devInterceptor.d.ts +8 -12
- package/dist/devInterceptor.js +10 -15
- package/dist/errors.d.ts +4 -5
- package/dist/errors.js +4 -5
- package/dist/eventBus.d.ts +13 -28
- package/dist/eventBus.js +19 -40
- package/dist/fileDialogs.d.ts +8 -11
- package/dist/fileDialogs.js +9 -13
- package/dist/hooks.d.ts +15 -24
- package/dist/hooks.js +19 -31
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -14
- package/dist/internal.d.ts +2 -8
- package/dist/internal.js +2 -8
- package/dist/media.d.ts +14 -22
- package/dist/media.js +14 -22
- package/dist/mediaPlayer.d.ts +39 -22
- package/dist/mediaPlayer.js +54 -44
- package/dist/moduleService.d.ts +11 -22
- package/dist/moduleService.js +11 -22
- package/dist/requests.d.ts +34 -65
- package/dist/requests.js +21 -54
- package/dist/segmentBinder.d.ts +13 -26
- package/dist/segmentBinder.js +25 -42
- package/dist/segmentStream.d.ts +43 -54
- package/dist/segmentStream.js +52 -61
- package/dist/store.d.ts +15 -26
- package/dist/store.js +27 -55
- package/dist/transport.d.ts +9 -18
- package/dist/transport.js +9 -18
- package/dist/types.d.ts +23 -57
- package/dist/types.js +19 -40
- package/dist/useDropZone.d.ts +13 -25
- package/dist/useDropZone.js +24 -41
- package/dist/windowCommands.d.ts +14 -18
- package/dist/windowCommands.js +16 -23
- package/package.json +1 -1
package/dist/media.d.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Building the URL that lets a page render LOCAL content — media, images, documents, exports — that it
|
|
3
|
-
* cannot reach directly.
|
|
4
|
-
*
|
|
5
|
-
* The host answers these through its resource interceptor; this module only builds the address, which is
|
|
6
|
-
* why it is a pure function and not a hook. A hook (`useMediaSource`) can follow if an adopter wants
|
|
7
|
-
* load/error state, but nothing needs one to start.
|
|
3
|
+
* cannot reach directly. The host answers these through its resource interceptor; this module only
|
|
4
|
+
* builds the address, which is why it is a pure function and not a hook.
|
|
8
5
|
*/
|
|
9
6
|
/**
|
|
10
7
|
* Build a URL the host's resource interceptor will answer: `<route>?<base64url of JSON>`.
|
|
11
8
|
*
|
|
12
|
-
* ⚠ **The result is RELATIVE, and that is
|
|
9
|
+
* ⚠ **The result is RELATIVE, and that is load-bearing.** Written as a path rather than with a scheme, it
|
|
13
10
|
* resolves against whatever origin the page is already served from — which means the browser hands the host
|
|
14
11
|
* a URL each platform can actually decode:
|
|
15
12
|
*
|
|
@@ -19,19 +16,16 @@
|
|
|
19
16
|
* | Android | `https://0.0.0.1/media?…` — Android's media pipeline REFUSES a non-standard scheme |
|
|
20
17
|
* | desktop | the app's virtual host |
|
|
21
18
|
*
|
|
22
|
-
*
|
|
23
|
-
* neither: iOS cannot register a handler for `https`, and Android cannot register a scheme
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* The PAYLOAD is opaque to this package: whatever you pass is JSON-encoded and handed to your own host-side
|
|
27
|
-
* route, which decodes it. That keeps the kit out of your addressing scheme — a filename, an id, a container
|
|
28
|
-
* preference, a cache key, several of them. The kit encodes; you decide what it means.
|
|
19
|
+
* 🔴 **So do not hardcode `app://`.** Either fixed form fails on exactly one platform, and registering the
|
|
20
|
+
* scheme rescues neither: iOS cannot register a handler for `https`, and Android cannot register a scheme
|
|
21
|
+
* at all.
|
|
29
22
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* The payload is opaque to this package — JSON-encoded and handed to your own host-side route, which
|
|
24
|
+
* decodes it. base64**url** specifically, so `+`, `/` and `=` cannot be re-interpreted by anything that
|
|
25
|
+
* parses URLs along the way.
|
|
32
26
|
*
|
|
33
|
-
* ⚠ An encoded payload costs debuggability
|
|
34
|
-
*
|
|
27
|
+
* ⚠ An encoded payload costs debuggability: have the HOST log what it decoded to, because an error
|
|
28
|
+
* response body cannot say without leaking paths.
|
|
35
29
|
*
|
|
36
30
|
* @param payload Anything JSON-serialisable. Your host route decides what the shape means.
|
|
37
31
|
* @param route The reserved path the host answers on. **Must not collide with a real asset in your bundle** —
|
|
@@ -52,14 +46,12 @@ export declare function mediaUrl(payload: unknown, route?: string): string;
|
|
|
52
46
|
* The payload encoding on its own, for a caller that builds its own URL but wants the same wire format —
|
|
53
47
|
* and so a host-side decoder has one documented thing to mirror.
|
|
54
48
|
*
|
|
55
|
-
* `TextEncoder` before `btoa
|
|
56
|
-
* non-ASCII title or path would fail at the call site
|
|
49
|
+
* ⚠ `TextEncoder` before `btoa`, which throws on any character above U+00FF — a payload carrying a
|
|
50
|
+
* non-ASCII title or path would fail at the call site.
|
|
57
51
|
*/
|
|
58
52
|
export declare function encodeMediaPayload(payload: unknown): string;
|
|
59
53
|
/**
|
|
60
54
|
* Decode what {@link encodeMediaPayload} produced. Here for tests and for a page that round-trips its own
|
|
61
|
-
* URLs; the real decoder is host-side
|
|
62
|
-
*
|
|
63
|
-
* Padding is restored before decoding — base64url drops `=`, and `atob` requires it.
|
|
55
|
+
* URLs; the real decoder is host-side. Padding is restored first — base64url drops `=`, `atob` needs it.
|
|
64
56
|
*/
|
|
65
57
|
export declare function decodeMediaPayload<T = unknown>(encoded: string): T;
|
package/dist/media.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Building the URL that lets a page render LOCAL content — media, images, documents, exports — that it
|
|
3
|
-
* cannot reach directly.
|
|
4
|
-
*
|
|
5
|
-
* The host answers these through its resource interceptor; this module only builds the address, which is
|
|
6
|
-
* why it is a pure function and not a hook. A hook (`useMediaSource`) can follow if an adopter wants
|
|
7
|
-
* load/error state, but nothing needs one to start.
|
|
3
|
+
* cannot reach directly. The host answers these through its resource interceptor; this module only
|
|
4
|
+
* builds the address, which is why it is a pure function and not a hook.
|
|
8
5
|
*/
|
|
9
6
|
/**
|
|
10
7
|
* Build a URL the host's resource interceptor will answer: `<route>?<base64url of JSON>`.
|
|
11
8
|
*
|
|
12
|
-
* ⚠ **The result is RELATIVE, and that is
|
|
9
|
+
* ⚠ **The result is RELATIVE, and that is load-bearing.** Written as a path rather than with a scheme, it
|
|
13
10
|
* resolves against whatever origin the page is already served from — which means the browser hands the host
|
|
14
11
|
* a URL each platform can actually decode:
|
|
15
12
|
*
|
|
@@ -19,19 +16,16 @@
|
|
|
19
16
|
* | Android | `https://0.0.0.1/media?…` — Android's media pipeline REFUSES a non-standard scheme |
|
|
20
17
|
* | desktop | the app's virtual host |
|
|
21
18
|
*
|
|
22
|
-
*
|
|
23
|
-
* neither: iOS cannot register a handler for `https`, and Android cannot register a scheme
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* The PAYLOAD is opaque to this package: whatever you pass is JSON-encoded and handed to your own host-side
|
|
27
|
-
* route, which decodes it. That keeps the kit out of your addressing scheme — a filename, an id, a container
|
|
28
|
-
* preference, a cache key, several of them. The kit encodes; you decide what it means.
|
|
19
|
+
* 🔴 **So do not hardcode `app://`.** Either fixed form fails on exactly one platform, and registering the
|
|
20
|
+
* scheme rescues neither: iOS cannot register a handler for `https`, and Android cannot register a scheme
|
|
21
|
+
* at all.
|
|
29
22
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* The payload is opaque to this package — JSON-encoded and handed to your own host-side route, which
|
|
24
|
+
* decodes it. base64**url** specifically, so `+`, `/` and `=` cannot be re-interpreted by anything that
|
|
25
|
+
* parses URLs along the way.
|
|
32
26
|
*
|
|
33
|
-
* ⚠ An encoded payload costs debuggability
|
|
34
|
-
*
|
|
27
|
+
* ⚠ An encoded payload costs debuggability: have the HOST log what it decoded to, because an error
|
|
28
|
+
* response body cannot say without leaking paths.
|
|
35
29
|
*
|
|
36
30
|
* @param payload Anything JSON-serialisable. Your host route decides what the shape means.
|
|
37
31
|
* @param route The reserved path the host answers on. **Must not collide with a real asset in your bundle** —
|
|
@@ -58,8 +52,8 @@ export function mediaUrl(payload, route = 'media') {
|
|
|
58
52
|
* The payload encoding on its own, for a caller that builds its own URL but wants the same wire format —
|
|
59
53
|
* and so a host-side decoder has one documented thing to mirror.
|
|
60
54
|
*
|
|
61
|
-
* `TextEncoder` before `btoa
|
|
62
|
-
* non-ASCII title or path would fail at the call site
|
|
55
|
+
* ⚠ `TextEncoder` before `btoa`, which throws on any character above U+00FF — a payload carrying a
|
|
56
|
+
* non-ASCII title or path would fail at the call site.
|
|
63
57
|
*/
|
|
64
58
|
export function encodeMediaPayload(payload) {
|
|
65
59
|
const bytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
@@ -70,9 +64,7 @@ export function encodeMediaPayload(payload) {
|
|
|
70
64
|
}
|
|
71
65
|
/**
|
|
72
66
|
* Decode what {@link encodeMediaPayload} produced. Here for tests and for a page that round-trips its own
|
|
73
|
-
* URLs; the real decoder is host-side
|
|
74
|
-
*
|
|
75
|
-
* Padding is restored before decoding — base64url drops `=`, and `atob` requires it.
|
|
67
|
+
* URLs; the real decoder is host-side. Padding is restored first — base64url drops `=`, `atob` needs it.
|
|
76
68
|
*/
|
|
77
69
|
export function decodeMediaPayload(encoded) {
|
|
78
70
|
let padded = encoded.replace(/-/g, '+').replace(/_/g, '/');
|
package/dist/mediaPlayer.d.ts
CHANGED
|
@@ -2,12 +2,11 @@ import { type RefObject } from 'react';
|
|
|
2
2
|
import { type ShenoraBridge } from './bridge.js';
|
|
3
3
|
import { type ShenoraEventBus } from './eventBus.js';
|
|
4
4
|
/**
|
|
5
|
-
* The module the player speaks on. Matches `MediaPlayerOptions.Access.Module` on the host
|
|
6
|
-
* containment/module boundary every media delivery path shares — `MediaAccessOptions`, D71), which
|
|
5
|
+
* The module the player speaks on. Matches `MediaPlayerOptions.Access.Module` on the host, which
|
|
7
6
|
* defaults to the same string — change one and you must change the other.
|
|
8
7
|
*
|
|
9
|
-
* ⚠ The `SHENORA.` prefix is RESERVED for the kit's own modules (D64),
|
|
10
|
-
*
|
|
8
|
+
* ⚠ The `SHENORA.` prefix is RESERVED for the kit's own modules (D64), so your app stays free to own
|
|
9
|
+
* a module called plainly `MEDIA`.
|
|
11
10
|
*/
|
|
12
11
|
export declare const MEDIA_PLAYER_MODULE = "SHENORA.MEDIA";
|
|
13
12
|
/**
|
|
@@ -24,11 +23,38 @@ export declare const MediaPlayerCommands: {
|
|
|
24
23
|
};
|
|
25
24
|
/** The one message the page sends back. The host turns it into `MediaPlayer.Report(...)`. */
|
|
26
25
|
export declare const MEDIA_PLAYER_REPORT = "PLAYER_REPORT";
|
|
26
|
+
/** What the page asks to read the host player's current status (`MediaPlayerModule.StatusType`). */
|
|
27
|
+
export declare const MEDIA_PLAYER_STATUS = "PLAYER_STATUS";
|
|
28
|
+
/**
|
|
29
|
+
* A CONVERSION's events, on the same module. **A wire contract** mirrored from C#
|
|
30
|
+
* `MediaConversionEvents` — a conversion outlives the request that started it, so the page learns from
|
|
31
|
+
* these rather than from a response.
|
|
32
|
+
*/
|
|
33
|
+
export declare const MediaConversionEvents: {
|
|
34
|
+
/** Fraction complete: `{ source, progress }`. Throttle in the app if the engine is chatty. */
|
|
35
|
+
readonly sourceProgress: "SOURCE_PROGRESS";
|
|
36
|
+
/** The converted file is servable: `{ source }`. Set the element's `src` now. */
|
|
37
|
+
readonly ready: "READY";
|
|
38
|
+
/** Failed: `{ source, reason }`, plus `dropped` when `reason` is {@link MediaConversionErrorCodes}. */
|
|
39
|
+
readonly failed: "FAILED";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Stable `reason` tokens on {@link MediaConversionEvents.failed}. Anything else is a TYPE name from an
|
|
43
|
+
* unexpected fault — never exception text.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MediaConversionErrorCodes: {
|
|
46
|
+
/**
|
|
47
|
+
* The output would have lost a stream, so nothing was cached; the event carries `dropped`, the codecs.
|
|
48
|
+
* ⚠ It means "not playable HERE", not always "not supported" — a run configured with no conversion
|
|
49
|
+
* seam never asked the platform.
|
|
50
|
+
*/
|
|
51
|
+
readonly unsupportedCodec: "UNSUPPORTED_CODEC";
|
|
52
|
+
};
|
|
27
53
|
/**
|
|
28
54
|
* What the element is doing, in the host's vocabulary (`MediaPlayerState`).
|
|
29
55
|
*
|
|
30
|
-
* ⚠ `
|
|
31
|
-
*
|
|
56
|
+
* ⚠ `Opening` and `Buffering` are distinct: opening is "no position yet", buffering is "had one and it
|
|
57
|
+
* stopped moving". Collapsing them makes a UI extrapolate a position that is not advancing.
|
|
32
58
|
*/
|
|
33
59
|
export type MediaPlayerReportState = 'Empty' | 'Opening' | 'Paused' | 'Playing' | 'Buffering' | 'Ended' | 'Failed';
|
|
34
60
|
/** One state report, sent on TRANSITIONS only. */
|
|
@@ -60,27 +86,18 @@ export interface UseMediaPlayerOptions {
|
|
|
60
86
|
* ```
|
|
61
87
|
*
|
|
62
88
|
* **That is the whole page-side integration.** No `src`, no play button wiring, no state machine — the app
|
|
63
|
-
* calls `IMediaPlayer.OpenAsync/PlayAsync` in C#, and this drives the element to match. The
|
|
64
|
-
*
|
|
65
|
-
* at all, which needs a probe and a device capability query).
|
|
66
|
-
*
|
|
67
|
-
* ⚠ **The host half needs nothing from you.** The reports posted here are an ordinary IPC message
|
|
68
|
-
* (`PLAYER_REPORT` on {@link MEDIA_PLAYER_MODULE}) and the kit's own `MediaPlayerModule` answers it,
|
|
69
|
-
* registered by the media feature itself. If you wrote that route by hand against a build from before
|
|
70
|
-
* 2026-08-07, **delete it** — two facades on one module is a duplicate the host rejects.
|
|
89
|
+
* calls `IMediaPlayer.OpenAsync/PlayAsync` in C#, and this drives the element to match. The host half
|
|
90
|
+
* needs nothing from you: the kit's own `MediaPlayerModule` answers the reports posted here.
|
|
71
91
|
*
|
|
72
92
|
* ⚠ **The element must exist when this effect first runs.** `ref.current` is read once, and a `useRef`
|
|
73
93
|
* object is stable, so an element rendered CONDITIONALLY (`{ready && <video ref={ref} />}`) mounts after
|
|
74
|
-
* the effect and never binds — silently. Render
|
|
75
|
-
*
|
|
94
|
+
* the effect and never binds — silently. Render it unconditionally and hide it with CSS, or key the
|
|
95
|
+
* component so the hook remounts with it.
|
|
76
96
|
*
|
|
77
|
-
* ⚠ **It reports on TRANSITIONS, never on `timeupdate
|
|
78
|
-
*
|
|
79
|
-
* you need a moving scrubber, read `element.currentTime` in your own render loop — locally, at the rate you
|
|
80
|
-
* actually redraw.
|
|
97
|
+
* ⚠ **It reports on TRANSITIONS, never on `timeupdate`**, which fires ~4×/second. For a moving scrubber,
|
|
98
|
+
* read `element.currentTime` in your own render loop.
|
|
81
99
|
*
|
|
82
100
|
* ⚠ **Autoplay policies still apply.** A `PLAYER_PLAY` arriving before any user gesture can be refused by
|
|
83
|
-
* the browser, and the element reports `Failed
|
|
84
|
-
* hears about it rather than silently believing playback started.
|
|
101
|
+
* the browser, and the element then reports `Failed` rather than the host believing playback started.
|
|
85
102
|
*/
|
|
86
103
|
export declare function useMediaPlayer(ref: RefObject<HTMLMediaElement | null>, options?: UseMediaPlayerOptions): void;
|
package/dist/mediaPlayer.js
CHANGED
|
@@ -2,12 +2,11 @@ import { useEffect } from 'react';
|
|
|
2
2
|
import { getBridge } from './bridge.js';
|
|
3
3
|
import { eventBus as defaultEventBus } from './eventBus.js';
|
|
4
4
|
/**
|
|
5
|
-
* The module the player speaks on. Matches `MediaPlayerOptions.Access.Module` on the host
|
|
6
|
-
* containment/module boundary every media delivery path shares — `MediaAccessOptions`, D71), which
|
|
5
|
+
* The module the player speaks on. Matches `MediaPlayerOptions.Access.Module` on the host, which
|
|
7
6
|
* defaults to the same string — change one and you must change the other.
|
|
8
7
|
*
|
|
9
|
-
* ⚠ The `SHENORA.` prefix is RESERVED for the kit's own modules (D64),
|
|
10
|
-
*
|
|
8
|
+
* ⚠ The `SHENORA.` prefix is RESERVED for the kit's own modules (D64), so your app stays free to own
|
|
9
|
+
* a module called plainly `MEDIA`.
|
|
11
10
|
*/
|
|
12
11
|
export const MEDIA_PLAYER_MODULE = 'SHENORA.MEDIA';
|
|
13
12
|
/**
|
|
@@ -24,6 +23,33 @@ export const MediaPlayerCommands = {
|
|
|
24
23
|
};
|
|
25
24
|
/** The one message the page sends back. The host turns it into `MediaPlayer.Report(...)`. */
|
|
26
25
|
export const MEDIA_PLAYER_REPORT = 'PLAYER_REPORT';
|
|
26
|
+
/** What the page asks to read the host player's current status (`MediaPlayerModule.StatusType`). */
|
|
27
|
+
export const MEDIA_PLAYER_STATUS = 'PLAYER_STATUS';
|
|
28
|
+
/**
|
|
29
|
+
* A CONVERSION's events, on the same module. **A wire contract** mirrored from C#
|
|
30
|
+
* `MediaConversionEvents` — a conversion outlives the request that started it, so the page learns from
|
|
31
|
+
* these rather than from a response.
|
|
32
|
+
*/
|
|
33
|
+
export const MediaConversionEvents = {
|
|
34
|
+
/** Fraction complete: `{ source, progress }`. Throttle in the app if the engine is chatty. */
|
|
35
|
+
sourceProgress: 'SOURCE_PROGRESS',
|
|
36
|
+
/** The converted file is servable: `{ source }`. Set the element's `src` now. */
|
|
37
|
+
ready: 'READY',
|
|
38
|
+
/** Failed: `{ source, reason }`, plus `dropped` when `reason` is {@link MediaConversionErrorCodes}. */
|
|
39
|
+
failed: 'FAILED',
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Stable `reason` tokens on {@link MediaConversionEvents.failed}. Anything else is a TYPE name from an
|
|
43
|
+
* unexpected fault — never exception text.
|
|
44
|
+
*/
|
|
45
|
+
export const MediaConversionErrorCodes = {
|
|
46
|
+
/**
|
|
47
|
+
* The output would have lost a stream, so nothing was cached; the event carries `dropped`, the codecs.
|
|
48
|
+
* ⚠ It means "not playable HERE", not always "not supported" — a run configured with no conversion
|
|
49
|
+
* seam never asked the platform.
|
|
50
|
+
*/
|
|
51
|
+
unsupportedCodec: 'UNSUPPORTED_CODEC',
|
|
52
|
+
};
|
|
27
53
|
/**
|
|
28
54
|
* Bind a `<video>`/`<audio>` element to the HOST's player: the element becomes the display and the sound,
|
|
29
55
|
* and .NET owns the lifecycle (D58).
|
|
@@ -35,28 +61,19 @@ export const MEDIA_PLAYER_REPORT = 'PLAYER_REPORT';
|
|
|
35
61
|
* ```
|
|
36
62
|
*
|
|
37
63
|
* **That is the whole page-side integration.** No `src`, no play button wiring, no state machine — the app
|
|
38
|
-
* calls `IMediaPlayer.OpenAsync/PlayAsync` in C#, and this drives the element to match. The
|
|
39
|
-
*
|
|
40
|
-
* at all, which needs a probe and a device capability query).
|
|
41
|
-
*
|
|
42
|
-
* ⚠ **The host half needs nothing from you.** The reports posted here are an ordinary IPC message
|
|
43
|
-
* (`PLAYER_REPORT` on {@link MEDIA_PLAYER_MODULE}) and the kit's own `MediaPlayerModule` answers it,
|
|
44
|
-
* registered by the media feature itself. If you wrote that route by hand against a build from before
|
|
45
|
-
* 2026-08-07, **delete it** — two facades on one module is a duplicate the host rejects.
|
|
64
|
+
* calls `IMediaPlayer.OpenAsync/PlayAsync` in C#, and this drives the element to match. The host half
|
|
65
|
+
* needs nothing from you: the kit's own `MediaPlayerModule` answers the reports posted here.
|
|
46
66
|
*
|
|
47
67
|
* ⚠ **The element must exist when this effect first runs.** `ref.current` is read once, and a `useRef`
|
|
48
68
|
* object is stable, so an element rendered CONDITIONALLY (`{ready && <video ref={ref} />}`) mounts after
|
|
49
|
-
* the effect and never binds — silently. Render
|
|
50
|
-
*
|
|
69
|
+
* the effect and never binds — silently. Render it unconditionally and hide it with CSS, or key the
|
|
70
|
+
* component so the hook remounts with it.
|
|
51
71
|
*
|
|
52
|
-
* ⚠ **It reports on TRANSITIONS, never on `timeupdate
|
|
53
|
-
*
|
|
54
|
-
* you need a moving scrubber, read `element.currentTime` in your own render loop — locally, at the rate you
|
|
55
|
-
* actually redraw.
|
|
72
|
+
* ⚠ **It reports on TRANSITIONS, never on `timeupdate`**, which fires ~4×/second. For a moving scrubber,
|
|
73
|
+
* read `element.currentTime` in your own render loop.
|
|
56
74
|
*
|
|
57
75
|
* ⚠ **Autoplay policies still apply.** A `PLAYER_PLAY` arriving before any user gesture can be refused by
|
|
58
|
-
* the browser, and the element reports `Failed
|
|
59
|
-
* hears about it rather than silently believing playback started.
|
|
76
|
+
* the browser, and the element then reports `Failed` rather than the host believing playback started.
|
|
60
77
|
*/
|
|
61
78
|
export function useMediaPlayer(ref, options = {}) {
|
|
62
79
|
const { module = MEDIA_PLAYER_MODULE, bridge, eventBus = defaultEventBus } = options;
|
|
@@ -65,8 +82,7 @@ export function useMediaPlayer(ref, options = {}) {
|
|
|
65
82
|
if (!element)
|
|
66
83
|
return;
|
|
67
84
|
const link = bridge ?? getBridge();
|
|
68
|
-
// The element is the only clock: the host
|
|
69
|
-
// report always carries what the element actually believes.
|
|
85
|
+
// The element is the only clock: the host tracks no position of its own.
|
|
70
86
|
const report = (state, error) => {
|
|
71
87
|
const duration = Number.isFinite(element.duration) ? element.duration : null;
|
|
72
88
|
const payload = {
|
|
@@ -77,11 +93,10 @@ export function useMediaPlayer(ref, options = {}) {
|
|
|
77
93
|
};
|
|
78
94
|
link.post(module, MEDIA_PLAYER_REPORT, { payload });
|
|
79
95
|
};
|
|
80
|
-
// The PENDING start-at seek, if any. 🔴 It has to be cancellable: `{ once: true }` removes a
|
|
81
|
-
// only when it FIRES, and a second PLAYER_LOAD
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// listener of its own and A's is still attached.
|
|
96
|
+
// The PENDING start-at seek, if any. 🔴 It has to be cancellable: `{ once: true }` removes a
|
|
97
|
+
// listener only when it FIRES, and a second PLAYER_LOAD aborts the first load so its
|
|
98
|
+
// `loadedmetadata` never comes. The stale listener then runs on the NEXT track's metadata — load A
|
|
99
|
+
// at 10:00 then B at 0:00 and B starts ten minutes in.
|
|
85
100
|
let pendingSeek = null;
|
|
86
101
|
const cancelPendingSeek = () => {
|
|
87
102
|
if (pendingSeek)
|
|
@@ -107,10 +122,9 @@ export function useMediaPlayer(ref, options = {}) {
|
|
|
107
122
|
}),
|
|
108
123
|
eventBus.subscribe(module, MediaPlayerCommands.play, () => {
|
|
109
124
|
// A rejected play() is an autoplay refusal, and the host must hear it rather than assume success.
|
|
110
|
-
// ⚠ Read `.name` STRUCTURALLY
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// what an autoplay block actually says, and it is the one an adopter will want to branch on.
|
|
125
|
+
// ⚠ Read `.name` STRUCTURALLY, never via `instanceof Error`: browsers reject with a
|
|
126
|
+
// DOMException, which is not an Error subclass everywhere (jsdom's is not). `NotAllowedError`
|
|
127
|
+
// is what an autoplay block says, and the name an adopter branches on.
|
|
114
128
|
void element.play().catch((cause) => {
|
|
115
129
|
const name = cause?.name;
|
|
116
130
|
report('Failed', typeof name === 'string' && name ? name : 'PlayRejected');
|
|
@@ -133,7 +147,7 @@ export function useMediaPlayer(ref, options = {}) {
|
|
|
133
147
|
}),
|
|
134
148
|
];
|
|
135
149
|
// ── element → host ────────────────────────────────────────────────────────────────────────────
|
|
136
|
-
// Transitions only
|
|
150
|
+
// Transitions only — no `timeupdate`, see the remarks.
|
|
137
151
|
const listeners = [
|
|
138
152
|
['loadedmetadata', () => report('Paused')],
|
|
139
153
|
['canplay', () => report(element.paused ? 'Paused' : 'Playing')],
|
|
@@ -148,17 +162,13 @@ export function useMediaPlayer(ref, options = {}) {
|
|
|
148
162
|
for (const [event, handler] of listeners)
|
|
149
163
|
element.addEventListener(event, handler);
|
|
150
164
|
// 🔴 REPORT WHEN THE PAGE IS ABOUT TO BE HIDDEN, or the host's position is whatever the last
|
|
151
|
-
// TRANSITION left —
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// while the host believed 0.01 s, and `BackgroundPlaybackTransfer` handed the native player 0.01 s.
|
|
155
|
-
// The user backgrounds mid-film and resumes from the beginning. The platform's `pause` at background
|
|
156
|
-
// time does fire, but not in time to cross IPC before the process is frozen.
|
|
165
|
+
// TRANSITION left — for steady playback, the moment it started. The user then backgrounds mid-film
|
|
166
|
+
// and `BackgroundPlaybackTransfer` resumes from the beginning. The platform's `pause` does fire,
|
|
167
|
+
// but not in time to cross IPC before the process is frozen.
|
|
157
168
|
//
|
|
158
|
-
// ⚠ `visibilitychange` rather than `pagehide`:
|
|
159
|
-
// bridge can still post, and
|
|
160
|
-
//
|
|
161
|
-
// is still deliberately absent.
|
|
169
|
+
// ⚠ `visibilitychange` rather than `pagehide`: it fires while the document is still alive and the
|
|
170
|
+
// bridge can still post, and both mobile shells raise it on the way to the background. ONE report
|
|
171
|
+
// per background, not `timeupdate`'s ~4/second.
|
|
162
172
|
const onHidden = () => {
|
|
163
173
|
if (document.visibilityState !== 'hidden')
|
|
164
174
|
return;
|
|
@@ -178,8 +188,8 @@ export function useMediaPlayer(ref, options = {}) {
|
|
|
178
188
|
/**
|
|
179
189
|
* A short, stable reason from `MediaError`.
|
|
180
190
|
*
|
|
181
|
-
* ⚠
|
|
182
|
-
*
|
|
191
|
+
* ⚠ Never `error.message`: browsers put decoder internals and sometimes the full URL in it, and this
|
|
192
|
+
* string crosses to the host and can reach a log.
|
|
183
193
|
*/
|
|
184
194
|
function mediaErrorReason(element) {
|
|
185
195
|
switch (element.error?.code) {
|
package/dist/moduleService.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { type ShenoraBridge } from './bridge.js';
|
|
2
2
|
/**
|
|
3
|
-
* Base class for typed module services
|
|
4
|
-
* module
|
|
5
|
-
*
|
|
6
|
-
* (`{ [type]: payloadType }`) for compile-time payload checking:
|
|
3
|
+
* Base class for typed module services: each backend module gets one service subclass that binds the
|
|
4
|
+
* module name once and exposes app-typed methods over {@link send}. Bind `TRequests` to the module's
|
|
5
|
+
* request map (`{ [type]: payloadType }`) for compile-time payload checking:
|
|
7
6
|
*
|
|
8
7
|
* ```ts
|
|
9
8
|
* interface NoteRequests { GET_ALL: void; ADD: { title: string } }
|
|
@@ -18,18 +17,11 @@ import { type ShenoraBridge } from './bridge.js';
|
|
|
18
17
|
* inference, so naming the response argument makes `TType` fall back to its DEFAULT — the union of every
|
|
19
18
|
* key — and `payload` collapses to the union of every route's payload. The check silently stops
|
|
20
19
|
* checking: `send<Note>('ADD', { payload: { notAField: 1 } })` compiles clean, while the same call
|
|
21
|
-
* without the type argument is a TS2353. The response is inferred from the
|
|
22
|
-
* type instead, which every method here has anyway. `moduleService.test.ts` pins both halves.
|
|
20
|
+
* without the type argument is a TS2353. The response is inferred from the declared return type instead.
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `TRequests extends object`, NOT `extends Record<string, unknown>` (P5.5 H6). The stricter bound was
|
|
28
|
-
* unsatisfiable by a plain `interface` — interfaces get no implicit index signature — so the example
|
|
29
|
-
* above and the README's snippet both failed with TS2344, on the first line an adopter copies. And
|
|
30
|
-
* satisfying it the way the kit's own `windowCommands.ts` did (`interface X extends Record<string,
|
|
31
|
-
* unknown>`) widened `keyof TRequests & string` back to `string`, so a mistyped request type compiled
|
|
32
|
-
* and every payload collapsed to `unknown` — the flagship typed-service feature checking nothing at all.
|
|
22
|
+
* ⚠ `TRequests extends object`, never `extends Record<string, unknown>`: the stricter bound is
|
|
23
|
+
* unsatisfiable by a plain `interface` (TS2344 on the example above), and satisfying it by widening the
|
|
24
|
+
* request map widens `keyof TRequests & string` back to `string` — turning the payload check off again.
|
|
33
25
|
*/
|
|
34
26
|
export declare abstract class BaseModuleService<TRequests extends object = Record<string, unknown>> {
|
|
35
27
|
/** The backend module this service fronts (e.g. `"NOTES"`). */
|
|
@@ -50,13 +42,10 @@ export declare abstract class BaseModuleService<TRequests extends object = Recor
|
|
|
50
42
|
/**
|
|
51
43
|
* The bridge this service speaks over — resolved on every access, never captured.
|
|
52
44
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* H2). Module services are commonly module-level singletons, so constructing one before the app's
|
|
58
|
-
* startup configuration ran is the normal case, not an edge case. `useDropZone` already resolved
|
|
59
|
-
* lazily for exactly this reason; this matches it.
|
|
45
|
+
* 🔴 Never default it at CONSTRUCTION. Module services are commonly module-level singletons, so one
|
|
46
|
+
* is routinely built before `configureBridge()` runs — and `configureBridge` DISPOSES the bridge it
|
|
47
|
+
* replaces, so a captured default makes every later call reject with "Bridge disposed" for the rest
|
|
48
|
+
* of the session, with nothing to suggest why.
|
|
60
49
|
*/
|
|
61
50
|
protected get bridge(): ShenoraBridge;
|
|
62
51
|
/** Send one typed request to this module and await the typed response data. */
|
package/dist/moduleService.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { getBridge } from './bridge.js';
|
|
2
2
|
/**
|
|
3
|
-
* Base class for typed module services
|
|
4
|
-
* module
|
|
5
|
-
*
|
|
6
|
-
* (`{ [type]: payloadType }`) for compile-time payload checking:
|
|
3
|
+
* Base class for typed module services: each backend module gets one service subclass that binds the
|
|
4
|
+
* module name once and exposes app-typed methods over {@link send}. Bind `TRequests` to the module's
|
|
5
|
+
* request map (`{ [type]: payloadType }`) for compile-time payload checking:
|
|
7
6
|
*
|
|
8
7
|
* ```ts
|
|
9
8
|
* interface NoteRequests { GET_ALL: void; ADD: { title: string } }
|
|
@@ -18,18 +17,11 @@ import { getBridge } from './bridge.js';
|
|
|
18
17
|
* inference, so naming the response argument makes `TType` fall back to its DEFAULT — the union of every
|
|
19
18
|
* key — and `payload` collapses to the union of every route's payload. The check silently stops
|
|
20
19
|
* checking: `send<Note>('ADD', { payload: { notAField: 1 } })` compiles clean, while the same call
|
|
21
|
-
* without the type argument is a TS2353. The response is inferred from the
|
|
22
|
-
* type instead, which every method here has anyway. `moduleService.test.ts` pins both halves.
|
|
20
|
+
* without the type argument is a TS2353. The response is inferred from the declared return type instead.
|
|
23
21
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `TRequests extends object`, NOT `extends Record<string, unknown>` (P5.5 H6). The stricter bound was
|
|
28
|
-
* unsatisfiable by a plain `interface` — interfaces get no implicit index signature — so the example
|
|
29
|
-
* above and the README's snippet both failed with TS2344, on the first line an adopter copies. And
|
|
30
|
-
* satisfying it the way the kit's own `windowCommands.ts` did (`interface X extends Record<string,
|
|
31
|
-
* unknown>`) widened `keyof TRequests & string` back to `string`, so a mistyped request type compiled
|
|
32
|
-
* and every payload collapsed to `unknown` — the flagship typed-service feature checking nothing at all.
|
|
22
|
+
* ⚠ `TRequests extends object`, never `extends Record<string, unknown>`: the stricter bound is
|
|
23
|
+
* unsatisfiable by a plain `interface` (TS2344 on the example above), and satisfying it by widening the
|
|
24
|
+
* request map widens `keyof TRequests & string` back to `string` — turning the payload check off again.
|
|
33
25
|
*/
|
|
34
26
|
export class BaseModuleService {
|
|
35
27
|
constructor(
|
|
@@ -46,13 +38,10 @@ export class BaseModuleService {
|
|
|
46
38
|
/**
|
|
47
39
|
* The bridge this service speaks over — resolved on every access, never captured.
|
|
48
40
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* H2). Module services are commonly module-level singletons, so constructing one before the app's
|
|
54
|
-
* startup configuration ran is the normal case, not an edge case. `useDropZone` already resolved
|
|
55
|
-
* lazily for exactly this reason; this matches it.
|
|
41
|
+
* 🔴 Never default it at CONSTRUCTION. Module services are commonly module-level singletons, so one
|
|
42
|
+
* is routinely built before `configureBridge()` runs — and `configureBridge` DISPOSES the bridge it
|
|
43
|
+
* replaces, so a captured default makes every later call reject with "Bridge disposed" for the rest
|
|
44
|
+
* of the session, with nothing to suggest why.
|
|
56
45
|
*/
|
|
57
46
|
get bridge() {
|
|
58
47
|
return this.explicitBridge ?? getBridge();
|