@banou/media-player 0.8.19 → 0.8.21

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.
@@ -0,0 +1,155 @@
1
+ import type { Transport } from 'osra';
2
+ import type { PlayerMedia } from '../react/media';
3
+ import type { Writable } from './protocol';
4
+ export type { PlayerEvent, PlayerSnapshot, PlayerUpdate } from './protocol';
5
+ export { DEFAULT_PLAYER_ID, PLAYER_CHANNEL, PLAYER_EVENTS } from './protocol';
6
+ /**
7
+ * Where a player serves from, and to whom.
8
+ *
9
+ * With nothing given it serves the window that FRAMES this document, and only that window: a tab is
10
+ * full of frames, and a player that answered whichever of them asked first would be anyone's.
11
+ * `origin` narrows the embedder further when it is known; left unset, the player serves the framing
12
+ * window whatever its origin, so what it serves (its snapshot, `currentSrc` included) reaches any
13
+ * page that embeds it. `transport` replaces all of that for an unusual topology (a worker, a port
14
+ * the two sides already share). `channel` is what separates this from other osra traffic on the same
15
+ * window, and both sides must agree on it.
16
+ */
17
+ export type ExposePlayerOptions = {
18
+ /**
19
+ * Which player this is, for a document that serves more than one: an embedder asks for it by the
20
+ * same id. Defaults to `DEFAULT_PLAYER_ID`, so a document with one player never says it.
21
+ *
22
+ * Serving the same id twice REPLACES it, which is how a player that switches media is followed;
23
+ * two different ids are two players and neither disturbs the other.
24
+ */
25
+ id?: string;
26
+ transport?: Transport;
27
+ /**
28
+ * The embedder's origin, when it is known: what is served reaches nobody else.
29
+ *
30
+ * Two calls naming different origins are two channels, and both answer on the same wire, so a
31
+ * document that serves one embedder openly and another narrowly should give them different
32
+ * `channel` names rather than relying on the origin to separate them.
33
+ */
34
+ origin?: string;
35
+ channel?: string;
36
+ /**
37
+ * Stops serving this media, exactly as the returned teardown does, AND closes the channel it was
38
+ * served on, which is the only thing that ever does.
39
+ *
40
+ * A teardown deliberately does not: a document that swaps a media removes one and adds the next,
41
+ * and a channel that closed in between would drop every embedder listening to the others.
42
+ */
43
+ signal?: AbortSignal;
44
+ };
45
+ /**
46
+ * Serve `media` to this document's embedder. Returns the teardown for THIS media.
47
+ *
48
+ * A plain function rather than a controller object, unlike the engine's, because the caller is
49
+ * almost always a React effect and this is exactly what one returns.
50
+ *
51
+ * The channel to the embedder lives for the document's life and is opened once; what `exposePlayer`
52
+ * does is put a media on it. Tearing down and calling it again with the next media (which is what
53
+ * `<MediaPlayer expose>` does whenever its media changes) swaps the media on the same channel, so an
54
+ * embedder's mirror follows the switch as an element would report it: `emptied`, then the new media's
55
+ * state. An embedder never has to reconnect.
56
+ *
57
+ * Two calls that name a different `origin` or `channel` are two channels. A caller that supplies its
58
+ * own `transport` gets a channel of its own, which its teardown closes.
59
+ *
60
+ * Safe to call in a document nobody frames: it serves nobody and returns at once.
61
+ */
62
+ export declare const exposePlayer: (media: PlayerMedia, options?: ExposePlayerOptions) => (() => void);
63
+ /** The properties an embedder may write. Everything else mirrors the far side and is read-only. */
64
+ type Mirrored = Omit<PlayerMedia, Writable>;
65
+ /**
66
+ * A player in another document, as a media of this one.
67
+ *
68
+ * Everything the mirror carries is present, unlike `PlayerMedia`, where most of it is optional
69
+ * because a media may not implement it: the mirror always answers, from a snapshot that always has
70
+ * every field. So a consumer reads `player.buffered.length` rather than `player.buffered?.length`.
71
+ */
72
+ export type MediaPlayerHandle = {
73
+ readonly [K in keyof Mirrored]: Mirrored[K];
74
+ } & Readonly<Required<Pick<PlayerMedia, 'src' | 'currentSrc' | 'ended' | 'buffered' | 'seekable' | 'error' | 'videoWidth' | 'videoHeight'>>> & Required<Pick<PlayerMedia, Writable>> & {
75
+ /**
76
+ * Settles once the far side has first answered with its state; reads before that answer the
77
+ * empty defaults. Stays pending for as long as nobody answers, and rejects with an AbortError on
78
+ * `destroy()` or when `signal` aborts.
79
+ *
80
+ * Nobody answering covers more than an embed with no player in it: a `MessagePort` whose peer has
81
+ * gone raises no event, so a mirror on a dead transport cannot tell that from a slow one. Bound
82
+ * the wait rather than expecting it to end by itself, with `signal` for the player's whole
83
+ * lifetime or `Promise.race` for the wait alone.
84
+ */
85
+ readonly ready: Promise<void>;
86
+ /**
87
+ * Start playing, muting first if that is what the far document requires, and say which happened.
88
+ *
89
+ * `play()` runs under the FAR document's autoplay policy, and no message carries a user gesture
90
+ * across a frame boundary: however deliberately somebody clicked over here, an unmuted element in
91
+ * a document nobody has touched refuses to start. Muted playback is always permitted, so this
92
+ * tries honestly first and falls back rather than leaving the player stopped.
93
+ *
94
+ * `muted` in the result is what the player ended up as, so an app that gets `true` can offer the
95
+ * viewer a way to turn sound on. That click is itself the gesture the document was missing, and
96
+ * clearing `muted` afterwards needs no permission from anyone.
97
+ *
98
+ * If muted playback is refused too, the FIRST error is what rejects, since a policy refusal says
99
+ * more than whatever the retry hit, and the player is left unmuted as it was found.
100
+ */
101
+ autoplay: () => Promise<{
102
+ muted: boolean;
103
+ }>;
104
+ /** stop mirroring and release the channel; the far player keeps playing */
105
+ destroy: () => void;
106
+ };
107
+ /**
108
+ * Where the far player is.
109
+ *
110
+ * `target` is the iframe that holds it, in the common case. A `Window` covers a frame reached some
111
+ * other way, and any osra transport (a `MessagePort` the two sides already share, a worker) is taken
112
+ * as it is.
113
+ *
114
+ * `origin` is the far side's, when the embedder knows it: both a filter on what is heard and the only
115
+ * origin spoken to. Left unset, whatever origin answers FIRST is pinned for the connection's life and
116
+ * everything after is posted to it, so a frame that later navigates elsewhere is neither heard nor
117
+ * told anything.
118
+ */
119
+ export type MediaPlayerHandleOptions = {
120
+ /**
121
+ * Which player to mirror, when the far document serves more than one. Defaults to
122
+ * `DEFAULT_PLAYER_ID`. An id nothing is serving is not an error: `ready` simply stays pending until
123
+ * something serves it, which is how an embedder waits for a player that has not mounted yet.
124
+ */
125
+ id?: string;
126
+ /** the far side's origin, e.g. `https://torrent.fkn.app`; a url with a path is refused */
127
+ origin?: string;
128
+ /** must match the player's; see `PLAYER_CHANNEL` */
129
+ channel?: string;
130
+ /** closes the mirror: `ready` rejects with an AbortError and nothing more is heard */
131
+ signal?: AbortSignal;
132
+ };
133
+ /**
134
+ * Mirror a player another document is serving, as a media of this one.
135
+ *
136
+ * ```ts
137
+ * const player = mediaPlayer(iframe, { origin: 'https://torrent.fkn.app' })
138
+ * await player.ready
139
+ * await player.play()
140
+ * ```
141
+ *
142
+ * The handle is a `PlayerMedia`, so it can be read, written, listened to, or handed to a
143
+ * `<MediaPlayer media={player}>` of this document's own. Reads answer synchronously from a mirror of
144
+ * the far side's last snapshot; writes move that mirror at once and go out to be applied.
145
+ *
146
+ * Everything this document mirrors on one `target` shares one connection, so calling it once per
147
+ * player is what you should do.
148
+ *
149
+ * `ready` may never settle: an embed that serves no player, or an id nobody serves, is silence, and
150
+ * a transport whose peer has gone raises nothing. Bound it with `signal` or by racing it.
151
+ *
152
+ * @throws TypeError if `target` is an iframe with no window yet (append it to a document first), or
153
+ * if `origin` is not a serialized origin.
154
+ */
155
+ export declare const mediaPlayer: (target: HTMLIFrameElement | Window | Transport, options?: MediaPlayerHandleOptions) => MediaPlayerHandle;
@@ -0,0 +1,416 @@
1
+ import { expose as e } from "osra";
2
+ //#region src/lib/remote/protocol.ts
3
+ var t = [
4
+ "loadstart",
5
+ "loadedmetadata",
6
+ "loadeddata",
7
+ "durationchange",
8
+ "timeupdate",
9
+ "play",
10
+ "playing",
11
+ "pause",
12
+ "seeking",
13
+ "seeked",
14
+ "progress",
15
+ "ratechange",
16
+ "volumechange",
17
+ "ended",
18
+ "waiting",
19
+ "stalled",
20
+ "suspend",
21
+ "canplay",
22
+ "canplaythrough",
23
+ "emptied",
24
+ "resize",
25
+ "error"
26
+ ], n = [
27
+ "currentTime",
28
+ "volume",
29
+ "muted",
30
+ "playbackRate"
31
+ ], r = [
32
+ "play",
33
+ "pause",
34
+ "load"
35
+ ], i = "banou-media-player", a = "default", o = (e) => [
36
+ "loadstart",
37
+ ...e.readyState >= 1 ? ["loadedmetadata", "durationchange"] : [],
38
+ ...e.readyState >= 2 ? ["loadeddata"] : [],
39
+ ...e.readyState >= 3 ? ["canplay"] : [],
40
+ "volumechange",
41
+ "ratechange",
42
+ "progress",
43
+ "resize",
44
+ e.paused ? "pause" : "playing",
45
+ ...e.ended ? ["ended"] : [],
46
+ ...e.error ? ["error"] : []
47
+ ], s = (e) => {
48
+ if (!e) return [];
49
+ let t = [];
50
+ for (let n = 0; n < e.length; n++) t.push([e.start(n), e.end(n)]);
51
+ return t;
52
+ }, c = (e) => ({
53
+ currentTime: l(e.currentTime),
54
+ duration: l(e.duration),
55
+ paused: e.paused,
56
+ ended: e.ended ?? !1,
57
+ seeking: e.seeking,
58
+ readyState: e.readyState,
59
+ volume: e.volume ?? 1,
60
+ muted: e.muted ?? !1,
61
+ playbackRate: e.playbackRate ?? 1,
62
+ src: e.src ?? "",
63
+ currentSrc: e.currentSrc ?? "",
64
+ buffered: s(e.buffered),
65
+ seekable: s(e.seekable),
66
+ error: e.error ? {
67
+ code: e.error.code,
68
+ message: e.error.message
69
+ } : null,
70
+ videoWidth: e.videoWidth ?? 0,
71
+ videoHeight: e.videoHeight ?? 0
72
+ }), l = (e) => Number.isNaN(e) ? 0 : e, u = {
73
+ currentTime: 0,
74
+ duration: 0,
75
+ paused: !0,
76
+ ended: !1,
77
+ seeking: !1,
78
+ readyState: 0,
79
+ volume: 1,
80
+ muted: !1,
81
+ playbackRate: 1,
82
+ src: "",
83
+ currentSrc: "",
84
+ buffered: [],
85
+ seekable: [],
86
+ error: null,
87
+ videoWidth: 0,
88
+ videoHeight: 0
89
+ }, d = (e) => ({
90
+ length: e.length,
91
+ start: (t) => e[t]?.[0] ?? 0,
92
+ end: (t) => e[t]?.[1] ?? 0
93
+ }), f = 1e4, p = 1500, m = () => new DOMException("the far player did not start playing", "NotAllowedError"), h = 8, g = 32, _ = (e, t = {}) => {
94
+ T(t.origin);
95
+ let n = typeof window < "u" && window.parent !== window;
96
+ return !t.transport && !n ? () => {} : b(t).serve(e, t);
97
+ }, v = /* @__PURE__ */ new WeakMap(), y = /* @__PURE__ */ new Map(), b = (i) => {
98
+ let a = i.channel ?? "banou-media-player", s = i.transport, l = `${a}|${i.origin ?? "*"}`, d = s ? v.get(s) ?? (v.set(s, /* @__PURE__ */ new Map()), v.get(s)) : y, f = d.get(l);
99
+ if (f) return f;
100
+ let p = /* @__PURE__ */ new Map(), m = /* @__PURE__ */ new Map(), _ = (e) => m.get(e) ?? m.set(e, /* @__PURE__ */ new Set()).get(e), b = (e, t) => {
101
+ m.get(e)?.delete(t);
102
+ try {
103
+ t.onmessage = null, t.close();
104
+ } catch {}
105
+ }, x = (e, t, n) => {
106
+ for (let r of n ? [n] : [..._(e)]) try {
107
+ r.postMessage(t);
108
+ } catch {
109
+ b(e, r);
110
+ }
111
+ }, S = (e) => {
112
+ let t = p.get(e);
113
+ return t ? c(t) : void 0;
114
+ }, C = (e, t) => {
115
+ if (!m.get(e)?.size) return;
116
+ let n = S(e);
117
+ n && x(e, {
118
+ event: t,
119
+ snapshot: n
120
+ });
121
+ }, w = {
122
+ subscribe: (e, t) => {
123
+ if (typeof e != "string") throw TypeError("a player id must be a string");
124
+ if (typeof MessagePort < "u" && !(t instanceof MessagePort)) throw TypeError("subscribe takes a MessagePort");
125
+ if (!m.has(e) && m.size >= g) throw RangeError("too many players on this channel");
126
+ let n = _(e);
127
+ if (n.size >= h) throw RangeError("too many embedders for this player");
128
+ n.add(t), t.onmessage = () => b(e, t), t.start?.();
129
+ let r = S(e);
130
+ r && x(e, {
131
+ event: "loadstart",
132
+ snapshot: r
133
+ }, t);
134
+ },
135
+ set: (e, t, r) => {
136
+ let i = p.get(e);
137
+ if (!(!i || !n.includes(t))) try {
138
+ i[t] = r;
139
+ } catch {}
140
+ },
141
+ call: async (e, t) => {
142
+ let n = p.get(e);
143
+ if (!(!n || !r.includes(t))) try {
144
+ t === "play" ? await n.play() : t === "pause" ? n.pause() : n.load?.();
145
+ } finally {
146
+ C(e, "timeupdate");
147
+ }
148
+ }
149
+ }, T = new AbortController();
150
+ T.signal.addEventListener("abort", () => {
151
+ d.delete(l);
152
+ for (let [e, t] of m) for (let n of [...t]) b(e, n);
153
+ m.clear(), p.clear();
154
+ }, { once: !0 }), e(w, {
155
+ transport: i.transport ?? {
156
+ emit: window.parent,
157
+ receive: A(() => window.parent, i.origin)
158
+ },
159
+ origin: i.origin,
160
+ key: a,
161
+ unregisterSignal: T.signal
162
+ });
163
+ let E = { serve: (e, n) => {
164
+ let r = n.id ?? "default", i = t.map((t) => [t, () => {
165
+ p.get(r) === e && C(r, t);
166
+ }]);
167
+ for (let [t, n] of i) e.addEventListener(t, n);
168
+ let a = p.has(r);
169
+ p.set(r, e), a && x(r, {
170
+ event: "emptied",
171
+ snapshot: { ...u }
172
+ });
173
+ for (let t of o(c(e))) C(r, t);
174
+ let s = () => {
175
+ for (let [t, n] of i) e.removeEventListener(t, n);
176
+ p.get(r) === e && (p.delete(r), x(r, {
177
+ event: "emptied",
178
+ snapshot: { ...u }
179
+ }));
180
+ }, l = () => {
181
+ s(), T.abort();
182
+ };
183
+ return n.signal && (n.signal.aborted ? l() : n.signal.addEventListener("abort", l, { once: !0 })), s;
184
+ } };
185
+ return d.set(l, E), E;
186
+ }, x = /* @__PURE__ */ new WeakMap(), S = (t, n) => {
187
+ let r = n.channel ?? "banou-media-player", i = `${r}|${n.origin ?? "*"}`, a = x.get(t) ?? (x.set(t, /* @__PURE__ */ new Map()), x.get(t)), o = a.get(i);
188
+ if (o) return o;
189
+ let s = O(t, n.origin), c = /* @__PURE__ */ new Set(), l = new AbortController(), u, d = (e) => {
190
+ try {
191
+ e.port?.postMessage("close");
192
+ } catch {}
193
+ try {
194
+ e.port?.close();
195
+ } catch {}
196
+ e.port = void 0;
197
+ }, f = async (e) => {
198
+ d(e);
199
+ let { port1: t, port2: n } = new MessageChannel();
200
+ e.port = t, t.onmessage = ({ data: t }) => {
201
+ l.signal.aborted || e.update(t);
202
+ }, await w(() => u.subscribe(e.id, n)).catch((t) => {
203
+ throw d(e), t;
204
+ });
205
+ }, p = () => {
206
+ l.abort();
207
+ for (let e of c) d(e);
208
+ c.clear(), a.delete(i);
209
+ };
210
+ (async () => {
211
+ for await (let t of e({}, {
212
+ transport: s,
213
+ origin: n.origin,
214
+ key: r,
215
+ unregisterSignal: l.signal
216
+ })) {
217
+ if (l.signal.aborted) return;
218
+ u = t;
219
+ for (let e of c) e.reset();
220
+ await Promise.allSettled([...c].map((e) => f(e)));
221
+ }
222
+ u = void 0;
223
+ for (let e of c) e.reset();
224
+ })().catch(() => {
225
+ u = void 0;
226
+ for (let e of c) e.reset();
227
+ });
228
+ let m = {
229
+ attach: (e) => (c.add(e), u && f(e).catch(() => {}), () => {
230
+ d(e), c.delete(e), c.size || p();
231
+ }),
232
+ set: (e, t, n) => u?.set(e, t, n),
233
+ call: async (e, t) => {
234
+ if (!u) throw new DOMException("the remote player was closed", "AbortError");
235
+ await u.call(e, t);
236
+ }
237
+ };
238
+ return a.set(i, m), m;
239
+ }, C = (e, t = {}) => {
240
+ T(t.origin);
241
+ let r = new AbortController(), i = t.signal ? AbortSignal.any([t.signal, r.signal]) : r.signal, a = t.id ?? "default", s = { ...u }, c = new EventTarget(), l = (e) => c.dispatchEvent(new Event(e)), f, h, g = new Promise((e, t) => {
242
+ f = e, h = t;
243
+ });
244
+ g.catch(() => {});
245
+ let _ = () => new DOMException("the remote player was closed", "AbortError"), v = /* @__PURE__ */ new Set(), y = () => {
246
+ for (let e of [...v]) e(_());
247
+ v.clear();
248
+ }, b = /* @__PURE__ */ new Map(), x = !1, C = S(e, t).attach({
249
+ id: a,
250
+ reset: () => {
251
+ x = !1, y();
252
+ },
253
+ update: ({ event: e, snapshot: t }) => {
254
+ let n = !x;
255
+ x = !0, Object.assign(s, t);
256
+ for (let [e, t] of b) s[e] = t;
257
+ if (!n) {
258
+ l(e);
259
+ return;
260
+ }
261
+ for (let e of o(s)) l(e);
262
+ f();
263
+ }
264
+ }), w = () => {
265
+ C(), y(), h(_());
266
+ };
267
+ i.aborted ? queueMicrotask(w) : i.addEventListener("abort", w, { once: !0 });
268
+ let E = (e, t) => Object.defineProperty(c, e, {
269
+ get: () => t ? t(s[e]) : s[e],
270
+ enumerable: !0
271
+ });
272
+ for (let e of [
273
+ "duration",
274
+ "paused",
275
+ "ended",
276
+ "seeking",
277
+ "readyState",
278
+ "src",
279
+ "currentSrc",
280
+ "error",
281
+ "videoWidth",
282
+ "videoHeight"
283
+ ]) E(e);
284
+ E("buffered", d), E("seekable", d);
285
+ let D = (e) => g.then(() => i.aborted ? void 0 : e()).catch(() => {}), O = S(e, t);
286
+ for (let e of n) Object.defineProperty(c, e, {
287
+ get: () => s[e],
288
+ set: (t) => {
289
+ s[e] = t, b.set(e, t), D(async () => {
290
+ await O.set(a, e, t), b.get(e) === t && b.delete(e);
291
+ });
292
+ },
293
+ enumerable: !0
294
+ });
295
+ let k = (e) => g.then(() => {
296
+ if (i.aborted) throw _();
297
+ let t = O.call(a, e);
298
+ return new Promise((e, n) => {
299
+ v.add(n), t.then(() => {
300
+ v.delete(n), e();
301
+ }, (e) => {
302
+ v.delete(n), n(i.aborted ? _() : e);
303
+ });
304
+ });
305
+ });
306
+ c.play = () => (s.paused = !1, k("play").catch((e) => {
307
+ throw s.paused = !0, e;
308
+ }));
309
+ let A = () => new Promise((e) => {
310
+ let t = (t) => {
311
+ clearTimeout(r), c.removeEventListener("playing", n), c.removeEventListener("play", n), e(t);
312
+ }, n = () => t(!0);
313
+ c.addEventListener("playing", n), c.addEventListener("play", n);
314
+ let r = setTimeout(() => t(!s.paused), p);
315
+ }), j = async () => {
316
+ let e = !s.paused, t;
317
+ try {
318
+ await c.play();
319
+ } catch (e) {
320
+ t = e;
321
+ }
322
+ return e ? {
323
+ started: !0,
324
+ refusal: t
325
+ } : {
326
+ started: await A(),
327
+ refusal: t
328
+ };
329
+ };
330
+ return c.autoplay = async () => {
331
+ let e = await j();
332
+ if (e.started) return { muted: c.muted };
333
+ if (c.muted || i.aborted) throw e.refusal ?? m();
334
+ c.muted = !0;
335
+ let t = await j();
336
+ if (t.started) return { muted: !0 };
337
+ throw c.muted = !1, e.refusal ?? t.refusal ?? m();
338
+ }, c.pause = () => {
339
+ s.paused = !0, k("pause").catch(() => {});
340
+ }, c.load = () => {
341
+ k("load").catch(() => {});
342
+ }, Object.defineProperty(c, "ready", {
343
+ value: g,
344
+ enumerable: !1
345
+ }), c.destroy = () => r.abort(), c;
346
+ }, w = async (e) => {
347
+ let t;
348
+ try {
349
+ return await Promise.race([e(), new Promise((e, n) => {
350
+ t = setTimeout(() => n(/* @__PURE__ */ Error("the player did not answer")), f);
351
+ })]);
352
+ } finally {
353
+ t && clearTimeout(t);
354
+ }
355
+ }, T = (e) => {
356
+ if (e === void 0 || e === "*") return;
357
+ if (e === "null") throw TypeError("an opaque origin (\"null\") cannot be spoken to; leave `origin` unset and the sender check stands alone");
358
+ let t;
359
+ try {
360
+ t = new URL(e);
361
+ } catch {
362
+ throw TypeError(`\`origin\` must be an origin like https://example.com, not ${e}`);
363
+ }
364
+ if (t.origin !== e) throw TypeError(`\`origin\` must be an origin like ${t.origin}, not ${e}`);
365
+ }, E = (e) => {
366
+ try {
367
+ return !!e && typeof e == "object" && e.window === e;
368
+ } catch {
369
+ return !1;
370
+ }
371
+ }, D = (e) => {
372
+ if (E(e)) return !1;
373
+ try {
374
+ return !!e && typeof e == "object" && "contentWindow" in e;
375
+ } catch {
376
+ return !1;
377
+ }
378
+ }, O = (e, t) => {
379
+ if (E(e)) {
380
+ let n = k(t);
381
+ return {
382
+ emit: (t, r) => {
383
+ e.postMessage(t, n.target(), r ?? []);
384
+ },
385
+ receive: A(() => e, t, n)
386
+ };
387
+ }
388
+ if (D(e)) {
389
+ if (!e.contentWindow) throw TypeError("mediaPlayer: the iframe has no window yet; append it to a document first");
390
+ let n = () => e.contentWindow, r = k(t);
391
+ return {
392
+ emit: (e, t) => {
393
+ n()?.postMessage(e, r.target(), t ?? []);
394
+ },
395
+ receive: A(n, t, r)
396
+ };
397
+ }
398
+ return e;
399
+ }, k = (e) => {
400
+ let t = e && e !== "*" ? e : void 0;
401
+ return {
402
+ target: () => t && t !== "null" ? t : "*",
403
+ admits: (e) => t ? e === t : (e && (t = e), !0)
404
+ };
405
+ }, A = (e, t, n = k(t)) => (t) => {
406
+ let r = (r) => {
407
+ r.source === e() && n.admits(r.origin) && t(r.data, {
408
+ source: r.source,
409
+ origin: r.origin,
410
+ receiveTransport: window
411
+ });
412
+ };
413
+ return window.addEventListener("message", r), () => window.removeEventListener("message", r);
414
+ };
415
+ //#endregion
416
+ export { a as DEFAULT_PLAYER_ID, i as PLAYER_CHANNEL, t as PLAYER_EVENTS, _ as exposePlayer, C as mediaPlayer };
@@ -0,0 +1,91 @@
1
+ import type { PlayerMedia, TimeRangesLike } from '../react/media';
2
+ /**
3
+ * Everything readable about a player, as one plain object.
4
+ *
5
+ * A media's properties are SYNCHRONOUS (`currentTime` is a getter that must answer now) and every
6
+ * cross-document transport is not, so the far side is mirrored: the player's document sends one of
7
+ * these with every event it raises, and the embedder answers reads from the latest one it holds.
8
+ * Plain pairs for the ranges, because a `TimeRanges` has nothing to hand across a boundary.
9
+ */
10
+ export type PlayerSnapshot = {
11
+ currentTime: number;
12
+ duration: number;
13
+ paused: boolean;
14
+ ended: boolean;
15
+ seeking: boolean;
16
+ readyState: number;
17
+ volume: number;
18
+ muted: boolean;
19
+ playbackRate: number;
20
+ src: string;
21
+ currentSrc: string;
22
+ buffered: [number, number][];
23
+ seekable: [number, number][];
24
+ error: {
25
+ code: number;
26
+ message: string;
27
+ } | null;
28
+ videoWidth: number;
29
+ videoHeight: number;
30
+ };
31
+ /**
32
+ * The events a player raises that an embedder can hear, each carrying a fresh snapshot.
33
+ *
34
+ * Every event video.js's own features sync on is here, `emptied` and `loadstart` included: a store
35
+ * attached to the mirror resets on those, and an entry costs nothing until it fires.
36
+ */
37
+ export declare const PLAYER_EVENTS: readonly ['loadstart', 'loadedmetadata', 'loadeddata', 'durationchange', 'timeupdate', 'play', 'playing', 'pause', 'seeking', 'seeked', 'progress', 'ratechange', 'volumechange', 'ended', 'waiting', 'stalled', 'suspend', 'canplay', 'canplaythrough', 'emptied', 'resize', 'error'];
38
+ export type PlayerEvent = typeof PLAYER_EVENTS[number];
39
+ /** The properties an embedder may set. Everything else is read-only on a media, here as there. */
40
+ export declare const WRITABLE: readonly ['currentTime', 'volume', 'muted', 'playbackRate'];
41
+ export type Writable = typeof WRITABLE[number];
42
+ /** The methods an embedder may call. */
43
+ export declare const CALLABLE: readonly ['play', 'pause', 'load'];
44
+ export type Callable = typeof CALLABLE[number];
45
+ /** The one channel both sides default to, so a page can run other osra channels over the same window. */
46
+ export declare const PLAYER_CHANNEL = "banou-media-player";
47
+ /**
48
+ * Which player, for a document that serves more than one.
49
+ *
50
+ * A document with one player never says it; both sides default to this and find each other. A
51
+ * document with several gives each an id and an embedder asks for the one it wants. The ids are the
52
+ * serving document's to choose and the embedder has to know them, the same way it knows the url it
53
+ * framed.
54
+ */
55
+ export declare const DEFAULT_PLAYER_ID = "default";
56
+ /** What rides the event port: the event's name and the state right after it. */
57
+ export type PlayerUpdate = {
58
+ event: PlayerEvent;
59
+ snapshot: PlayerSnapshot;
60
+ };
61
+ /**
62
+ * What the player's document serves, one document at a time, addressed by player id. Internal: an
63
+ * embedder never sees it, `mediaPlayer` wraps it.
64
+ *
65
+ * `subscribe` takes a MessagePort rather than a function on purpose. Events are ONE-WAY: the player
66
+ * posts every update on the port and never waits for an answer, so an embedder that vanished without
67
+ * closing (a tab killed, a frame torn out) pins nothing in the player's document, where a function
68
+ * handle called across a dead connection would leave a promise pending per event, unbounded.
69
+ *
70
+ * There is no `snapshot`: subscribing is what answers with the state, in one round trip instead of
71
+ * two, and it cannot miss what happened between them. Subscribing to an id nothing is serving is
72
+ * allowed and says nothing back until something is, which is how an embedder waits for a player that
73
+ * has not mounted yet.
74
+ */
75
+ export type PlayerService = {
76
+ subscribe: (id: string, port: MessagePort) => void;
77
+ set: (id: string, name: Writable, value: number | boolean) => void;
78
+ call: (id: string, name: Callable) => Promise<void>;
79
+ };
80
+ /**
81
+ * The events that bring a store attached to the mirror up to date from a snapshot alone, in the
82
+ * order an element would raise them on load. Dispatched after every snapshot that arrives with no
83
+ * event of its own: the first one, and the one a new player connection answers with.
84
+ */
85
+ export declare const syncEventsFor: (snapshot: PlayerSnapshot) => PlayerEvent[];
86
+ /** A media as a snapshot. A NaN duration, which a bare element answers before it has metadata, is sent as 0. */
87
+ export declare const snapshotOf: (media: PlayerMedia) => PlayerSnapshot;
88
+ /** The snapshot an embedder holds before the player's document has said anything. */
89
+ export declare const EMPTY_SNAPSHOT: PlayerSnapshot;
90
+ /** Plain pairs back into the structural `TimeRanges` the player reads. */
91
+ export declare const toTimeRanges: (ranges: [number, number][]) => TimeRangesLike;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/media-player",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "description": "A video player for containers and codecs the browser cannot play natively, remuxed on the fly",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,6 +14,10 @@
14
14
  "types": "./dist/engine/index.d.ts",
15
15
  "import": "./dist/engine/index.js"
16
16
  },
17
+ "./remote": {
18
+ "types": "./dist/remote/index.d.ts",
19
+ "import": "./dist/remote/index.js"
20
+ },
17
21
  "./package.json": "./package.json"
18
22
  },
19
23
  "files": [
@@ -43,6 +47,7 @@
43
47
  "ass-compiler": "^0.1.16",
44
48
  "jassub": "^1.8.6",
45
49
  "libav-wasm": "^0.8.4",
50
+ "osra": "^0.6.12",
46
51
  "react-feather": "^2.0.10",
47
52
  "react-tooltip": "^5.28.0"
48
53
  },
@@ -68,5 +73,9 @@
68
73
  },
69
74
  "overrides": {
70
75
  "vite": "npm:@voidzero-dev/vite-plus-core@0.2.4"
76
+ },
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "git+https://github.com/Banou26/media-player.git"
71
80
  }
72
81
  }