reactor-effect-browser 0.2.0 → 0.3.0-rc.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/README.md +12 -5
- package/dist/_internal/audio-packet.d.ts.map +1 -1
- package/dist/_internal/audio-packet.js +7 -7
- package/dist/_internal/audio-packet.js.map +1 -1
- package/dist/_internal/audio.d.ts +27 -7
- package/dist/_internal/audio.d.ts.map +1 -1
- package/dist/_internal/audio.js +43 -35
- package/dist/_internal/audio.js.map +1 -1
- package/dist/_internal/media.d.ts +17 -3
- package/dist/_internal/media.d.ts.map +1 -1
- package/dist/_internal/media.js +45 -40
- package/dist/_internal/media.js.map +1 -1
- package/dist/_internal/peer.d.ts +1 -1
- package/dist/_internal/peer.d.ts.map +1 -1
- package/dist/_internal/peer.js +24 -24
- package/dist/_internal/peer.js.map +1 -1
- package/dist/_internal/recording.d.ts +13 -21
- package/dist/_internal/recording.d.ts.map +1 -1
- package/dist/_internal/recording.js +8 -109
- package/dist/_internal/recording.js.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -10
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -1,110 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
let init;
|
|
10
|
-
const media = [];
|
|
11
|
-
for (const line of text
|
|
12
|
-
.split(/\r?\n/)
|
|
13
|
-
.map((s) => s.trim())
|
|
14
|
-
.filter(Boolean)) {
|
|
15
|
-
if (/^#EXT-X-(STREAM-INF|I-FRAME-STREAM-INF|BYTERANGE|DISCONTINUITY|PART|PRELOAD-HINT|SKIP|GAP|SESSION-KEY|DEFINE)(:|$)/.test(line))
|
|
16
|
-
throw new ReactorError("UnsupportedCapability", "master, byte-range, discontinuous, encrypted, gap, variable, or low-latency playlists require an HLS player, not byte concatenation");
|
|
17
|
-
if (line.startsWith("#EXT-X-KEY:") && !/(?:^|,)METHOD=NONE(?:,|$)/.test(line.slice(11)))
|
|
18
|
-
throw new ReactorError("UnsupportedCapability", "encrypted clips are not assembled by this client");
|
|
19
|
-
if (line.startsWith("#EXT-X-MAP:")) {
|
|
20
|
-
if (line.includes("BYTERANGE="))
|
|
21
|
-
throw new ReactorError("UnsupportedCapability", "byte-range init segments unsupported");
|
|
22
|
-
const match = /(?:^|,)URI="([^"]+)"/.exec(line.slice(11)), uri = match?.[1];
|
|
23
|
-
if (uri === undefined || (init !== undefined && init !== uri))
|
|
24
|
-
throw new ReactorError("Protocol", "missing or changing HLS init URI");
|
|
25
|
-
init = uri;
|
|
26
|
-
}
|
|
27
|
-
else if (!line.startsWith("#")) {
|
|
28
|
-
if (media.length >= maxSegments)
|
|
29
|
-
throw new ReactorError("Overflow", "clip segment count bound exceeded");
|
|
30
|
-
media.push(line);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (media.length === 0)
|
|
34
|
-
throw new ReactorError("Protocol", "empty clip playlist");
|
|
35
|
-
const resolve = (uri) => {
|
|
36
|
-
const url = new URL(uri, baseUrl);
|
|
37
|
-
if (!(url.protocol === "http:" || url.protocol === "https:") || url.username || url.password)
|
|
38
|
-
throw new ReactorError("Protocol", "clip URL must use http(s) with no embedded credentials");
|
|
39
|
-
return url.href;
|
|
40
|
-
};
|
|
41
|
-
if (init === undefined &&
|
|
42
|
-
media.some((uri) => /\.(?:m4s|mp4|m4a|m4v)$/i.test(new URL(uri, baseUrl).pathname)))
|
|
43
|
-
throw new ReactorError("Protocol", "fragmented MP4 playlist lacks EXT-X-MAP init segment");
|
|
44
|
-
const segments = init === undefined ? [] : [{ kind: "init", url: resolve(init) }];
|
|
45
|
-
for (const uri of media)
|
|
46
|
-
segments.push({ kind: "media", url: resolve(uri) });
|
|
47
|
-
return Object.freeze(segments.map((s) => Object.freeze(s)));
|
|
48
|
-
};
|
|
49
|
-
/** Bounded transport of a prepared recording. Does not decode, play, or assert further generation.
|
|
50
|
-
* This API intentionally uses a caller wall deadline, not an unbounded wait based on a prediction. */
|
|
51
|
-
export const downloadClip = (http, clip, options = {}) => Effect.suspend(() => {
|
|
52
|
-
const action = Effect.gen(function* () {
|
|
53
|
-
const bounds = yield* pure(() => ({
|
|
54
|
-
timeout: positiveLimit(options.timeoutMs ?? 60_000, "clip timeout", 600_000),
|
|
55
|
-
manifest: positiveLimit(options.maxManifestBytes ?? 262_144, "manifest bytes", 2_097_152),
|
|
56
|
-
segment: positiveLimit(options.maxSegmentBytes ?? 16_777_216, "segment bytes", 64 * 1024 * 1024),
|
|
57
|
-
total: positiveLimit(options.maxTotalBytes ?? 64 * 1024 * 1024, "clip bytes", 64 * 1024 * 1024),
|
|
58
|
-
count: positiveLimit(options.maxSegments ?? 1024, "clip segments", 16_384),
|
|
59
|
-
}));
|
|
60
|
-
const fetchAndAssemble = Effect.gen(function* () {
|
|
61
|
-
let segments;
|
|
62
|
-
while (true) {
|
|
63
|
-
const response = yield* http.request({
|
|
64
|
-
operation: "clip playlist",
|
|
65
|
-
url: clip.playlist_url,
|
|
66
|
-
auth: "same-origin",
|
|
67
|
-
maxBytes: bounds.manifest,
|
|
68
|
-
});
|
|
69
|
-
if (response.status !== 202) {
|
|
70
|
-
segments = yield* pure(() => parsePlaylist(new TextDecoder("utf-8", { fatal: true }).decode(response.bytes), clip.playlist_url, bounds.count));
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
// A local peer disconnection does not prove stopped inference. Query the remote descriptor.
|
|
74
|
-
const descriptor = yield* http.read(clip.session_id);
|
|
75
|
-
if (terminal(descriptor.state))
|
|
76
|
-
return yield* Effect.fail(new ReactorError("TerminalSession", "session terminated before playlist became available"));
|
|
77
|
-
yield* Effect.sleep(Math.max(200, Math.min(retryAfterMs(response.headers) ?? 2000, 2000)));
|
|
78
|
-
}
|
|
79
|
-
let size = 0;
|
|
80
|
-
const chunks = [];
|
|
81
|
-
for (const segment of segments) {
|
|
82
|
-
const response = yield* http.request({
|
|
83
|
-
operation: "clip segment",
|
|
84
|
-
url: segment.url,
|
|
85
|
-
auth: "same-origin",
|
|
86
|
-
maxBytes: Math.min(bounds.segment, bounds.total - size || 1),
|
|
87
|
-
});
|
|
88
|
-
if (response.status === 202)
|
|
89
|
-
return yield* Effect.fail(new ReactorError("Protocol", "playlist referenced a segment that is not ready"));
|
|
90
|
-
size += response.bytes.length;
|
|
91
|
-
if (size > bounds.total)
|
|
92
|
-
return yield* Effect.fail(new ReactorError("Overflow", "assembled clip byte bound exceeded"));
|
|
93
|
-
chunks.push(response.bytes);
|
|
94
|
-
}
|
|
95
|
-
const bytes = new Uint8Array(size);
|
|
96
|
-
let at = 0;
|
|
97
|
-
for (const chunk of chunks) {
|
|
98
|
-
bytes.set(chunk, at);
|
|
99
|
-
at += chunk.length;
|
|
100
|
-
}
|
|
101
|
-
return { bytes, segments };
|
|
102
|
-
});
|
|
103
|
-
return yield* fetchAndAssemble.pipe(Effect.timeoutOrElse({
|
|
104
|
-
duration: bounds.timeout,
|
|
105
|
-
orElse: () => Effect.fail(new ReactorError("Timeout", "clip download deadline; remote generation outcome unchanged")),
|
|
106
|
-
}));
|
|
107
|
-
});
|
|
108
|
-
return action;
|
|
109
|
-
});
|
|
1
|
+
import { Coordinator } from "reactor-effect-client";
|
|
2
|
+
/** Parse a clip's HLS media playlist into the segments to concatenate. */
|
|
3
|
+
export const parsePlaylist = Coordinator.parsePlaylist;
|
|
4
|
+
/**
|
|
5
|
+
* Bounded transport of a prepared recording, through the portable coordinator
|
|
6
|
+
* client (`Coordinator.make()`); see `Coordinator.Client.downloadClip`.
|
|
7
|
+
*/
|
|
8
|
+
export const downloadClip = (coordinator, clip, options) => coordinator.downloadClip(clip, options);
|
|
110
9
|
//# sourceMappingURL=recording.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording.js","sourceRoot":"","sources":["../../src/_internal/recording.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recording.js","sourceRoot":"","sources":["../../src/_internal/recording.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAQpD,0EAA0E;AAC1E,MAAM,CAAC,MAAM,aAAa,GAAG,WAAW,CAAC,aAAa,CAAC;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,WAA+B,EAC/B,IAAe,EACf,OAAyB,EACoB,EAAE,CAAC,WAAW,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import * as Crypto from "effect/Crypto";
|
|
3
2
|
import * as Layer from "effect/Layer";
|
|
4
|
-
import * as Http from "effect/unstable/http/HttpClient";
|
|
5
3
|
import type * as Scope from "effect/Scope";
|
|
6
4
|
import { PeerFactory } from "reactor-effect-client";
|
|
7
|
-
import type {
|
|
5
|
+
import type { ReactorError, Session } from "reactor-effect-client";
|
|
8
6
|
import type { TrackGeneration } from "reactor-effect-client/host";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Browser transport capability for the portable Client. Building the layer
|
|
9
|
+
* detects WebRTC support, so a host without it fails there, before any Client
|
|
10
|
+
* exists to allocate a remote session. Provide it to the client layer:
|
|
11
|
+
* `Reactor.layer(configuration).pipe(Layer.provide(Browser.layer))`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const layer: Layer.Layer<PeerFactory, ReactorError>;
|
|
12
14
|
export interface MediaGeneration extends Omit<TrackGeneration, "track" | "publish"> {
|
|
13
15
|
readonly track: (name: string) => Effect.Effect<MediaStreamTrack, ReactorError, Scope.Scope>;
|
|
14
16
|
readonly publish: (name: string, source: MediaStreamTrack) => Effect.Effect<void, ReactorError>;
|
|
@@ -17,5 +19,5 @@ export interface MediaGeneration extends Omit<TrackGeneration, "track" | "publis
|
|
|
17
19
|
export declare const media: (session: Session) => Effect.Effect<MediaGeneration, ReactorError>;
|
|
18
20
|
export * as Recording from "./_internal/recording.js";
|
|
19
21
|
export { videoFrames, audioSamples, mediaFacilities, audioContext, webAudioSamples, play, nextPresentation, } from "./_internal/media.js";
|
|
20
|
-
export type { VideoSample, AudioSample, MediaOptions, Presentation, WebAudioOptions, WebAudioSample, } from "./_internal/media.js";
|
|
22
|
+
export type { VideoSample, AudioSample, AudioContextOptions, MediaOptions, PlayOptions, Presentation, WebAudioOptions, WebAudioSample, } from "./_internal/media.js";
|
|
21
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGlE;;;;;GAKG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,WAAW,EAAE,YAAY,CAGxD,CAAC;AAEF,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,SAAS,CAAC;IACjF,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7F,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;CACjG;AAED,+EAA+E;AAC/E,eAAO,MAAM,KAAK,YAAa,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CASjF,CAAC;AAEJ,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,EACL,WAAW,EACX,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import * as Crypto from "effect/Crypto";
|
|
3
2
|
import * as Layer from "effect/Layer";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { errorOf, trackGeneration } from "reactor-effect-client/host";
|
|
3
|
+
import { PeerFactory } from "reactor-effect-client";
|
|
4
|
+
import { parsed, trackGeneration } from "reactor-effect-client/host";
|
|
7
5
|
import { BrowserPeer, requireBrowserPeer } from "./_internal/peer.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Browser transport capability for the portable Client. Building the layer
|
|
8
|
+
* detects WebRTC support, so a host without it fails there, before any Client
|
|
9
|
+
* exists to allocate a remote session. Provide it to the client layer:
|
|
10
|
+
* `Reactor.layer(configuration).pipe(Layer.provide(Browser.layer))`.
|
|
11
|
+
*/
|
|
12
|
+
export const layer = Layer.effect(PeerFactory, parsed(requireBrowserPeer).pipe(Effect.as(PeerFactory.of({ make: () => new BrowserPeer() }))));
|
|
14
13
|
/** Track leases and publication retain the generation that negotiated them. */
|
|
15
14
|
export const media = (session) => trackGeneration(session).pipe(Effect.map((generation) => ({
|
|
16
15
|
...generation,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAErE,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEtE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,KAAK,GAA2C,KAAK,CAAC,MAAM,CACvE,WAAW,EACX,MAAM,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAC9F,CAAC;AAOF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,OAAgB,EAAgD,EAAE,CACtF,eAAe,CAAC,OAAO,CAAC,CAAC,IAAI,CAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC1B,GAAG,UAAU;IACb,0EAA0E;IAC1E,wEAAwE;IACxE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CACtB,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAyB,CAAC,CAAC;CAChF,CAAC,CAAC,CACJ,CAAC;AAEJ,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,EACL,WAAW,EACX,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,eAAe,EACf,IAAI,EACJ,gBAAgB,GACjB,MAAM,sBAAsB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reactor-effect-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-rc.0",
|
|
4
4
|
"description": "Browser WebRTC host for reactor-effect-client: built-in peer connections, generation-scoped tracks, media conversion and recording",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"effect": "4.0.0-rc.115",
|
|
28
|
-
"reactor-effect-client": "0.
|
|
28
|
+
"reactor-effect-client": "0.3.0-rc.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@types/bun": "1.4.2",
|
|
32
31
|
"effect": "4.0.0-rc.115",
|
|
33
|
-
"reactor-effect-client": "0.
|
|
34
|
-
"reactor-effect-test-kit": "0.
|
|
32
|
+
"reactor-effect-client": "0.3.0-rc.0",
|
|
33
|
+
"reactor-effect-test-kit": "0.3.0-rc.0",
|
|
34
|
+
"vitest": "5.0.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
38
38
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
|
-
"test": "
|
|
39
|
+
"test": "vitest run"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public",
|