@banou/media-player 0.8.20 → 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.
- package/dist/remote/index.d.ts +18 -0
- package/dist/remote/index.js +109 -75
- package/package.json +1 -1
- package/src/lib/remote/index.ts +83 -3
package/dist/remote/index.d.ts
CHANGED
|
@@ -83,6 +83,24 @@ export type MediaPlayerHandle = {
|
|
|
83
83
|
* lifetime or `Promise.race` for the wait alone.
|
|
84
84
|
*/
|
|
85
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
|
+
}>;
|
|
86
104
|
/** stop mirroring and release the channel; the far player keeps playing */
|
|
87
105
|
destroy: () => void;
|
|
88
106
|
};
|
package/dist/remote/index.js
CHANGED
|
@@ -90,29 +90,29 @@ var t = [
|
|
|
90
90
|
length: e.length,
|
|
91
91
|
start: (t) => e[t]?.[0] ?? 0,
|
|
92
92
|
end: (t) => e[t]?.[1] ?? 0
|
|
93
|
-
}), f = 1e4, p =
|
|
94
|
-
|
|
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
95
|
let n = typeof window < "u" && window.parent !== window;
|
|
96
|
-
return !t.transport && !n ? () => {} :
|
|
97
|
-
},
|
|
98
|
-
let a = i.channel ?? "banou-media-player", s = i.transport, l = `${a}|${i.origin ?? "*"}`, d = s ?
|
|
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
99
|
if (f) return f;
|
|
100
|
-
let
|
|
101
|
-
|
|
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
102
|
try {
|
|
103
103
|
t.onmessage = null, t.close();
|
|
104
104
|
} catch {}
|
|
105
105
|
}, x = (e, t, n) => {
|
|
106
|
-
for (let r of n ? [n] : [...
|
|
106
|
+
for (let r of n ? [n] : [..._(e)]) try {
|
|
107
107
|
r.postMessage(t);
|
|
108
108
|
} catch {
|
|
109
109
|
b(e, r);
|
|
110
110
|
}
|
|
111
111
|
}, S = (e) => {
|
|
112
|
-
let t =
|
|
112
|
+
let t = p.get(e);
|
|
113
113
|
return t ? c(t) : void 0;
|
|
114
114
|
}, C = (e, t) => {
|
|
115
|
-
if (!
|
|
115
|
+
if (!m.get(e)?.size) return;
|
|
116
116
|
let n = S(e);
|
|
117
117
|
n && x(e, {
|
|
118
118
|
event: t,
|
|
@@ -122,9 +122,9 @@ var t = [
|
|
|
122
122
|
subscribe: (e, t) => {
|
|
123
123
|
if (typeof e != "string") throw TypeError("a player id must be a string");
|
|
124
124
|
if (typeof MessagePort < "u" && !(t instanceof MessagePort)) throw TypeError("subscribe takes a MessagePort");
|
|
125
|
-
if (!
|
|
126
|
-
let n =
|
|
127
|
-
if (n.size >=
|
|
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
128
|
n.add(t), t.onmessage = () => b(e, t), t.start?.();
|
|
129
129
|
let r = S(e);
|
|
130
130
|
r && x(e, {
|
|
@@ -133,24 +133,28 @@ var t = [
|
|
|
133
133
|
}, t);
|
|
134
134
|
},
|
|
135
135
|
set: (e, t, r) => {
|
|
136
|
-
let i =
|
|
136
|
+
let i = p.get(e);
|
|
137
137
|
if (!(!i || !n.includes(t))) try {
|
|
138
138
|
i[t] = r;
|
|
139
139
|
} catch {}
|
|
140
140
|
},
|
|
141
141
|
call: async (e, t) => {
|
|
142
|
-
let n =
|
|
143
|
-
!n || !r.includes(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
|
+
}
|
|
144
148
|
}
|
|
145
149
|
}, T = new AbortController();
|
|
146
150
|
T.signal.addEventListener("abort", () => {
|
|
147
151
|
d.delete(l);
|
|
148
|
-
for (let [e, t] of
|
|
149
|
-
|
|
152
|
+
for (let [e, t] of m) for (let n of [...t]) b(e, n);
|
|
153
|
+
m.clear(), p.clear();
|
|
150
154
|
}, { once: !0 }), e(w, {
|
|
151
155
|
transport: i.transport ?? {
|
|
152
156
|
emit: window.parent,
|
|
153
|
-
receive:
|
|
157
|
+
receive: A(() => window.parent, i.origin)
|
|
154
158
|
},
|
|
155
159
|
origin: i.origin,
|
|
156
160
|
key: a,
|
|
@@ -158,18 +162,18 @@ var t = [
|
|
|
158
162
|
});
|
|
159
163
|
let E = { serve: (e, n) => {
|
|
160
164
|
let r = n.id ?? "default", i = t.map((t) => [t, () => {
|
|
161
|
-
|
|
165
|
+
p.get(r) === e && C(r, t);
|
|
162
166
|
}]);
|
|
163
167
|
for (let [t, n] of i) e.addEventListener(t, n);
|
|
164
|
-
let a =
|
|
165
|
-
|
|
168
|
+
let a = p.has(r);
|
|
169
|
+
p.set(r, e), a && x(r, {
|
|
166
170
|
event: "emptied",
|
|
167
171
|
snapshot: { ...u }
|
|
168
172
|
});
|
|
169
173
|
for (let t of o(c(e))) C(r, t);
|
|
170
174
|
let s = () => {
|
|
171
175
|
for (let [t, n] of i) e.removeEventListener(t, n);
|
|
172
|
-
|
|
176
|
+
p.get(r) === e && (p.delete(r), x(r, {
|
|
173
177
|
event: "emptied",
|
|
174
178
|
snapshot: { ...u }
|
|
175
179
|
}));
|
|
@@ -179,10 +183,10 @@ var t = [
|
|
|
179
183
|
return n.signal && (n.signal.aborted ? l() : n.signal.addEventListener("abort", l, { once: !0 })), s;
|
|
180
184
|
} };
|
|
181
185
|
return d.set(l, E), E;
|
|
182
|
-
},
|
|
183
|
-
let r = n.channel ?? "banou-media-player", i = `${r}|${n.origin ?? "*"}`, a =
|
|
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);
|
|
184
188
|
if (o) return o;
|
|
185
|
-
let s =
|
|
189
|
+
let s = O(t, n.origin), c = /* @__PURE__ */ new Set(), l = new AbortController(), u, d = (e) => {
|
|
186
190
|
try {
|
|
187
191
|
e.port?.postMessage("close");
|
|
188
192
|
} catch {}
|
|
@@ -195,7 +199,7 @@ var t = [
|
|
|
195
199
|
let { port1: t, port2: n } = new MessageChannel();
|
|
196
200
|
e.port = t, t.onmessage = ({ data: t }) => {
|
|
197
201
|
l.signal.aborted || e.update(t);
|
|
198
|
-
}, await
|
|
202
|
+
}, await w(() => u.subscribe(e.id, n)).catch((t) => {
|
|
199
203
|
throw d(e), t;
|
|
200
204
|
});
|
|
201
205
|
}, p = () => {
|
|
@@ -232,24 +236,24 @@ var t = [
|
|
|
232
236
|
}
|
|
233
237
|
};
|
|
234
238
|
return a.set(i, m), m;
|
|
235
|
-
},
|
|
236
|
-
|
|
237
|
-
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,
|
|
238
|
-
f = e,
|
|
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;
|
|
239
243
|
});
|
|
240
|
-
|
|
241
|
-
let
|
|
242
|
-
for (let e of [...
|
|
243
|
-
|
|
244
|
-
},
|
|
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({
|
|
245
249
|
id: a,
|
|
246
250
|
reset: () => {
|
|
247
|
-
|
|
251
|
+
x = !1, y();
|
|
248
252
|
},
|
|
249
253
|
update: ({ event: e, snapshot: t }) => {
|
|
250
|
-
let n = !
|
|
251
|
-
|
|
252
|
-
for (let [e, t] of
|
|
254
|
+
let n = !x;
|
|
255
|
+
x = !0, Object.assign(s, t);
|
|
256
|
+
for (let [e, t] of b) s[e] = t;
|
|
253
257
|
if (!n) {
|
|
254
258
|
l(e);
|
|
255
259
|
return;
|
|
@@ -257,11 +261,11 @@ var t = [
|
|
|
257
261
|
for (let e of o(s)) l(e);
|
|
258
262
|
f();
|
|
259
263
|
}
|
|
260
|
-
}),
|
|
261
|
-
|
|
264
|
+
}), w = () => {
|
|
265
|
+
C(), y(), h(_());
|
|
262
266
|
};
|
|
263
|
-
i.aborted ? queueMicrotask(
|
|
264
|
-
let
|
|
267
|
+
i.aborted ? queueMicrotask(w) : i.addEventListener("abort", w, { once: !0 });
|
|
268
|
+
let E = (e, t) => Object.defineProperty(c, e, {
|
|
265
269
|
get: () => t ? t(s[e]) : s[e],
|
|
266
270
|
enumerable: !0
|
|
267
271
|
});
|
|
@@ -276,40 +280,70 @@ var t = [
|
|
|
276
280
|
"error",
|
|
277
281
|
"videoWidth",
|
|
278
282
|
"videoHeight"
|
|
279
|
-
])
|
|
280
|
-
|
|
281
|
-
let
|
|
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);
|
|
282
286
|
for (let e of n) Object.defineProperty(c, e, {
|
|
283
287
|
get: () => s[e],
|
|
284
288
|
set: (t) => {
|
|
285
|
-
s[e] = t,
|
|
286
|
-
await
|
|
289
|
+
s[e] = t, b.set(e, t), D(async () => {
|
|
290
|
+
await O.set(a, e, t), b.get(e) === t && b.delete(e);
|
|
287
291
|
});
|
|
288
292
|
},
|
|
289
293
|
enumerable: !0
|
|
290
294
|
});
|
|
291
|
-
let
|
|
292
|
-
if (i.aborted) throw
|
|
293
|
-
let t =
|
|
295
|
+
let k = (e) => g.then(() => {
|
|
296
|
+
if (i.aborted) throw _();
|
|
297
|
+
let t = O.call(a, e);
|
|
294
298
|
return new Promise((e, n) => {
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
v.add(n), t.then(() => {
|
|
300
|
+
v.delete(n), e();
|
|
297
301
|
}, (e) => {
|
|
298
|
-
|
|
302
|
+
v.delete(n), n(i.aborted ? _() : e);
|
|
299
303
|
});
|
|
300
304
|
});
|
|
301
305
|
});
|
|
302
|
-
|
|
306
|
+
c.play = () => (s.paused = !1, k("play").catch((e) => {
|
|
303
307
|
throw s.paused = !0, e;
|
|
304
|
-
}))
|
|
305
|
-
|
|
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(() => {});
|
|
306
340
|
}, c.load = () => {
|
|
307
|
-
|
|
341
|
+
k("load").catch(() => {});
|
|
308
342
|
}, Object.defineProperty(c, "ready", {
|
|
309
|
-
value:
|
|
343
|
+
value: g,
|
|
310
344
|
enumerable: !1
|
|
311
345
|
}), c.destroy = () => r.abort(), c;
|
|
312
|
-
},
|
|
346
|
+
}, w = async (e) => {
|
|
313
347
|
let t;
|
|
314
348
|
try {
|
|
315
349
|
return await Promise.race([e(), new Promise((e, n) => {
|
|
@@ -318,7 +352,7 @@ var t = [
|
|
|
318
352
|
} finally {
|
|
319
353
|
t && clearTimeout(t);
|
|
320
354
|
}
|
|
321
|
-
},
|
|
355
|
+
}, T = (e) => {
|
|
322
356
|
if (e === void 0 || e === "*") return;
|
|
323
357
|
if (e === "null") throw TypeError("an opaque origin (\"null\") cannot be spoken to; leave `origin` unset and the sender check stands alone");
|
|
324
358
|
let t;
|
|
@@ -328,47 +362,47 @@ var t = [
|
|
|
328
362
|
throw TypeError(`\`origin\` must be an origin like https://example.com, not ${e}`);
|
|
329
363
|
}
|
|
330
364
|
if (t.origin !== e) throw TypeError(`\`origin\` must be an origin like ${t.origin}, not ${e}`);
|
|
331
|
-
},
|
|
365
|
+
}, E = (e) => {
|
|
332
366
|
try {
|
|
333
367
|
return !!e && typeof e == "object" && e.window === e;
|
|
334
368
|
} catch {
|
|
335
369
|
return !1;
|
|
336
370
|
}
|
|
337
|
-
},
|
|
338
|
-
if (
|
|
371
|
+
}, D = (e) => {
|
|
372
|
+
if (E(e)) return !1;
|
|
339
373
|
try {
|
|
340
374
|
return !!e && typeof e == "object" && "contentWindow" in e;
|
|
341
375
|
} catch {
|
|
342
376
|
return !1;
|
|
343
377
|
}
|
|
344
|
-
},
|
|
345
|
-
if (
|
|
346
|
-
let n =
|
|
378
|
+
}, O = (e, t) => {
|
|
379
|
+
if (E(e)) {
|
|
380
|
+
let n = k(t);
|
|
347
381
|
return {
|
|
348
382
|
emit: (t, r) => {
|
|
349
383
|
e.postMessage(t, n.target(), r ?? []);
|
|
350
384
|
},
|
|
351
|
-
receive:
|
|
385
|
+
receive: A(() => e, t, n)
|
|
352
386
|
};
|
|
353
387
|
}
|
|
354
|
-
if (
|
|
388
|
+
if (D(e)) {
|
|
355
389
|
if (!e.contentWindow) throw TypeError("mediaPlayer: the iframe has no window yet; append it to a document first");
|
|
356
|
-
let n = () => e.contentWindow, r =
|
|
390
|
+
let n = () => e.contentWindow, r = k(t);
|
|
357
391
|
return {
|
|
358
392
|
emit: (e, t) => {
|
|
359
393
|
n()?.postMessage(e, r.target(), t ?? []);
|
|
360
394
|
},
|
|
361
|
-
receive:
|
|
395
|
+
receive: A(n, t, r)
|
|
362
396
|
};
|
|
363
397
|
}
|
|
364
398
|
return e;
|
|
365
|
-
},
|
|
399
|
+
}, k = (e) => {
|
|
366
400
|
let t = e && e !== "*" ? e : void 0;
|
|
367
401
|
return {
|
|
368
402
|
target: () => t && t !== "null" ? t : "*",
|
|
369
403
|
admits: (e) => t ? e === t : (e && (t = e), !0)
|
|
370
404
|
};
|
|
371
|
-
},
|
|
405
|
+
}, A = (e, t, n = k(t)) => (t) => {
|
|
372
406
|
let r = (r) => {
|
|
373
407
|
r.source === e() && n.admits(r.origin) && t(r.data, {
|
|
374
408
|
source: r.source,
|
|
@@ -379,4 +413,4 @@ var t = [
|
|
|
379
413
|
return window.addEventListener("message", r), () => window.removeEventListener("message", r);
|
|
380
414
|
};
|
|
381
415
|
//#endregion
|
|
382
|
-
export { a as DEFAULT_PLAYER_ID, i as PLAYER_CHANNEL, t as PLAYER_EVENTS,
|
|
416
|
+
export { a as DEFAULT_PLAYER_ID, i as PLAYER_CHANNEL, t as PLAYER_EVENTS, _ as exposePlayer, C as mediaPlayer };
|
package/package.json
CHANGED
package/src/lib/remote/index.ts
CHANGED
|
@@ -24,6 +24,17 @@ export { DEFAULT_PLAYER_ID, PLAYER_CHANNEL, PLAYER_EVENTS } from './protocol'
|
|
|
24
24
|
/** How long a peer has to answer its handshake before the mirror stops waiting on it and moves on. */
|
|
25
25
|
const HANDSHAKE_MS = 10_000
|
|
26
26
|
/** How many embedders one player will report to, per id. A page needs one; the cap is there so a peer cannot grow the set without bound. */
|
|
27
|
+
/**
|
|
28
|
+
* How long `autoplay` waits for the far player to actually start before calling it refused.
|
|
29
|
+
*
|
|
30
|
+
* It is a bound on a refusal that announces nothing, not on buffering: a player that starts answers
|
|
31
|
+
* with its own `playing` and settles the wait immediately, however long the source took.
|
|
32
|
+
*/
|
|
33
|
+
const PLAY_CONFIRM_MS = 1_500
|
|
34
|
+
|
|
35
|
+
const notStartedError = () =>
|
|
36
|
+
new DOMException('the far player did not start playing', 'NotAllowedError')
|
|
37
|
+
|
|
27
38
|
const MAX_SUBSCRIBERS = 8
|
|
28
39
|
/** How many distinct player ids one channel will hold. The id comes from the peer, so it is bounded too. */
|
|
29
40
|
const MAX_PLAYERS = 32
|
|
@@ -184,9 +195,21 @@ const channelFor = (options: ExposePlayerOptions): Channel => {
|
|
|
184
195
|
call: async (id, name) => {
|
|
185
196
|
const media = medias.get(id)
|
|
186
197
|
if (!media || !CALLABLE.includes(name)) return
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
try {
|
|
199
|
+
if (name === 'play') await media.play()
|
|
200
|
+
else if (name === 'pause') media.pause()
|
|
201
|
+
else media.load?.()
|
|
202
|
+
} finally {
|
|
203
|
+
// What the call ACTUALLY did, whether it threw or not.
|
|
204
|
+
//
|
|
205
|
+
// An embedder writes its mirror optimistically when it calls, exactly as an element does, and
|
|
206
|
+
// then waits for events to correct it. A play that is refused fires no event at all, so
|
|
207
|
+
// without this the mirror is left believing it is playing, stops asking, and the far video
|
|
208
|
+
// sits paused for ever while seeks keep landing on it. Worse, a player whose `play()`
|
|
209
|
+
// RESOLVES without starting (a wrapper that swallows the refusal) is invisible to a promise
|
|
210
|
+
// and visible here. Measured against stub's watch party, 2026-09-07.
|
|
211
|
+
announce(id, 'timeupdate')
|
|
212
|
+
}
|
|
190
213
|
},
|
|
191
214
|
}
|
|
192
215
|
|
|
@@ -265,6 +288,22 @@ export type MediaPlayerHandle =
|
|
|
265
288
|
* lifetime or `Promise.race` for the wait alone.
|
|
266
289
|
*/
|
|
267
290
|
readonly ready: Promise<void>
|
|
291
|
+
/**
|
|
292
|
+
* Start playing, muting first if that is what the far document requires, and say which happened.
|
|
293
|
+
*
|
|
294
|
+
* `play()` runs under the FAR document's autoplay policy, and no message carries a user gesture
|
|
295
|
+
* across a frame boundary: however deliberately somebody clicked over here, an unmuted element in
|
|
296
|
+
* a document nobody has touched refuses to start. Muted playback is always permitted, so this
|
|
297
|
+
* tries honestly first and falls back rather than leaving the player stopped.
|
|
298
|
+
*
|
|
299
|
+
* `muted` in the result is what the player ended up as, so an app that gets `true` can offer the
|
|
300
|
+
* viewer a way to turn sound on. That click is itself the gesture the document was missing, and
|
|
301
|
+
* clearing `muted` afterwards needs no permission from anyone.
|
|
302
|
+
*
|
|
303
|
+
* If muted playback is refused too, the FIRST error is what rejects, since a policy refusal says
|
|
304
|
+
* more than whatever the retry hit, and the player is left unmuted as it was found.
|
|
305
|
+
*/
|
|
306
|
+
autoplay: () => Promise<{ muted: boolean }>
|
|
268
307
|
/** stop mirroring and release the channel; the far player keeps playing */
|
|
269
308
|
destroy: () => void
|
|
270
309
|
}
|
|
@@ -523,6 +562,47 @@ export const mediaPlayer = (
|
|
|
523
562
|
// like an element's, `play` settles with the far side's own answer, so an autoplay refusal over
|
|
524
563
|
// there rejects over here, and the mirror goes back to paused when it does
|
|
525
564
|
player.play = () => { state.paused = false; return call('play').catch(error => { state.paused = true; throw error }) }
|
|
565
|
+
// Whether the far player is actually running, shortly after being asked to. Its own `playing`
|
|
566
|
+
// settles this the moment it arrives; the timeout is for the refusal that announces nothing, and
|
|
567
|
+
// reads the state the call itself reported on its way out.
|
|
568
|
+
const startedPlaying = () => new Promise<boolean>(resolve => {
|
|
569
|
+
// Deliberately NOT short-circuiting on `state.paused` here: `play()` has just written it
|
|
570
|
+
// optimistically, so it reads as playing whatever the far side does. Only an event from over
|
|
571
|
+
// there, or the state left behind once the call has answered, can settle this.
|
|
572
|
+
const finish = (started: boolean) => {
|
|
573
|
+
clearTimeout(timer)
|
|
574
|
+
player.removeEventListener('playing', ok)
|
|
575
|
+
player.removeEventListener('play', ok)
|
|
576
|
+
resolve(started)
|
|
577
|
+
}
|
|
578
|
+
const ok = () => finish(true)
|
|
579
|
+
player.addEventListener('playing', ok)
|
|
580
|
+
player.addEventListener('play', ok)
|
|
581
|
+
const timer = setTimeout(() => finish(!state.paused), PLAY_CONFIRM_MS)
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
const askToPlay = async () => {
|
|
585
|
+
// read before `play()` writes its optimism over it
|
|
586
|
+
const alreadyPlaying = !state.paused
|
|
587
|
+
let refusal: unknown
|
|
588
|
+
try { await player.play() } catch (error) { refusal = error }
|
|
589
|
+
if (alreadyPlaying) return { started: true, refusal }
|
|
590
|
+
// the far side's own word, not the promise's: a wrapper that resolves a refused play is common
|
|
591
|
+
// enough that trusting the promise is what left followers paused for ever
|
|
592
|
+
return { started: await startedPlaying(), refusal }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
player.autoplay = async () => {
|
|
596
|
+
const first = await askToPlay()
|
|
597
|
+
if (first.started) return { muted: player.muted }
|
|
598
|
+
// already muted and still refused, or torn down mid-try: nothing left to trade away
|
|
599
|
+
if (player.muted || signal.aborted) throw first.refusal ?? notStartedError()
|
|
600
|
+
player.muted = true
|
|
601
|
+
const second = await askToPlay()
|
|
602
|
+
if (second.started) return { muted: true }
|
|
603
|
+
player.muted = false
|
|
604
|
+
throw first.refusal ?? second.refusal ?? notStartedError()
|
|
605
|
+
}
|
|
526
606
|
player.pause = () => { state.paused = true; call('pause').catch(() => {}) }
|
|
527
607
|
player.load = () => { call('load').catch(() => {}) }
|
|
528
608
|
Object.defineProperty(player, 'ready', { value: ready, enumerable: false })
|