@vestra-player/sdk 1.5.1 → 1.5.3
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/CHANGELOG.md +30 -0
- package/dist/plugin/packages.js +5 -0
- package/dist/plugin/resolvers.js +16 -4
- package/dist/plugin/stream.d.ts +11 -1
- package/dist/plugin/stream.js +142 -68
- package/dist/plugin/testing.d.ts +88 -2
- package/dist/plugin/testing.js +360 -22
- package/dist/plugin/types.d.ts +369 -22
- package/dist/plugin/ui.js +99 -3
- package/dist/source-resolvers.d.ts +33 -6
- package/dist/source-resolvers.js +42 -14
- package/dist/wire.d.ts +20 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.3
|
|
4
|
+
|
|
5
|
+
**File resolvers: local files as generated streams** (player 0.4.1). Additive; `apiVersion` stays `vestra/1`. Plugins that register one set `"minPlayerVersion": "0.4.1"`: an older player refuses the declaration (`invalid`).
|
|
6
|
+
|
|
7
|
+
- **`resolvers.add({ id, extensions, resolve })`** (`FileResolverOptions`). When anyone opens a local file of those extensions with `player.open` (the Open dialog, the playlist, next / previous, the recent list), the player asks this plugin for a stream instead of playing the file. `resolve(path, signal)` returns a `ResolvedStream`: `{ kind: "stream", width, height, frame, started? }`, with the frame contract of `openStream`. Needs `source.resolve` and `producer.av`. `ResolverOptions` is now the union of `WebResolverOptions` (the existing form) and `FileResolverOptions`.
|
|
8
|
+
- The open keeps its arbitration: its `ifCurrent` and operation (the opener's `open` / `media.ready()` settle as usual), and `paused` reaches the stream's start. `state.source` stays the path, `state.kind` is `"stream"`.
|
|
9
|
+
- Cancellation: `signal` aborts when the open is replaced or closed (`player.close()` included), times out (30 s) or the resolver is disposed. Cancelled after `resolve` returned but before the stream ran, the SDK releases the stream and never calls `started`.
|
|
10
|
+
- `started(stream)` is called synchronously right after activation, before any `onStop`; the `Stream` is the same as `openStream`'s and belongs to the resolver's plugin.
|
|
11
|
+
- Extensions: 1–16 entries of 1–16 lower-case letters or digits, not `"folder"`; only absolute local paths match (any case), URLs never do.
|
|
12
|
+
- **One extension, and one scheme + host pair, has one resolver.** Another plugin's makes `Resolver.ready` reject with `conflict` (reason `resolver-taken` on the wire), and that no longer fails activation, for web resolvers too (as for panels and the sequence since 1.5.2); the player shows a warning (`PluginInfo.warnings`: `code` `"resolver-taken"`, `owner`, the new `target`). The plugin's own second claim still fails activation.
|
|
13
|
+
- **Resolver failures are sent as the error's message** (`error.message`, was `String(error)`, e.g. `"Error: …"`), and the player shows them word for word (it prefixed `resolver failed: `). This changes the failure text of web resolvers too.
|
|
14
|
+
- **Docs**: `PlayerState.source` documents the naming of generated streams: `producer://<n>` (opaque) for `openStream`, the path for a file resolver's; tell streams apart by `kind`.
|
|
15
|
+
- **`TestHost`** simulates file resolutions: opening a matching path (`player.open`, `openElsewhere`) sends `source.resolveRequest` and keeps the open pending; the answer selects the stream (or fails the open); any later source change cancels it (`source.resolveCancel`). `resolutions` lists the pending ones, `otherResolvers` simulates other plugins' registrations (conflicts). `startPlugin` does not fail activation for such a conflict. Web resolutions are still driven by the test (emit `source.resolveRequest`).
|
|
16
|
+
- Internals: `openStream` is split into prepare / activate / abandon steps shared with file resolvers; the wire client (`sourceResolvers.register`) takes a `{ id, extensions }` declaration and a handler answer with the prepared producer.
|
|
17
|
+
|
|
18
|
+
## 1.5.2
|
|
19
|
+
|
|
20
|
+
**Sidebar tabs** (the player after 0.4.0). Additive; `apiVersion` stays `vestra/1`, and plugins written for 1.5.1 need no change. The new members work against older players too (see each one).
|
|
21
|
+
|
|
22
|
+
- **Several sidebar panels.** A plugin mounting in the sidebar while another plugin's panel is there no longer fails: its `ready` resolves and the panel is a second tab. The player draws a 31px tab strip (the playlist's) above the current tab while there are two or more; with one panel nothing changes. Shown, hidden and floating apply to the whole sidebar. Tabs are labelled with the mount's `title` (else the plugin's name); built-in plugins' tabs come first, the playlist's is the first.
|
|
23
|
+
- **`Panel.state`** (`PanelState`: `active`, `preferred`, `effective`, `reason`, `detached`) and **`Panel.onChange`**: the panel's own state. A panel in a background tab has `active: false`, `effective: false` and `reason: "otherTab"`. Older players report the position's state with `active: true`.
|
|
24
|
+
- **`Panel.show()`** makes the panel's tab current and shows the sidebar; **`Panel.toggle()`** hides the sidebar when the panel's tab is shown, otherwise shows it (a background tab comes forward). Use them for a "Show my panel" command: `checked: () => !!list.state?.preferred && !!list.state?.active`. Older players: `setVisible(true)` / `toggle()` of the position.
|
|
25
|
+
- **`PanelSlot.panels`** (`SlotPanel[]`: `plugin`, `title`, `active`) lists a position's tabs, `null` from older players; `PanelSlot.onChange` also runs when only the tabs change. **`PanelSlot.activate(plugin)`** switches the current tab (older players ignore it). `SlotState` is unchanged.
|
|
26
|
+
- **The `tabbed` property.** If a sidebar panel's bridge global declares `in property <bool> tabbed`, the player sets it to `true` while it draws its tab strip, so a panel with a tab strip of its own (the playlist) hides it. Drop coordinates (`Panel.onDrop`) start below the strip.
|
|
27
|
+
- **`Panel.showMenu`** in a background tab rejects with `unavailable`.
|
|
28
|
+
- **Taken positions** (player 0.4.x after 0.4.0, documented now): the bottom bar and the backdrop take one panel; another plugin's mount there rejects `ready` with `conflict` (「底栏已被 vestra.playback 使用」), as does a ninth sidebar tab. This refusal, and `conflict` from `player.provideSequence` while another plugin provides next/previous, do not fail activation in the player: the plugin runs without its panel or sequence. Players 0.4.0 and older answered a taken bottom bar or sidebar with `ui-prepare` and failed the activation.
|
|
29
|
+
- **`PluginInfo.warnings`** (`PluginWarning`: `code` `"slot-occupied"` or `"sequence-taken"`, `slot`, `owner`): what a running plugin was refused because another plugin holds it. Empty from older players.
|
|
30
|
+
- **`TestHost`** models both: `host.mountOther(slot, plugin, { title })` and `host.unmountOther(plugin)` simulate another plugin's panel (a sidebar tab, or a taken bottom bar or backdrop: the plugin's mount then rejects with `conflict`), `host.selectTab(plugin)` the user clicking a tab; `host.layout.sidebar.panels` / `.owner` are the tabs and the current one, `layout.set` takes `active`. `startPlugin` does not fail on a taken position, like the player.
|
|
31
|
+
- Examples: panel-menu no longer asks you not to load it with the playlist; it is a second tab there. Its list ends in a stretching filler: without it the rows' fixed heights capped the player's main window at its minimum height.
|
|
32
|
+
|
|
3
33
|
## 1.5.1
|
|
4
34
|
|
|
5
35
|
Fix; no API or type changes.
|
package/dist/plugin/packages.js
CHANGED
|
@@ -220,4 +220,9 @@ const info = (plugin) => Object.freeze({
|
|
|
220
220
|
builtin: plugin.builtin === true,
|
|
221
221
|
source: plugin.source ? Object.freeze({ ...plugin.source }) : null,
|
|
222
222
|
dataDirectory: plugin.dataDirectory ?? null,
|
|
223
|
+
warnings: Object.freeze((plugin.warnings ?? []).map((warning) => Object.freeze({
|
|
224
|
+
code: warning.code,
|
|
225
|
+
...(warning.slot === undefined ? {} : { slot: warning.slot }),
|
|
226
|
+
...(warning.owner === undefined ? {} : { owner: warning.owner }),
|
|
227
|
+
}))),
|
|
223
228
|
});
|
package/dist/plugin/resolvers.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Source resolvers: page URLs to stream URLs, or local files to a stream
|
|
3
|
+
* this plugin generates, for `player.open`.
|
|
4
|
+
*/
|
|
5
|
+
import { failure, quiet } from "./runtime.js";
|
|
6
|
+
import { playerOf } from "./player.js";
|
|
7
|
+
import { resolvedStream } from "./stream.js";
|
|
8
|
+
const isFiles = (options) => "extensions" in options;
|
|
3
9
|
export const resolvers = (scope) => ({
|
|
4
10
|
add(options) {
|
|
5
|
-
const { resolve, ...declaration } = options;
|
|
6
11
|
const runtime = scope.runtime;
|
|
7
|
-
const registration =
|
|
12
|
+
const registration = isFiles(options)
|
|
13
|
+
? runtime.host.sourceResolvers.register({ id: options.id, extensions: options.extensions }, async ({ url, signal }) => {
|
|
14
|
+
const stream = await options.resolve(url, signal);
|
|
15
|
+
if (stream?.kind !== "stream")
|
|
16
|
+
throw failure("invalid", 'a file resolver returns { kind: "stream", width, height, frame }');
|
|
17
|
+
return resolvedStream(scope, playerOf(runtime), stream, signal);
|
|
18
|
+
})
|
|
19
|
+
: runtime.host.sourceResolvers.register({ id: options.id, schemes: options.schemes, hosts: options.hosts }, ({ url, signal }) => options.resolve(url, signal));
|
|
8
20
|
let disposed = false;
|
|
9
21
|
const handle = {
|
|
10
22
|
ready: quiet(runtime
|
package/dist/plugin/stream.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { PlayerService } from "./player.ts";
|
|
2
2
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ResolvedProducer } from "../source-resolvers.ts";
|
|
4
|
+
import type { ResolvedStream, Stream, StreamOptions } from "./types.ts";
|
|
4
5
|
export declare function openStream(scope: ScopeImpl, service: PlayerService, options: StreamOptions): Promise<Stream>;
|
|
6
|
+
/**
|
|
7
|
+
* A file resolver's stream (`ResolvedStream`): prepared and prerolled while
|
|
8
|
+
* the open waits, then handed to the host as the answer. Once the host
|
|
9
|
+
* selected it, the start control is acknowledged and `started` gets the
|
|
10
|
+
* running stream, synchronously, before any `onStop`. Cancelled before or
|
|
11
|
+
* during activation (the open was replaced or closed, the plugin stopped),
|
|
12
|
+
* the producer is released and `started` is never called.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolvedStream(scope: ScopeImpl, service: PlayerService, stream: ResolvedStream, signal: AbortSignal): Promise<ResolvedProducer>;
|
package/dist/plugin/stream.js
CHANGED
|
@@ -154,17 +154,11 @@ async function sendFrame(socket, frame, clock, epoch) {
|
|
|
154
154
|
clock.advance(samples.length / 2);
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
163
|
-
let prepared;
|
|
164
|
-
let socket;
|
|
165
|
-
let generation;
|
|
166
|
-
/** The request selecting the stream, once sent. */
|
|
167
|
-
let opened;
|
|
157
|
+
/**
|
|
158
|
+
* Prepare a producer, connect and preroll it until the host could select it.
|
|
159
|
+
* `check` throws to give up (an abort); the producer is then released.
|
|
160
|
+
*/
|
|
161
|
+
async function prepare(host, options, check) {
|
|
168
162
|
const bytes = options.width * options.height * 4;
|
|
169
163
|
/** The plugin's frame, checked against the declared size. */
|
|
170
164
|
const frame = () => {
|
|
@@ -173,103 +167,183 @@ export async function openStream(scope, service, options) {
|
|
|
173
167
|
throw failure("invalid", `frame().video must be ${options.width}×${options.height}×4 = ${bytes} bytes of RGBA`);
|
|
174
168
|
return next;
|
|
175
169
|
};
|
|
170
|
+
let producer;
|
|
171
|
+
let socket;
|
|
176
172
|
try {
|
|
177
173
|
check();
|
|
178
174
|
const spec = await host.producer.prepare({
|
|
179
175
|
width: options.width,
|
|
180
176
|
height: options.height,
|
|
181
177
|
});
|
|
182
|
-
|
|
178
|
+
producer = spec.producerId;
|
|
183
179
|
check();
|
|
184
180
|
socket = await host.producer.connect(spec);
|
|
185
181
|
check();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
advance: (n) => (sample += n),
|
|
194
|
-
nextSequence: () => sequence++,
|
|
182
|
+
const p = {
|
|
183
|
+
producer,
|
|
184
|
+
socket,
|
|
185
|
+
frame,
|
|
186
|
+
epoch: spec.epoch,
|
|
187
|
+
sample: 0,
|
|
188
|
+
sequence: 0,
|
|
195
189
|
};
|
|
190
|
+
const clock = clockOf(p);
|
|
196
191
|
// A prepared producer is paused at sample zero. Supply a frame at zero and
|
|
197
192
|
// enough contiguous PCM before asking the host to select it.
|
|
198
|
-
while (sample < PREROLL) {
|
|
193
|
+
while (p.sample < PREROLL) {
|
|
199
194
|
check();
|
|
200
|
-
await writeFrame(socket, frame(), clock, epoch);
|
|
195
|
+
await writeFrame(socket, frame(), clock, p.epoch);
|
|
201
196
|
}
|
|
202
197
|
for (const deadline = Date.now() + 10_000;; await sleep(10)) {
|
|
203
198
|
check();
|
|
204
|
-
const state = await host.producer.state(
|
|
199
|
+
const state = await host.producer.state(producer);
|
|
205
200
|
if (state.error)
|
|
206
201
|
throw failure("stream", state.error);
|
|
207
|
-
if (state.epoch !== epoch)
|
|
202
|
+
if (state.epoch !== p.epoch)
|
|
208
203
|
throw failure("stream", "producer epoch changed before activation");
|
|
209
204
|
if (!["ok", "recoverableXrun", "deviceChanged"].includes(state.health))
|
|
210
205
|
throw failure("device", `audio device is not ready: ${state.health}`);
|
|
211
206
|
if (state.acknowledged && state.receivedFrames >= 1024 && state.hasVideo)
|
|
212
|
-
|
|
207
|
+
return p;
|
|
213
208
|
if (Date.now() >= deadline)
|
|
214
209
|
throw timeout("prefill", "timed out preparing the first buffers");
|
|
215
210
|
}
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
socket?.close();
|
|
214
|
+
if (producer !== undefined)
|
|
215
|
+
await host.producer.release(producer).catch(() => { });
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const clockOf = (p) => ({
|
|
220
|
+
get sample() {
|
|
221
|
+
return p.sample;
|
|
222
|
+
},
|
|
223
|
+
advance: (n) => (p.sample += n),
|
|
224
|
+
nextSequence: () => p.sequence++,
|
|
225
|
+
});
|
|
226
|
+
/**
|
|
227
|
+
* Wait until the host runs the selected producer: acknowledge its start
|
|
228
|
+
* control. `selected` says whether the host has accepted the selection yet
|
|
229
|
+
* (`"selected"`: it has, and a producer no longer active was replaced);
|
|
230
|
+
* `check` throws to give up.
|
|
231
|
+
*/
|
|
232
|
+
async function activate(host, p, check, selected) {
|
|
233
|
+
for (const deadline = Date.now() + 10_000;; await sleep(10)) {
|
|
234
|
+
check();
|
|
235
|
+
const state = await host.producer.state(p.producer);
|
|
236
|
+
if (state.error)
|
|
237
|
+
throw failure("stream", state.error);
|
|
238
|
+
if (selected === "selected" && !state.active)
|
|
239
|
+
throw abortError();
|
|
240
|
+
if (state.pendingControl) {
|
|
241
|
+
p.epoch = state.pendingControl.epoch;
|
|
242
|
+
p.sample = state.pendingControl.baseSample;
|
|
243
|
+
p.sequence = 0;
|
|
244
|
+
await host.producer.ack(state.pendingControl);
|
|
245
|
+
}
|
|
246
|
+
if ((selected === "selected" || selected()) &&
|
|
247
|
+
state.active &&
|
|
248
|
+
!state.pendingControl &&
|
|
249
|
+
state.acknowledged &&
|
|
250
|
+
state.epoch === p.epoch)
|
|
251
|
+
return;
|
|
252
|
+
if (Date.now() >= deadline)
|
|
253
|
+
throw timeout("activation", "timed out waiting for stream activation");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Give a prepared producer up: its source (if still ours), connection, producer. */
|
|
257
|
+
async function abandon(service, p, generation) {
|
|
258
|
+
// A cancelled open may already have selected this producer: close only
|
|
259
|
+
// that source, before its frame connection (as in `stop`).
|
|
260
|
+
if (generation !== undefined)
|
|
261
|
+
await service.closeIfCurrent(generation).catch(() => { });
|
|
262
|
+
p.socket.close();
|
|
263
|
+
await service.runtime.host.producer.release(p.producer).catch(() => { });
|
|
264
|
+
}
|
|
265
|
+
const handleOf = (scope, service, p, generation) => new StreamHandle(scope, service, {
|
|
266
|
+
producer: p.producer,
|
|
267
|
+
socket: p.socket,
|
|
268
|
+
generation,
|
|
269
|
+
frame: p.frame,
|
|
270
|
+
epoch: p.epoch,
|
|
271
|
+
sample: p.sample,
|
|
272
|
+
sequence: p.sequence,
|
|
273
|
+
});
|
|
274
|
+
export async function openStream(scope, service, options) {
|
|
275
|
+
const host = service.runtime.host;
|
|
276
|
+
const check = () => {
|
|
277
|
+
if (options.signal?.aborted || scope.disposed)
|
|
278
|
+
throw abortError();
|
|
279
|
+
};
|
|
280
|
+
const p = await prepare(host, options, check);
|
|
281
|
+
let generation;
|
|
282
|
+
/** The request selecting the stream. */
|
|
283
|
+
let opened;
|
|
284
|
+
try {
|
|
216
285
|
let accepted = false;
|
|
286
|
+
let failed;
|
|
217
287
|
opened = service
|
|
218
|
-
.submit({ kind: "openProvider", producerId:
|
|
288
|
+
.submit({ kind: "openProvider", producerId: p.producer, startPaused: false }, { accepted: (ticket) => (generation = ticket.stamp.generation) })
|
|
219
289
|
.then((ticket) => {
|
|
220
290
|
generation = ticket.stamp.generation;
|
|
221
291
|
accepted = true;
|
|
222
292
|
});
|
|
223
|
-
let failed;
|
|
224
293
|
opened.catch((error) => (failed = error));
|
|
225
294
|
// Activation needs this side to acknowledge the host's start control.
|
|
226
|
-
|
|
295
|
+
await activate(host, p, () => {
|
|
227
296
|
check();
|
|
228
297
|
if (failed)
|
|
229
298
|
throw failed;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
throw failure("stream", state.error);
|
|
233
|
-
if (state.pendingControl) {
|
|
234
|
-
epoch = state.pendingControl.epoch;
|
|
235
|
-
sample = state.pendingControl.baseSample;
|
|
236
|
-
sequence = 0;
|
|
237
|
-
await host.producer.ack(state.pendingControl);
|
|
238
|
-
}
|
|
239
|
-
if (accepted &&
|
|
240
|
-
state.active &&
|
|
241
|
-
!state.pendingControl &&
|
|
242
|
-
state.acknowledged &&
|
|
243
|
-
state.epoch === epoch)
|
|
244
|
-
break;
|
|
245
|
-
if (Date.now() >= deadline)
|
|
246
|
-
throw timeout("activation", "timed out waiting for stream activation");
|
|
247
|
-
}
|
|
248
|
-
const handle = new StreamHandle(scope, service, {
|
|
249
|
-
producer: prepared,
|
|
250
|
-
socket,
|
|
251
|
-
generation: generation,
|
|
252
|
-
frame,
|
|
253
|
-
epoch,
|
|
254
|
-
sample,
|
|
255
|
-
sequence,
|
|
256
|
-
});
|
|
257
|
-
prepared = undefined;
|
|
258
|
-
socket = undefined;
|
|
259
|
-
return handle;
|
|
299
|
+
}, () => accepted);
|
|
300
|
+
return handleOf(scope, service, p, generation);
|
|
260
301
|
}
|
|
261
302
|
catch (error) {
|
|
262
303
|
// An abort can arrive before the host answered the selecting request: wait
|
|
263
304
|
// for that answer so a source selected afterwards is still closed.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// A cancelled open may already have selected this producer: close only
|
|
267
|
-
// that source, before its frame connection (as in `stop`).
|
|
268
|
-
if (generation !== undefined && prepared !== undefined)
|
|
269
|
-
await service.closeIfCurrent(generation).catch(() => { });
|
|
270
|
-
socket?.close();
|
|
271
|
-
if (prepared !== undefined)
|
|
272
|
-
await host.producer.release(prepared).catch(() => { });
|
|
305
|
+
await opened?.catch(() => { });
|
|
306
|
+
await abandon(service, p, generation);
|
|
273
307
|
throw error;
|
|
274
308
|
}
|
|
275
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* A file resolver's stream (`ResolvedStream`): prepared and prerolled while
|
|
312
|
+
* the open waits, then handed to the host as the answer. Once the host
|
|
313
|
+
* selected it, the start control is acknowledged and `started` gets the
|
|
314
|
+
* running stream, synchronously, before any `onStop`. Cancelled before or
|
|
315
|
+
* during activation (the open was replaced or closed, the plugin stopped),
|
|
316
|
+
* the producer is released and `started` is never called.
|
|
317
|
+
*/
|
|
318
|
+
export async function resolvedStream(scope, service, stream, signal) {
|
|
319
|
+
const host = service.runtime.host;
|
|
320
|
+
const p = await prepare(host, stream, () => {
|
|
321
|
+
if (signal.aborted || scope.disposed)
|
|
322
|
+
throw abortError();
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
kind: "stream",
|
|
326
|
+
producerId: p.producer,
|
|
327
|
+
abandoned: () => abandon(service, p, undefined),
|
|
328
|
+
accepted: async ({ stamp }) => {
|
|
329
|
+
try {
|
|
330
|
+
await activate(host, p, () => {
|
|
331
|
+
if (scope.disposed)
|
|
332
|
+
throw abortError();
|
|
333
|
+
}, "selected");
|
|
334
|
+
// Replaced before it ran: the source is no longer this stream.
|
|
335
|
+
if (service.raw.stamp.generation > stamp.generation)
|
|
336
|
+
throw abortError();
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
await abandon(service, p, stamp.generation);
|
|
340
|
+
if (error.code !== "aborted")
|
|
341
|
+
scope.report(error);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const handle = handleOf(scope, service, p, stamp.generation);
|
|
345
|
+
if (stream.started)
|
|
346
|
+
scope.run(stream.started, handle);
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -238,9 +238,16 @@ export declare class TestHost implements Transport {
|
|
|
238
238
|
* ```
|
|
239
239
|
*/
|
|
240
240
|
readonly layout: {
|
|
241
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* The bottom bar position: `preferred`, `effective`, `reason`,
|
|
243
|
+
* `detached`, `owner` and `panels` (its one panel).
|
|
244
|
+
*/
|
|
242
245
|
bottom: LayoutSlot | null;
|
|
243
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* The sidebar position: `preferred`, `effective`, `reason`, `detached`;
|
|
248
|
+
* `panels` are its tabs in order (`{ owner, plugin, title }`), `owner`
|
|
249
|
+
* the current tab's (the plugin under test is owner 1).
|
|
250
|
+
*/
|
|
244
251
|
sidebar: LayoutSlot | null;
|
|
245
252
|
};
|
|
246
253
|
/** Every `osd.show` text, newest last. */
|
|
@@ -554,6 +561,46 @@ export declare class TestHost implements Transport {
|
|
|
554
561
|
* appeared; `true`: there is none again); pushed to the plugin.
|
|
555
562
|
*/
|
|
556
563
|
setBackdropEffective(effective: boolean): void;
|
|
564
|
+
/**
|
|
565
|
+
* Simulate another plugin mounting a panel, before or after the plugin
|
|
566
|
+
* under test, as the player handles it: in the sidebar it is another tab
|
|
567
|
+
* (after the existing ones; the first panel's tab is current); the bottom
|
|
568
|
+
* bar and the backdrop take one panel, so the plugin's own mount there
|
|
569
|
+
* then rejects with `conflict` (not an activation failure, as in the
|
|
570
|
+
* player). Pushed as a `window.state` change.
|
|
571
|
+
*
|
|
572
|
+
* @param slot - The position.
|
|
573
|
+
* @param plugin - The other plugin's manifest id.
|
|
574
|
+
* @param options - `title`: its tab label (default: `plugin`).
|
|
575
|
+
*
|
|
576
|
+
* @example The playlist next to this plugin's sidebar panel
|
|
577
|
+
* ```ts
|
|
578
|
+
* const host = new TestHost();
|
|
579
|
+
* host.mountOther("sidebar", "vestra.playlist", { title: "播放列表" });
|
|
580
|
+
* const { plugin } = await startPlugin(activate, { host, directory });
|
|
581
|
+
* assert.equal(host.layout.sidebar?.panels?.length, 2);
|
|
582
|
+
* ```
|
|
583
|
+
*
|
|
584
|
+
* @since 1.5.2
|
|
585
|
+
*/
|
|
586
|
+
mountOther(slot: "bottom" | "sidebar" | "backdrop", plugin: string, options?: {
|
|
587
|
+
title?: string;
|
|
588
|
+
}): void;
|
|
589
|
+
/**
|
|
590
|
+
* Simulate another plugin's panel going away ({@link TestHost.mountOther}):
|
|
591
|
+
* when its tab was current, the next tab (else the previous one) is.
|
|
592
|
+
*
|
|
593
|
+
* @since 1.5.2
|
|
594
|
+
*/
|
|
595
|
+
unmountOther(plugin: string): void;
|
|
596
|
+
/**
|
|
597
|
+
* Simulate the user clicking a sidebar tab: `plugin`'s panel (the plugin
|
|
598
|
+
* under test's `manifestId`, or one of {@link TestHost.mountOther})
|
|
599
|
+
* becomes the current tab. Pushed as a `window.state` change.
|
|
600
|
+
*
|
|
601
|
+
* @since 1.5.2
|
|
602
|
+
*/
|
|
603
|
+
selectTab(plugin: string): void;
|
|
557
604
|
/**
|
|
558
605
|
* The display layer (`plugin.video.view()`): who owns it (this plugin is
|
|
559
606
|
* `manifestId`) and the values the player holds.
|
|
@@ -697,6 +744,45 @@ export declare class TestHost implements Transport {
|
|
|
697
744
|
}>;
|
|
698
745
|
/** Registered source resolvers (`resolvers.add`) by id: their declarations. */
|
|
699
746
|
readonly resolvers: Map<string, Json>;
|
|
747
|
+
/**
|
|
748
|
+
* Resolvers other plugins hold, as `resolvers.add` declares them plus the
|
|
749
|
+
* holder's id: a registration of this plugin that claims one of their
|
|
750
|
+
* extensions (or scheme + host pairs) is refused with `conflict`, which
|
|
751
|
+
* does not fail activation, like the player.
|
|
752
|
+
*
|
|
753
|
+
* @example
|
|
754
|
+
* ```ts
|
|
755
|
+
* const host = new TestHost();
|
|
756
|
+
* host.otherResolvers.push({ owner: "vestra.nes", extensions: ["nes"] });
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
readonly otherResolvers: {
|
|
760
|
+
/** The manifest id of the plugin holding them (shown in the refusal). */
|
|
761
|
+
owner: string;
|
|
762
|
+
/** File extensions it resolves (a file resolver), lower case, no dot. */
|
|
763
|
+
extensions?: string[];
|
|
764
|
+
/** URL schemes it resolves (a web resolver), with `hosts`. */
|
|
765
|
+
schemes?: string[];
|
|
766
|
+
/** Exact host names it resolves (a web resolver), with `schemes`. */
|
|
767
|
+
hosts?: string[];
|
|
768
|
+
}[];
|
|
769
|
+
/**
|
|
770
|
+
* File opens waiting for this plugin's file resolver (`extensions`), by
|
|
771
|
+
* request id: the path and how it was opened. Opening a matching path
|
|
772
|
+
* (`player.open`, {@link TestHost.openElsewhere}) sends
|
|
773
|
+
* `source.resolveRequest`; the answer selects the stream, fails the open
|
|
774
|
+
* or, once the source changed, is `stale`. Any later source change
|
|
775
|
+
* cancels it (`source.resolveCancel`). Web resolvers are not simulated:
|
|
776
|
+
* emit `source.resolveRequest` yourself.
|
|
777
|
+
*/
|
|
778
|
+
readonly resolutions: Map<number, {
|
|
779
|
+
/** The id of this plugin's resolver asked (`resolvers.add` `id`). */
|
|
780
|
+
resolverId: string;
|
|
781
|
+
/** The path being opened, as sent to `resolve`. */
|
|
782
|
+
source: string;
|
|
783
|
+
/** The open asked for a paused start (`player.open(path, { paused })`). */
|
|
784
|
+
paused: boolean;
|
|
785
|
+
}>;
|
|
700
786
|
/**
|
|
701
787
|
* Answers for `dialogs.open` (paths, `null` = cancelled) and
|
|
702
788
|
* `dialogs.confirm` (`{ confirmed, checked }`), consumed in order. A
|