@shenora/react 0.11.0 → 0.13.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 +65 -42
- package/dist/segmentStream.d.ts +43 -54
- package/dist/segmentStream.js +102 -70
- package/dist/store.d.ts +15 -26
- package/dist/store.js +63 -59
- 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/segmentStream.d.ts
CHANGED
|
@@ -2,17 +2,25 @@
|
|
|
2
2
|
* The page half of the host's segment route (D71 piece 4) — reading the manifest it serves, choosing the
|
|
3
3
|
* MediaSource this browser actually has, and deciding what to fetch next.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
* Everything here is PURE: the decisions live in this module and `segmentBinder.ts` holds the
|
|
6
|
+
* imperative half — creating a `SourceBuffer`, appending bytes, listening to element events.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The reserved path segment naming a source by the handle the HOST issued for it:
|
|
10
|
+
* `{routePath}${SEGMENT_REMOTE_PREFIX}{handle}/index.m3u8`.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* whether a real implementation accepts the bytes; that is measured on hardware and recorded in
|
|
14
|
-
* `docs/design/media.md`.
|
|
12
|
+
* 🔴 **A page cannot name a remote url, only a handle.** The host's `MediaSourceRegistry` issues one when
|
|
13
|
+
* the app authorises a source, and the route accepts nothing else. Mirrors
|
|
14
|
+
* `SegmentStreamOptions.RemotePrefix`.
|
|
15
15
|
*/
|
|
16
|
+
export declare const SEGMENT_REMOTE_PREFIX = "~remote/";
|
|
17
|
+
/**
|
|
18
|
+
* The manifest url for a handle the app handed this page.
|
|
19
|
+
*
|
|
20
|
+
* ⚠ The handle is opaque and is NOT a url — do not build one from it, and do not log it beside anything
|
|
21
|
+
* that identifies the user. It is a capability: whoever holds it can stream that source.
|
|
22
|
+
*/
|
|
23
|
+
export declare function remoteSegmentUrl(routePath: string, handle: string, resource?: string): string;
|
|
16
24
|
/** One entry in the playlist the host serves. */
|
|
17
25
|
export interface SegmentEntry {
|
|
18
26
|
/** Relative to the manifest — `seg12.m4s`. */
|
|
@@ -26,8 +34,8 @@ export interface SegmentManifest {
|
|
|
26
34
|
* The initialisation segment (`#EXT-X-MAP`), which carries the tracks and their decoder configuration.
|
|
27
35
|
*
|
|
28
36
|
* ⚠ **Null means the playlist declared none, and that is not playable through MediaSource** — a fragment
|
|
29
|
-
* repeats no configuration, so appending one without this
|
|
30
|
-
*
|
|
37
|
+
* repeats no configuration, so appending one without this is a silent decode error. The kit's host route
|
|
38
|
+
* always writes it; a foreign playlist may not.
|
|
31
39
|
*/
|
|
32
40
|
initUri: string | null;
|
|
33
41
|
/** The longest a segment may be, from `#EXT-X-TARGETDURATION`. */
|
|
@@ -35,11 +43,9 @@ export interface SegmentManifest {
|
|
|
35
43
|
segments: SegmentEntry[];
|
|
36
44
|
}
|
|
37
45
|
/**
|
|
38
|
-
* Parse the subset of HLS the host emits
|
|
39
|
-
* `SegmentStream` writes, and anything it does not understand is ignored rather than guessed at.
|
|
46
|
+
* Parse the subset of HLS the host's `SegmentStream` emits — not a general playlist parser.
|
|
40
47
|
*
|
|
41
|
-
* ⚠
|
|
42
|
-
* on the first one it had not met would break on a host newer than the page.
|
|
48
|
+
* ⚠ An unknown tag is SKIPPED, never an error, so a host newer than the page still parses.
|
|
43
49
|
*/
|
|
44
50
|
export declare function parseManifest(text: string): SegmentManifest;
|
|
45
51
|
/** Which MediaSource implementation this browser has, if any. */
|
|
@@ -52,20 +58,13 @@ export interface MediaSourceGlobals {
|
|
|
52
58
|
/**
|
|
53
59
|
* Which MediaSource to use — **`ManagedMediaSource` first where it exists**.
|
|
54
60
|
*
|
|
55
|
-
* 🔴 **
|
|
56
|
-
* `
|
|
57
|
-
*
|
|
58
|
-
* regardless is what the managed variant was introduced to stop.
|
|
59
|
-
*
|
|
60
|
-
* ✅ **Measured rather than assumed** (iPhone 16 Pro simulator, iOS 26, 2026-08-14): `window.MediaSource` is
|
|
61
|
-
* `false` there and `ManagedMediaSource` is `true`. So on iOS this is not a preference at all — **a binder
|
|
62
|
-
* that only knows `window.MediaSource` does nothing**, and the naming here is what makes one bundle work on
|
|
63
|
-
* both shells.
|
|
61
|
+
* 🔴 **iOS has ONLY `ManagedMediaSource` and Android has ONLY `MediaSource`**, so a binder that knows
|
|
62
|
+
* just `window.MediaSource` does nothing at all on iOS. Where both exist the managed one still wins:
|
|
63
|
+
* it is the one that says when the platform actually wants data. (Measured — `docs/design/media.md`.)
|
|
64
64
|
*
|
|
65
|
-
* ⚠ **`'managed'` carries an obligation, which is why this returns a KIND
|
|
66
|
-
*
|
|
67
|
-
* outside that window is the
|
|
68
|
-
* missed an optimisation.
|
|
65
|
+
* ⚠ **`'managed'` carries an obligation, which is why this returns a KIND and not a constructor.** A
|
|
66
|
+
* managed source only wants data between its `startstreaming` and `endstreaming` events, and fetching
|
|
67
|
+
* outside that window is the misuse it exists to detect.
|
|
69
68
|
*/
|
|
70
69
|
export declare function pickMediaSource(globals: MediaSourceGlobals): MediaSourceKind;
|
|
71
70
|
/** What {@link nextSegment} needs to know about the element and the buffer. */
|
|
@@ -88,29 +87,25 @@ export interface FetchPolicy {
|
|
|
88
87
|
/**
|
|
89
88
|
* The next segment index to fetch, or null for "nothing right now".
|
|
90
89
|
*
|
|
91
|
-
* 🔴 **Every branch
|
|
92
|
-
* function with a test rather than an `if` inside an event handler:
|
|
90
|
+
* 🔴 **Every branch is a decision whose failure is SILENT in a browser:**
|
|
93
91
|
*
|
|
94
|
-
* - **Not streaming → null.**
|
|
95
|
-
*
|
|
96
|
-
* - **Enough buffered → null.** Fetching further ahead
|
|
97
|
-
*
|
|
98
|
-
* - **Otherwise the segment CONTAINING `currentTime`, or the first unappended one after it
|
|
99
|
-
* "the next index after the last
|
|
100
|
-
* append is nowhere near where the user is now.
|
|
92
|
+
* - **Not streaming → null.** On iOS the penalty for fetching past `endstreaming` is the platform
|
|
93
|
+
* tearing the source down.
|
|
94
|
+
* - **Enough buffered → null.** Fetching further ahead fills a quota, and a `QuotaExceededError` on
|
|
95
|
+
* append arrives as a stall with no obvious cause.
|
|
96
|
+
* - **Otherwise the segment CONTAINING `currentTime`, or the first unappended one after it** — never
|
|
97
|
+
* "the next index after the last append", which breaks seeking.
|
|
101
98
|
*/
|
|
102
99
|
export declare function nextSegment(state: FetchState, policy: FetchPolicy): number | null;
|
|
103
100
|
/**
|
|
104
|
-
* The MIME type to open a `SourceBuffer` with.
|
|
101
|
+
* The MIME type to open a `SourceBuffer` with. The default is H.264 High 4.0 plus AAC-LC.
|
|
105
102
|
*
|
|
106
103
|
* ⚠ **The codecs parameter is REQUIRED, not decorative.** `addSourceBuffer('video/mp4')` throws
|
|
107
|
-
* `NotSupportedError` on every implementation — the buffer has to know what it is about to be fed
|
|
108
|
-
* init segment arrives.
|
|
104
|
+
* `NotSupportedError` on every implementation — the buffer has to know what it is about to be fed
|
|
105
|
+
* before the init segment arrives.
|
|
109
106
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* HEVC source arrives as `hvc1` and not `avc1` at all. The family is what an implementation actually checks,
|
|
113
|
-
* so an H.264 source of any profile plays through the default — **an HEVC one needs its own string**, e.g.
|
|
107
|
+
* ⚠ The default is not a guarantee: the host copies whatever the source already holds (D76), so an
|
|
108
|
+
* **HEVC source arrives as `hvc1` and needs its own string**, e.g.
|
|
114
109
|
* `segmentMimeType('hvc1.1.6.L93.B0,mp4a.40.2')`.
|
|
115
110
|
*/
|
|
116
111
|
export declare function segmentMimeType(codecs?: string): string;
|
|
@@ -118,16 +113,10 @@ export declare function segmentMimeType(codecs?: string): string;
|
|
|
118
113
|
* Read the codecs parameter out of an initialisation segment, so the `SourceBuffer` is opened for the
|
|
119
114
|
* tracks it will actually be fed.
|
|
120
115
|
*
|
|
121
|
-
* 🔴 **The TRACK SET
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* soundtrack is ordinary, so a fixed default cannot serve both.
|
|
126
|
-
*
|
|
127
|
-
* ⚠ **The profile and level, by contrast, are barely checked.** The same measurement fed High 2.1
|
|
128
|
-
* content to a buffer opened as Baseline 3.0 (`avc1.42E01E`) and it played. That is why this returns a
|
|
129
|
-
* precise string when the configuration is there to read and a family default when it is not: precision
|
|
130
|
-
* where it is free, and never a guess about which tracks exist.
|
|
116
|
+
* 🔴 **The TRACK SET must be right, and getting it wrong is fatal rather than degraded**: a video-only
|
|
117
|
+
* init segment appended to a buffer opened with the two-track default fails the FIRST append and plays
|
|
118
|
+
* nothing. A source with no soundtrack is ordinary, so no fixed default serves both. (The profile and
|
|
119
|
+
* level, by contrast, are barely checked — `docs/design/media.md` has the measurements.)
|
|
131
120
|
*
|
|
132
121
|
* @param init The bytes of the `#EXT-X-MAP` segment.
|
|
133
122
|
* @returns A codecs string for {@link segmentMimeType}, or null when no track could be read — the caller
|
package/dist/segmentStream.js
CHANGED
|
@@ -2,23 +2,32 @@
|
|
|
2
2
|
* The page half of the host's segment route (D71 piece 4) — reading the manifest it serves, choosing the
|
|
3
3
|
* MediaSource this browser actually has, and deciding what to fetch next.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
* Everything here is PURE: the decisions live in this module and `segmentBinder.ts` holds the
|
|
6
|
+
* imperative half — creating a `SourceBuffer`, appending bytes, listening to element events.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The reserved path segment naming a source by the handle the HOST issued for it:
|
|
10
|
+
* `{routePath}${SEGMENT_REMOTE_PREFIX}{handle}/index.m3u8`.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* whether a real implementation accepts the bytes; that is measured on hardware and recorded in
|
|
14
|
-
* `docs/design/media.md`.
|
|
12
|
+
* 🔴 **A page cannot name a remote url, only a handle.** The host's `MediaSourceRegistry` issues one when
|
|
13
|
+
* the app authorises a source, and the route accepts nothing else. Mirrors
|
|
14
|
+
* `SegmentStreamOptions.RemotePrefix`.
|
|
15
15
|
*/
|
|
16
|
+
export const SEGMENT_REMOTE_PREFIX = '~remote/';
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
-
* `SegmentStream` writes, and anything it does not understand is ignored rather than guessed at.
|
|
18
|
+
* The manifest url for a handle the app handed this page.
|
|
19
19
|
*
|
|
20
|
-
* ⚠
|
|
21
|
-
*
|
|
20
|
+
* ⚠ The handle is opaque and is NOT a url — do not build one from it, and do not log it beside anything
|
|
21
|
+
* that identifies the user. It is a capability: whoever holds it can stream that source.
|
|
22
|
+
*/
|
|
23
|
+
export function remoteSegmentUrl(routePath, handle, resource = 'index.m3u8') {
|
|
24
|
+
const base = routePath.endsWith('/') ? routePath : `${routePath}/`;
|
|
25
|
+
return `${base}${SEGMENT_REMOTE_PREFIX}${encodeURIComponent(handle)}/${resource}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse the subset of HLS the host's `SegmentStream` emits — not a general playlist parser.
|
|
29
|
+
*
|
|
30
|
+
* ⚠ An unknown tag is SKIPPED, never an error, so a host newer than the page still parses.
|
|
22
31
|
*/
|
|
23
32
|
export function parseManifest(text) {
|
|
24
33
|
const segments = [];
|
|
@@ -54,20 +63,13 @@ export function parseManifest(text) {
|
|
|
54
63
|
/**
|
|
55
64
|
* Which MediaSource to use — **`ManagedMediaSource` first where it exists**.
|
|
56
65
|
*
|
|
57
|
-
* 🔴 **
|
|
58
|
-
* `
|
|
59
|
-
*
|
|
60
|
-
* regardless is what the managed variant was introduced to stop.
|
|
61
|
-
*
|
|
62
|
-
* ✅ **Measured rather than assumed** (iPhone 16 Pro simulator, iOS 26, 2026-08-14): `window.MediaSource` is
|
|
63
|
-
* `false` there and `ManagedMediaSource` is `true`. So on iOS this is not a preference at all — **a binder
|
|
64
|
-
* that only knows `window.MediaSource` does nothing**, and the naming here is what makes one bundle work on
|
|
65
|
-
* both shells.
|
|
66
|
+
* 🔴 **iOS has ONLY `ManagedMediaSource` and Android has ONLY `MediaSource`**, so a binder that knows
|
|
67
|
+
* just `window.MediaSource` does nothing at all on iOS. Where both exist the managed one still wins:
|
|
68
|
+
* it is the one that says when the platform actually wants data. (Measured — `docs/design/media.md`.)
|
|
66
69
|
*
|
|
67
|
-
* ⚠ **`'managed'` carries an obligation, which is why this returns a KIND
|
|
68
|
-
*
|
|
69
|
-
* outside that window is the
|
|
70
|
-
* missed an optimisation.
|
|
70
|
+
* ⚠ **`'managed'` carries an obligation, which is why this returns a KIND and not a constructor.** A
|
|
71
|
+
* managed source only wants data between its `startstreaming` and `endstreaming` events, and fetching
|
|
72
|
+
* outside that window is the misuse it exists to detect.
|
|
71
73
|
*/
|
|
72
74
|
export function pickMediaSource(globals) {
|
|
73
75
|
if (typeof globals.ManagedMediaSource === 'function')
|
|
@@ -79,16 +81,14 @@ export function pickMediaSource(globals) {
|
|
|
79
81
|
/**
|
|
80
82
|
* The next segment index to fetch, or null for "nothing right now".
|
|
81
83
|
*
|
|
82
|
-
* 🔴 **Every branch
|
|
83
|
-
* function with a test rather than an `if` inside an event handler:
|
|
84
|
+
* 🔴 **Every branch is a decision whose failure is SILENT in a browser:**
|
|
84
85
|
*
|
|
85
|
-
* - **Not streaming → null.**
|
|
86
|
-
*
|
|
87
|
-
* - **Enough buffered → null.** Fetching further ahead
|
|
88
|
-
*
|
|
89
|
-
* - **Otherwise the segment CONTAINING `currentTime`, or the first unappended one after it
|
|
90
|
-
* "the next index after the last
|
|
91
|
-
* append is nowhere near where the user is now.
|
|
86
|
+
* - **Not streaming → null.** On iOS the penalty for fetching past `endstreaming` is the platform
|
|
87
|
+
* tearing the source down.
|
|
88
|
+
* - **Enough buffered → null.** Fetching further ahead fills a quota, and a `QuotaExceededError` on
|
|
89
|
+
* append arrives as a stall with no obvious cause.
|
|
90
|
+
* - **Otherwise the segment CONTAINING `currentTime`, or the first unappended one after it** — never
|
|
91
|
+
* "the next index after the last append", which breaks seeking.
|
|
92
92
|
*/
|
|
93
93
|
export function nextSegment(state, policy) {
|
|
94
94
|
if (!state.streaming)
|
|
@@ -97,8 +97,8 @@ export function nextSegment(state, policy) {
|
|
|
97
97
|
return null;
|
|
98
98
|
if (policy.segments.length === 0)
|
|
99
99
|
return null;
|
|
100
|
-
// Walk the playlist's own durations rather than assuming a fixed grid: the LAST segment is short,
|
|
101
|
-
// dividing by the target duration puts the tail index past the end.
|
|
100
|
+
// Walk the playlist's own durations rather than assuming a fixed grid: the LAST segment is short,
|
|
101
|
+
// so dividing by the target duration puts the tail index past the end.
|
|
102
102
|
let at = 0;
|
|
103
103
|
let index = 0;
|
|
104
104
|
for (; index < policy.segments.length; index++) {
|
|
@@ -114,16 +114,14 @@ export function nextSegment(state, policy) {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
* The MIME type to open a `SourceBuffer` with.
|
|
117
|
+
* The MIME type to open a `SourceBuffer` with. The default is H.264 High 4.0 plus AAC-LC.
|
|
118
118
|
*
|
|
119
119
|
* ⚠ **The codecs parameter is REQUIRED, not decorative.** `addSourceBuffer('video/mp4')` throws
|
|
120
|
-
* `NotSupportedError` on every implementation — the buffer has to know what it is about to be fed
|
|
121
|
-
* init segment arrives.
|
|
120
|
+
* `NotSupportedError` on every implementation — the buffer has to know what it is about to be fed
|
|
121
|
+
* before the init segment arrives.
|
|
122
122
|
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* HEVC source arrives as `hvc1` and not `avc1` at all. The family is what an implementation actually checks,
|
|
126
|
-
* so an H.264 source of any profile plays through the default — **an HEVC one needs its own string**, e.g.
|
|
123
|
+
* ⚠ The default is not a guarantee: the host copies whatever the source already holds (D76), so an
|
|
124
|
+
* **HEVC source arrives as `hvc1` and needs its own string**, e.g.
|
|
127
125
|
* `segmentMimeType('hvc1.1.6.L93.B0,mp4a.40.2')`.
|
|
128
126
|
*/
|
|
129
127
|
export function segmentMimeType(codecs = 'avc1.640028,mp4a.40.2') {
|
|
@@ -133,16 +131,10 @@ export function segmentMimeType(codecs = 'avc1.640028,mp4a.40.2') {
|
|
|
133
131
|
* Read the codecs parameter out of an initialisation segment, so the `SourceBuffer` is opened for the
|
|
134
132
|
* tracks it will actually be fed.
|
|
135
133
|
*
|
|
136
|
-
* 🔴 **The TRACK SET
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* soundtrack is ordinary, so a fixed default cannot serve both.
|
|
141
|
-
*
|
|
142
|
-
* ⚠ **The profile and level, by contrast, are barely checked.** The same measurement fed High 2.1
|
|
143
|
-
* content to a buffer opened as Baseline 3.0 (`avc1.42E01E`) and it played. That is why this returns a
|
|
144
|
-
* precise string when the configuration is there to read and a family default when it is not: precision
|
|
145
|
-
* where it is free, and never a guess about which tracks exist.
|
|
134
|
+
* 🔴 **The TRACK SET must be right, and getting it wrong is fatal rather than degraded**: a video-only
|
|
135
|
+
* init segment appended to a buffer opened with the two-track default fails the FIRST append and plays
|
|
136
|
+
* nothing. A source with no soundtrack is ordinary, so no fixed default serves both. (The profile and
|
|
137
|
+
* level, by contrast, are barely checked — `docs/design/media.md` has the measurements.)
|
|
146
138
|
*
|
|
147
139
|
* @param init The bytes of the `#EXT-X-MAP` segment.
|
|
148
140
|
* @returns A codecs string for {@link segmentMimeType}, or null when no track could be read — the caller
|
|
@@ -151,8 +143,8 @@ export function segmentMimeType(codecs = 'avc1.640028,mp4a.40.2') {
|
|
|
151
143
|
export function codecsFromInitSegment(init) {
|
|
152
144
|
const view = new DataView(init.buffer, init.byteOffset, init.byteLength);
|
|
153
145
|
const codecs = [];
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
146
|
+
/** Read through the view, not by index: a `!` on every `init[i]` would hide the one that IS out of
|
|
147
|
+
* range, where `getUint8` throws. */
|
|
156
148
|
const u8 = (at) => view.getUint8(at);
|
|
157
149
|
const fourcc = (at) => String.fromCharCode(u8(at), u8(at + 1), u8(at + 2), u8(at + 3));
|
|
158
150
|
const hex2 = (n) => n.toString(16).padStart(2, '0');
|
|
@@ -176,11 +168,52 @@ export function codecsFromInitSegment(init) {
|
|
|
176
168
|
at += size;
|
|
177
169
|
}
|
|
178
170
|
};
|
|
171
|
+
/**
|
|
172
|
+
* An ISO 14496-1 "expandable" descriptor length: 1–4 bytes, each with the high bit meaning "continue".
|
|
173
|
+
* Returns the value and the offset just past it.
|
|
174
|
+
*/
|
|
175
|
+
const expandable = (at) => {
|
|
176
|
+
let value = 0;
|
|
177
|
+
let p = at;
|
|
178
|
+
for (let i = 0; i < 4; i++) {
|
|
179
|
+
const b = u8(p++);
|
|
180
|
+
value = (value << 7) | (b & 0x7f);
|
|
181
|
+
if ((b & 0x80) === 0)
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
return [value, p];
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* The AAC audio object type declared inside an `esds`, or null when it cannot be read.
|
|
188
|
+
*
|
|
189
|
+
* Tolerant by design: it finds the DecoderConfigDescriptor rather than parsing the ES_Descriptor's
|
|
190
|
+
* optional fields, then steps its FIXED 13 bytes to the nested DecoderSpecificInfo, whose first 5 bits
|
|
191
|
+
* are the object type (2 = AAC-LC, 5 = HE-AAC, 29 = HE-AACv2, 42 = xHE-AAC).
|
|
192
|
+
*/
|
|
193
|
+
const audioObjectType = (from, to) => {
|
|
194
|
+
for (let p = from; p + 2 < to; p++) {
|
|
195
|
+
if (u8(p) !== 0x04)
|
|
196
|
+
continue; // DecoderConfigDescriptor
|
|
197
|
+
const [, afterLength] = expandable(p + 1);
|
|
198
|
+
if (afterLength >= to || u8(afterLength) !== 0x40)
|
|
199
|
+
continue; // MPEG-4 Audio, or not ours
|
|
200
|
+
// objectTypeIndication(1) streamType(1) bufferSizeDB(3) maxBitrate(4) avgBitrate(4)
|
|
201
|
+
const nested = afterLength + 13;
|
|
202
|
+
if (nested + 1 >= to || u8(nested) !== 0x05)
|
|
203
|
+
continue; // DecoderSpecificInfo
|
|
204
|
+
const [, config] = expandable(nested + 1);
|
|
205
|
+
if (config >= to)
|
|
206
|
+
continue;
|
|
207
|
+
const aot = u8(config) >> 3;
|
|
208
|
+
// 31 is the escape for an extended type; not worth decoding for a codec string, and 0 is invalid.
|
|
209
|
+
return aot === 0 || aot === 31 ? null : aot;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
179
213
|
/** The `avcC`/`hvcC`/`esds` inside a sample entry, whose own fields come first. */
|
|
180
214
|
const configuration = (format, start, end) => {
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
// reference index) plus VisualSampleEntry's 70. An `mp4a` entry is that base plus 20.
|
|
215
|
+
// The child boxes start after the sample entry's own fields: the 8-byte SampleEntry base
|
|
216
|
+
// (6 reserved + a data reference index) plus VisualSampleEntry's 70, or plus 20 for `mp4a`.
|
|
184
217
|
const visual = format === 'avc1' || format === 'avc3' || format === 'hvc1' || format === 'hev1';
|
|
185
218
|
const at = start + (visual ? 78 : 28);
|
|
186
219
|
let derived = '';
|
|
@@ -205,17 +238,16 @@ export function codecsFromInitSegment(init) {
|
|
|
205
238
|
derived = `${format}.${spaces}${idc}.${(reversed >>> 0).toString(16).toUpperCase()}.` +
|
|
206
239
|
`${tier ? 'H' : 'L'}${level}`;
|
|
207
240
|
}
|
|
208
|
-
else if (type === 'esds' && s +
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
derived = 'mp4a.40.2';
|
|
241
|
+
else if (type === 'esds' && s + 8 <= end) {
|
|
242
|
+
// 🔴 THE AUDIO OBJECT TYPE LIVES IN THE DecoderSpecificInfo, not beside objectTypeIndication.
|
|
243
|
+
// This used to read the byte straight after the `0x40`, which is the STREAM TYPE — for audio
|
|
244
|
+
// that is `(5 << 2) | 1` = 0x15, so every short-form esds produced `mp4a.40.21`,
|
|
245
|
+
// `addSourceBuffer` threw, and nothing played. It also assumed a ONE-byte descriptor length; the
|
|
246
|
+
// length is "expandable" (up to 4 bytes, high bit continues). The kit's own muxer always writes
|
|
247
|
+
// the 4-byte form, so the old scan never matched OUR files and the fallback hid it — the bug was
|
|
248
|
+
// reachable only from a foreign muxer, which is exactly what this parser is for.
|
|
249
|
+
const aot = audioObjectType(s, end);
|
|
250
|
+
derived = `mp4a.40.${aot ?? 2}`;
|
|
219
251
|
}
|
|
220
252
|
});
|
|
221
253
|
if (derived)
|
package/dist/store.d.ts
CHANGED
|
@@ -17,12 +17,9 @@ export interface ShenoraStoreIo<TState = unknown> {
|
|
|
17
17
|
* action can fully decide by itself. The host stays authoritative for everything a `snapshot`/`on`
|
|
18
18
|
* reducer covers; this is for state the ACTION already knows and the host would only echo back.
|
|
19
19
|
*
|
|
20
|
-
* 🔴 **Reach for it only when the host has no event to tell you.**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* gone, able to disagree with the host about it. If the host emits an event for the change, let the
|
|
24
|
-
* reducer own it — an optimistic path beside a wire event is a divergence waiting to happen, not a
|
|
25
|
-
* latency win.
|
|
20
|
+
* 🔴 **Reach for it only when the host has no event to tell you.** If one exists, let the reducer
|
|
21
|
+
* own it: an optimistic path beside a wire event is a second thing deciding the same state, and it
|
|
22
|
+
* can disagree with the host.
|
|
26
23
|
*/
|
|
27
24
|
setState: (reduce: (state: TState) => TState) => void;
|
|
28
25
|
}
|
|
@@ -34,15 +31,16 @@ export interface ShenoraStoreSnapshot<TState> {
|
|
|
34
31
|
/** Fold the response into state. */
|
|
35
32
|
apply: (state: TState, data: unknown) => TState;
|
|
36
33
|
}
|
|
34
|
+
/** Inputs for {@link createShenoraStore}. */
|
|
37
35
|
export interface ShenoraStoreOptions<TState, TActions> {
|
|
38
36
|
/** State before anything has arrived. */
|
|
39
37
|
initial: TState;
|
|
40
38
|
/**
|
|
41
39
|
* Load the CURRENT state when the first component subscribes.
|
|
42
40
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* ⚠ Optional in the type only. A component that mounts while work is already in flight has MISSED
|
|
42
|
+
* those events and a stream cannot be replayed, so deltas alone work silently only for whoever was
|
|
43
|
+
* watching from the start.
|
|
46
44
|
*/
|
|
47
45
|
snapshot?: ShenoraStoreSnapshot<TState>;
|
|
48
46
|
/** Event type (within this module) → PURE reducer over state. */
|
|
@@ -75,28 +73,19 @@ export interface ShenoraStore<TState, TActions> {
|
|
|
75
73
|
reset: () => void;
|
|
76
74
|
}
|
|
77
75
|
/**
|
|
78
|
-
* A store fed by one module's host event stream, shared by every component that reads it
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* existed here — see `.claude/knowledge/generic-library.md`'s worked example for that survey. It exists
|
|
82
|
-
* because status- and progress-driven UI is inherently MANY-WATCHERS: a full panel and a compact
|
|
83
|
-
* progress strip want the same live state, and without a shared store each re-implements the wiring,
|
|
84
|
-
* each opens its own subscription, and each starts empty.
|
|
76
|
+
* A store fed by one module's host event stream, shared by every component that reads it — for the
|
|
77
|
+
* status- and progress-driven UI that is inherently many-watchers, where a full panel and a compact
|
|
78
|
+
* progress strip want the same live state.
|
|
85
79
|
*
|
|
86
80
|
* What it guarantees:
|
|
87
81
|
* - **One subscription per event type, however many components read it.** Mounting N components does
|
|
88
82
|
* not open N subscriptions; unmounting the last one tears them down.
|
|
89
83
|
* - **A late mounter sees current state**, via {@link ShenoraStoreOptions.snapshot} on the first
|
|
90
|
-
* subscription.
|
|
91
|
-
* - **No state library.** Built on React's `useSyncExternalStore`,
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* - **Reducers are PURE and isolated**: they take state + payload and return state, so a store is
|
|
96
|
-
* testable with no bridge, and a throwing reducer is reported rather than corrupting shared state.
|
|
97
|
-
*
|
|
98
|
-
* Headless (D13/D21): the kit ships the MECHANISM. What an operation is — its phases, its progress
|
|
99
|
-
* shape, whether it queues — stays in the app; there is deliberately no job/queue/progress type here.
|
|
84
|
+
* subscription.
|
|
85
|
+
* - **No state library.** Built on React's `useSyncExternalStore`, so it is tearing-free under
|
|
86
|
+
* concurrent rendering and imposes no store dependency on the app.
|
|
87
|
+
* - **Reducers are PURE and isolated**: state + payload in, state out, so a store is testable with no
|
|
88
|
+
* bridge and a throwing reducer is reported rather than corrupting shared state.
|
|
100
89
|
*
|
|
101
90
|
* @example
|
|
102
91
|
* const useDeploy = createShenoraStore('DEPLOY', {
|