@vestra-player/sdk 1.5.25 → 1.5.27
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 +17 -0
- package/bin/vestra-plugin.mjs +85 -10
- package/dist/api-requests.d.ts +36 -0
- package/dist/api-requests.js +143 -0
- package/dist/capabilities.d.ts +6 -3
- package/dist/capabilities.js +16 -230
- package/dist/capability-check.d.ts +85 -0
- package/dist/capability-check.js +203 -0
- package/dist/capability-rules.d.ts +102 -0
- package/dist/capability-rules.js +132 -0
- package/dist/host-capabilities.d.ts +10 -0
- package/dist/host-capabilities.js +360 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/stream.js +56 -8
- package/dist/plugin/types.d.ts +2 -1
- package/dist/ts-plugin.d.ts +23 -0
- package/dist/ts-plugin.js +135 -0
- package/llms-full.txt +52 -7
- package/llms.txt +23 -23
- package/package.json +9 -3
- package/ts-plugin/index.d.ts +8 -0
- package/ts-plugin/index.js +6 -0
- package/ts-plugin/package.json +3 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capability rule, as data: which manifest capability a request needs
|
|
3
|
+
* (the player's table, `HOST_CAPABILITIES`, synced from its
|
|
4
|
+
* `src/host/capabilities.json`) and which requests each public member of the
|
|
5
|
+
* plugin API sends (`API_REQUESTS`). `CAPABILITY_INFO`, `vestra-plugin check`
|
|
6
|
+
* and the TypeScript language-service plugin are all built from these two
|
|
7
|
+
* tables, so they and the player cannot disagree.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
import { API_REQUESTS } from "./api-requests.js";
|
|
12
|
+
import { HOST_CAPABILITIES } from "./host-capabilities.js";
|
|
13
|
+
/** @internal The SDK's rules: the synced player table and `API_REQUESTS`. */
|
|
14
|
+
export const RULES = {
|
|
15
|
+
host: HOST_CAPABILITIES,
|
|
16
|
+
apis: API_REQUESTS,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The player's entry for a method, as the player routes it: the exact name
|
|
20
|
+
* first, then the first `prefix.*` entry whose prefix it starts with.
|
|
21
|
+
* `undefined`: the player does not know the method.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function hostMethod(method, host = RULES.host) {
|
|
26
|
+
const all = [
|
|
27
|
+
...host.capabilities.flatMap((capability) => capability.methods),
|
|
28
|
+
...host.uncapable,
|
|
29
|
+
];
|
|
30
|
+
return (all.find((entry) => entry.name === method) ??
|
|
31
|
+
all.find((entry) => entry.name.endsWith(".*") && method.startsWith(entry.name.slice(0, -1))));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The capability one request needs, as the player decides it
|
|
35
|
+
* (`capabilities::Route::needs`): `null` for none, `undefined` for a method
|
|
36
|
+
* the player does not know.
|
|
37
|
+
*
|
|
38
|
+
* @param method - The wire method.
|
|
39
|
+
* @param params - Its string parameters that a `Needs::Param` rule reads.
|
|
40
|
+
* @param declared - Whether the plugin declared a capability.
|
|
41
|
+
*
|
|
42
|
+
* @internal
|
|
43
|
+
*/
|
|
44
|
+
export function requestNeeds(method, params, declared, host = RULES.host) {
|
|
45
|
+
const entry = hostMethod(method, host);
|
|
46
|
+
if (!entry)
|
|
47
|
+
return undefined;
|
|
48
|
+
const needs = entry.needs;
|
|
49
|
+
if (needs === null || typeof needs === "string")
|
|
50
|
+
return needs;
|
|
51
|
+
if ("param" in needs) {
|
|
52
|
+
const value = params?.[needs.param];
|
|
53
|
+
return value !== undefined && Object.hasOwn(needs.cases, value)
|
|
54
|
+
? needs.cases[value]
|
|
55
|
+
: needs.otherwise;
|
|
56
|
+
}
|
|
57
|
+
return declared(needs.ifDeclared) ? needs.then : needs.otherwise;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Every capability a member of the plugin API needs with the given manifest:
|
|
61
|
+
* those its requests need and, through `requires`, those they need in turn.
|
|
62
|
+
* `undefined`: the member is not in the table (it needs none).
|
|
63
|
+
*
|
|
64
|
+
* @param api - The member as `Interface.member` (`Player.seek`).
|
|
65
|
+
* @param capabilities - The manifest's capabilities.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
69
|
+
export function memberNeeds(api, capabilities, rules = RULES) {
|
|
70
|
+
const requests = rules.apis[api];
|
|
71
|
+
if (!requests)
|
|
72
|
+
return undefined;
|
|
73
|
+
// An `ifDeclared` rule is decided last, as if the plugin had declared what
|
|
74
|
+
// the member's other requests need: `Player.seek` needs `playback.control`
|
|
75
|
+
// for `source.submit`, and with it `source.operation` needs nothing more.
|
|
76
|
+
const declared = (name) => capabilities.includes(name) ||
|
|
77
|
+
needs.some((need) => need.capability === name);
|
|
78
|
+
const conditional = (request) => {
|
|
79
|
+
const rule = hostMethod(request.method, rules.host)?.needs;
|
|
80
|
+
return typeof rule === "object" && rule !== null && "ifDeclared" in rule;
|
|
81
|
+
};
|
|
82
|
+
const needs = [];
|
|
83
|
+
const add = (need) => {
|
|
84
|
+
if (needs.some((each) => each.capability === need.capability))
|
|
85
|
+
return;
|
|
86
|
+
needs.push(need);
|
|
87
|
+
const entry = rules.host.capabilities.find((capability) => capability.name === need.capability);
|
|
88
|
+
for (const required of entry?.requires ?? [])
|
|
89
|
+
add({
|
|
90
|
+
capability: required,
|
|
91
|
+
method: need.method,
|
|
92
|
+
requiredBy: need.capability,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
for (const request of [
|
|
96
|
+
...requests.filter((request) => !conditional(request)),
|
|
97
|
+
...requests.filter(conditional),
|
|
98
|
+
]) {
|
|
99
|
+
const capability = requestNeeds(request.method, request.params, declared, rules.host);
|
|
100
|
+
if (capability)
|
|
101
|
+
add({ capability, method: request.method });
|
|
102
|
+
}
|
|
103
|
+
return needs;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `CAPABILITY_INFO` from the tables: the label, highlight and requirements
|
|
107
|
+
* from the player's table; the members from `API_REQUESTS` (every member a
|
|
108
|
+
* request of which needs the capability, in the table's order; requests
|
|
109
|
+
* marked `also` are left out, as they are documented at the member).
|
|
110
|
+
*
|
|
111
|
+
* @internal
|
|
112
|
+
*/
|
|
113
|
+
export function capabilityInfo(rules = RULES) {
|
|
114
|
+
const info = {};
|
|
115
|
+
for (const capability of rules.host.capabilities)
|
|
116
|
+
info[capability.name] = {
|
|
117
|
+
apis: [],
|
|
118
|
+
requires: [...capability.requires],
|
|
119
|
+
label: capability.label,
|
|
120
|
+
risky: capability.risky,
|
|
121
|
+
};
|
|
122
|
+
for (const [api, requests] of Object.entries(rules.apis))
|
|
123
|
+
for (const request of requests) {
|
|
124
|
+
if (request.also)
|
|
125
|
+
continue;
|
|
126
|
+
const capability = requestNeeds(request.method, request.params, () => true, rules.host);
|
|
127
|
+
const entry = capability ? info[capability] : undefined;
|
|
128
|
+
if (entry && !entry.apis.includes(api))
|
|
129
|
+
entry.apis.push(api);
|
|
130
|
+
}
|
|
131
|
+
return info;
|
|
132
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The player's capability table, copied from its `src/host/capabilities.json`
|
|
3
|
+
* by `npm run capabilities:sync -- <player repository>`. Do not edit: change
|
|
4
|
+
* the player's capability modules, regenerate its JSON and sync again.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
import type { HostCapabilities } from "./capability-rules.ts";
|
|
9
|
+
/** @internal */
|
|
10
|
+
export declare const HOST_CAPABILITIES: HostCapabilities;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// "then" is a field of the player's IfDeclared rule, not a thenable.
|
|
2
|
+
/* oxlint-disable unicorn/no-thenable */
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const HOST_CAPABILITIES = {
|
|
5
|
+
about: "Generated from src/host/src/capabilities/ (VESTRA_UPDATE_CAPABILITIES=1 cargo test capabilities); do not edit. A method's needs: null (none), a capability name, {param, cases, otherwise} (by a string parameter) or {ifDeclared, then, otherwise}. A method name ending in .* covers the prefix's other methods.",
|
|
6
|
+
apiVersion: "vestra/1",
|
|
7
|
+
capabilities: [
|
|
8
|
+
{
|
|
9
|
+
label: "Provide audio and video it generates itself",
|
|
10
|
+
methods: [
|
|
11
|
+
{ name: "producer.prepare", needs: "producer.av" },
|
|
12
|
+
{ name: "producer.state", needs: "producer.av" },
|
|
13
|
+
{ name: "producer.release", needs: "producer.av" },
|
|
14
|
+
{ name: "producer.ack", needs: "producer.av" },
|
|
15
|
+
{ name: "producer.declareInput", needs: "producer.av" },
|
|
16
|
+
{ name: "producer.ackInput", needs: "producer.av" },
|
|
17
|
+
],
|
|
18
|
+
name: "producer.av",
|
|
19
|
+
requires: ["playback.control"],
|
|
20
|
+
residency: "live",
|
|
21
|
+
risky: false,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "Read what is playing and the position",
|
|
25
|
+
methods: [
|
|
26
|
+
{ name: "source.state", needs: "playback.read" },
|
|
27
|
+
{ name: "processing.state", needs: "playback.read" },
|
|
28
|
+
{ name: "playback.stats", needs: "playback.read" },
|
|
29
|
+
{ name: "player.chapters", needs: "playback.read" },
|
|
30
|
+
{ name: "player.audioDevices", needs: "playback.read" },
|
|
31
|
+
{ name: "timeline.marks", needs: "playback.read" },
|
|
32
|
+
],
|
|
33
|
+
name: "playback.read",
|
|
34
|
+
requires: [],
|
|
35
|
+
residency: "none",
|
|
36
|
+
risky: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "Open media, control playback, keep the computer awake",
|
|
40
|
+
methods: [
|
|
41
|
+
{ name: "playback.set", needs: "playback.control" },
|
|
42
|
+
{ name: "source.submit", needs: "playback.control" },
|
|
43
|
+
{
|
|
44
|
+
name: "source.operation",
|
|
45
|
+
needs: {
|
|
46
|
+
ifDeclared: "playback.control",
|
|
47
|
+
otherwise: "playback.read",
|
|
48
|
+
then: "playback.control",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{ name: "audio.addTrack", needs: "playback.control" },
|
|
52
|
+
{ name: "audio.selectTrack", needs: "playback.control" },
|
|
53
|
+
{ name: "player.setLoop", needs: "playback.control" },
|
|
54
|
+
{ name: "player.setAudioDevice", needs: "playback.control" },
|
|
55
|
+
{ name: "player.step", needs: "playback.control" },
|
|
56
|
+
{ name: "player.setStepSeek", needs: "playback.control" },
|
|
57
|
+
{ name: "power.preventSleep", needs: "playback.control" },
|
|
58
|
+
{ name: "sequence.set", needs: "playback.control" },
|
|
59
|
+
{ name: "sequence.step", needs: "playback.control" },
|
|
60
|
+
],
|
|
61
|
+
name: "playback.control",
|
|
62
|
+
requires: [],
|
|
63
|
+
residency: "live",
|
|
64
|
+
risky: false,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
label: "Resolve online video addresses",
|
|
68
|
+
methods: [
|
|
69
|
+
{ name: "sourceResolvers.register", needs: "source.resolve" },
|
|
70
|
+
{ name: "sourceResolvers.unregister", needs: "source.resolve" },
|
|
71
|
+
{ name: "sourceResolvers.respond", needs: "source.resolve" },
|
|
72
|
+
{ name: "sourceResolvers.*", needs: "source.resolve" },
|
|
73
|
+
],
|
|
74
|
+
name: "source.resolve",
|
|
75
|
+
requires: [],
|
|
76
|
+
residency: "recordable",
|
|
77
|
+
risky: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "Receive playback changes as they happen",
|
|
81
|
+
methods: [
|
|
82
|
+
{
|
|
83
|
+
name: "events.subscribe",
|
|
84
|
+
needs: {
|
|
85
|
+
cases: {
|
|
86
|
+
"bindings.changed": "bindings.manage",
|
|
87
|
+
"packages.changed": null,
|
|
88
|
+
},
|
|
89
|
+
otherwise: "events",
|
|
90
|
+
param: "event",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "events.unsubscribe",
|
|
95
|
+
needs: {
|
|
96
|
+
cases: {
|
|
97
|
+
"bindings.changed": "bindings.manage",
|
|
98
|
+
"packages.changed": null,
|
|
99
|
+
},
|
|
100
|
+
otherwise: "events",
|
|
101
|
+
param: "event",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
name: "events",
|
|
106
|
+
requires: [],
|
|
107
|
+
residency: "live",
|
|
108
|
+
risky: false,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: "Add menu commands and shortcuts",
|
|
112
|
+
methods: [
|
|
113
|
+
{ name: "commands.register", needs: "commands" },
|
|
114
|
+
{ name: "commands.update", needs: "commands" },
|
|
115
|
+
{ name: "commands.unregister", needs: "commands" },
|
|
116
|
+
{ name: "host.run", needs: "commands" },
|
|
117
|
+
],
|
|
118
|
+
name: "commands",
|
|
119
|
+
requires: [],
|
|
120
|
+
residency: "recordable",
|
|
121
|
+
risky: false,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: "Open tool windows",
|
|
125
|
+
methods: [
|
|
126
|
+
{ name: "ui.open", needs: "ui.window" },
|
|
127
|
+
{ name: "ui.set", needs: "ui.window" },
|
|
128
|
+
{ name: "ui.show", needs: "ui.window" },
|
|
129
|
+
{ name: "ui.close", needs: "ui.window" },
|
|
130
|
+
],
|
|
131
|
+
name: "ui.window",
|
|
132
|
+
requires: [],
|
|
133
|
+
residency: "live",
|
|
134
|
+
risky: false,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
label: "Show panels in the bottom bar, the sidebar or behind the video",
|
|
138
|
+
methods: [
|
|
139
|
+
{ name: "ui.mount", needs: "ui.mount" },
|
|
140
|
+
{ name: "ui.setMount", needs: "ui.mount" },
|
|
141
|
+
{ name: "ui.unmount", needs: "ui.mount" },
|
|
142
|
+
{ name: "ui.interaction", needs: "ui.mount" },
|
|
143
|
+
{ name: "ui.menu", needs: "ui.mount" },
|
|
144
|
+
{ name: "layout.state", needs: "ui.mount" },
|
|
145
|
+
{ name: "layout.set", needs: "ui.mount" },
|
|
146
|
+
],
|
|
147
|
+
name: "ui.mount",
|
|
148
|
+
requires: [],
|
|
149
|
+
residency: "live",
|
|
150
|
+
risky: false,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: "Show file pickers and confirmations",
|
|
154
|
+
methods: [
|
|
155
|
+
{ name: "dialogs.open", needs: "ui.dialog" },
|
|
156
|
+
{ name: "dialogs.confirm", needs: "ui.dialog" },
|
|
157
|
+
],
|
|
158
|
+
name: "ui.dialog",
|
|
159
|
+
requires: [],
|
|
160
|
+
residency: "none",
|
|
161
|
+
risky: false,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
label: "Install, uninstall and disable other plugins",
|
|
165
|
+
methods: [{ name: "packages.*", needs: "plugins.manage" }],
|
|
166
|
+
name: "plugins.manage",
|
|
167
|
+
requires: [],
|
|
168
|
+
residency: "none",
|
|
169
|
+
risky: true,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
label: "Process the picture (filters, shaders)",
|
|
173
|
+
methods: [
|
|
174
|
+
{
|
|
175
|
+
name: "processing.replace",
|
|
176
|
+
needs: {
|
|
177
|
+
cases: { video: "processing.video" },
|
|
178
|
+
otherwise: "processing.audio",
|
|
179
|
+
param: "stage",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "processing.operation",
|
|
184
|
+
needs: {
|
|
185
|
+
cases: { video: "processing.video" },
|
|
186
|
+
otherwise: "processing.audio",
|
|
187
|
+
param: "stage",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{ name: "video.view.set", needs: "processing.video" },
|
|
191
|
+
{ name: "video.view.release", needs: "processing.video" },
|
|
192
|
+
{ name: "video.render.set", needs: "processing.video" },
|
|
193
|
+
{ name: "video.render.release", needs: "processing.video" },
|
|
194
|
+
],
|
|
195
|
+
name: "processing.video",
|
|
196
|
+
requires: ["playback.read"],
|
|
197
|
+
residency: "live",
|
|
198
|
+
risky: false,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
label: "Process the sound",
|
|
202
|
+
methods: [],
|
|
203
|
+
name: "processing.audio",
|
|
204
|
+
requires: ["playback.read", "events"],
|
|
205
|
+
residency: "live",
|
|
206
|
+
risky: false,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
label: "Read subtitles",
|
|
210
|
+
methods: [{ name: "subtitles.state", needs: "subtitles.read" }],
|
|
211
|
+
name: "subtitles.read",
|
|
212
|
+
requires: [],
|
|
213
|
+
residency: "none",
|
|
214
|
+
risky: false,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
label: "Load and change subtitles",
|
|
218
|
+
methods: [{ name: "subtitles.*", needs: "subtitles.control" }],
|
|
219
|
+
name: "subtitles.control",
|
|
220
|
+
requires: [],
|
|
221
|
+
residency: "live",
|
|
222
|
+
risky: false,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
label: "Take snapshots and export clips",
|
|
226
|
+
methods: [
|
|
227
|
+
{ name: "snapshot.state", needs: "media.read" },
|
|
228
|
+
{ name: "jobs.state", needs: "media.read" },
|
|
229
|
+
{ name: "jobs.operation", needs: "media.read" },
|
|
230
|
+
],
|
|
231
|
+
name: "media.read",
|
|
232
|
+
requires: ["media.control"],
|
|
233
|
+
residency: "none",
|
|
234
|
+
risky: false,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
label: "Take snapshots and export clips",
|
|
238
|
+
methods: [
|
|
239
|
+
{ name: "snapshot.*", needs: "media.control" },
|
|
240
|
+
{ name: "jobs.*", needs: "media.control" },
|
|
241
|
+
],
|
|
242
|
+
name: "media.control",
|
|
243
|
+
requires: ["media.read"],
|
|
244
|
+
residency: "live",
|
|
245
|
+
risky: false,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
label: "Show messages on the video",
|
|
249
|
+
methods: [
|
|
250
|
+
{ name: "osd.show", needs: "ui.osd" },
|
|
251
|
+
{ name: "osd.panel", needs: "ui.osd" },
|
|
252
|
+
],
|
|
253
|
+
name: "ui.osd",
|
|
254
|
+
requires: [],
|
|
255
|
+
residency: "live",
|
|
256
|
+
risky: false,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
label: "Read and write the clipboard",
|
|
260
|
+
methods: [
|
|
261
|
+
{ name: "clipboard.readText", needs: "clipboard" },
|
|
262
|
+
{ name: "clipboard.writeText", needs: "clipboard" },
|
|
263
|
+
{ name: "clipboard.writeImage", needs: "clipboard" },
|
|
264
|
+
],
|
|
265
|
+
name: "clipboard",
|
|
266
|
+
requires: [],
|
|
267
|
+
residency: "none",
|
|
268
|
+
risky: true,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
label: "Adjust the main window (size, on top, picture-in-picture)",
|
|
272
|
+
methods: [
|
|
273
|
+
{ name: "window.setVideoArea", needs: "window.control" },
|
|
274
|
+
{ name: "window.setAlwaysOnTop", needs: "window.control" },
|
|
275
|
+
{ name: "window.setCursorAutoHide", needs: "window.control" },
|
|
276
|
+
{ name: "window.setPictureInPicture", needs: "window.control" },
|
|
277
|
+
],
|
|
278
|
+
name: "window.control",
|
|
279
|
+
requires: [],
|
|
280
|
+
residency: "live",
|
|
281
|
+
risky: false,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
label: "Change all shortcuts",
|
|
285
|
+
methods: [
|
|
286
|
+
{ name: "bindings.list", needs: "bindings.manage" },
|
|
287
|
+
{ name: "bindings.set", needs: "bindings.manage" },
|
|
288
|
+
{ name: "bindings.reset", needs: "bindings.manage" },
|
|
289
|
+
],
|
|
290
|
+
name: "bindings.manage",
|
|
291
|
+
requires: [],
|
|
292
|
+
residency: "none",
|
|
293
|
+
risky: true,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
label: "Show marks on the progress bar",
|
|
297
|
+
methods: [{ name: "timeline.set", needs: "timeline.marks" }],
|
|
298
|
+
name: "timeline.marks",
|
|
299
|
+
requires: [],
|
|
300
|
+
residency: "live",
|
|
301
|
+
risky: false,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
label: "Receive files dropped on the player",
|
|
305
|
+
methods: [
|
|
306
|
+
{ name: "drops.provide", needs: "ui.drop" },
|
|
307
|
+
{ name: "drops.withdraw", needs: "ui.drop" },
|
|
308
|
+
{ name: "drops.listen", needs: "ui.drop" },
|
|
309
|
+
],
|
|
310
|
+
name: "ui.drop",
|
|
311
|
+
requires: [],
|
|
312
|
+
residency: "recordable",
|
|
313
|
+
risky: false,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
label: "Add pages to the Options window",
|
|
317
|
+
methods: [{ name: "settings.*", needs: "ui.settings" }],
|
|
318
|
+
name: "ui.settings",
|
|
319
|
+
requires: [],
|
|
320
|
+
residency: "recordable",
|
|
321
|
+
risky: false,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
label: "Change the player's palette",
|
|
325
|
+
methods: [
|
|
326
|
+
{ name: "appearance.set", needs: "ui.theme" },
|
|
327
|
+
{ name: "appearance.setLanguage", needs: "ui.theme" },
|
|
328
|
+
],
|
|
329
|
+
name: "ui.theme",
|
|
330
|
+
requires: [],
|
|
331
|
+
residency: "none",
|
|
332
|
+
risky: false,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
label: "Store sign-in details in the system keychain",
|
|
336
|
+
methods: [
|
|
337
|
+
{ name: "secrets.get", needs: "secrets" },
|
|
338
|
+
{ name: "secrets.set", needs: "secrets" },
|
|
339
|
+
{ name: "secrets.delete", needs: "secrets" },
|
|
340
|
+
],
|
|
341
|
+
name: "secrets",
|
|
342
|
+
requires: [],
|
|
343
|
+
residency: "none",
|
|
344
|
+
risky: true,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
uncapable: [
|
|
348
|
+
{ name: "packages.list", needs: null },
|
|
349
|
+
{ name: "window.state", needs: null },
|
|
350
|
+
{ name: "system.reveal", needs: null },
|
|
351
|
+
{ name: "system.openPath", needs: null },
|
|
352
|
+
{ name: "system.openUrl", needs: null },
|
|
353
|
+
{ name: "app.system", needs: null },
|
|
354
|
+
{ name: "ui.markdownScan", needs: null },
|
|
355
|
+
{ name: "storage.session.get", needs: null },
|
|
356
|
+
{ name: "storage.session.set", needs: null },
|
|
357
|
+
{ name: "storage.session.delete", needs: null },
|
|
358
|
+
{ name: "storage.session.keys", needs: null },
|
|
359
|
+
],
|
|
360
|
+
};
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type { PluginInstance, PluginManifest } from "./plugin/index.ts";
|
|
|
12
12
|
export { HostError } from "./wire.ts";
|
|
13
13
|
export type { Json, Transport } from "./wire.ts";
|
|
14
14
|
export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.ts";
|
|
15
|
+
export { HOST_CAPABILITIES } from "./host-capabilities.ts";
|
|
15
16
|
export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
|
|
16
17
|
export { BINDING_INPUTS } from "./plugin/bindings.ts";
|
|
17
18
|
export { OSD_LIMITS } from "./plugin/osd.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export { createPlugin } from "./plugin/index.js";
|
|
11
11
|
export { HostError } from "./wire.js";
|
|
12
12
|
export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.js";
|
|
13
|
+
export { HOST_CAPABILITIES } from "./host-capabilities.js";
|
|
13
14
|
export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
|
|
14
15
|
export { BINDING_INPUTS } from "./plugin/bindings.js";
|
|
15
16
|
export { OSD_LIMITS } from "./plugin/osd.js";
|
package/dist/plugin/stream.js
CHANGED
|
@@ -5,8 +5,33 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { failure, sleep, toError } from "./runtime.js";
|
|
7
7
|
import { abortError } from "./player.js";
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Frames of audio buffered ahead of the device before rendering pauses
|
|
10
|
+
* (50 ms). Each pump refills the whole lead, so it must outlast one pump
|
|
11
|
+
* period: the pump itself (a `producer.state` round trip and the frames),
|
|
12
|
+
* plus a Windows timer tick (15.6 ms) unless the pump rendered and already
|
|
13
|
+
* took {@link PERIOD}. Measured with a
|
|
14
|
+
* real-time clock, round trips up to about 30 ms never let the device run
|
|
15
|
+
* dry; near 50 ms they do now and then. A larger lead would add latency
|
|
16
|
+
* between input and picture.
|
|
17
|
+
*/
|
|
9
18
|
const AHEAD = 2400;
|
|
19
|
+
/**
|
|
20
|
+
* At most this many frames per pump. A lead is a few frames (three of
|
|
21
|
+
* 1/60 s); the cap only stops a plugin whose frames carry next to no audio
|
|
22
|
+
* from rendering without pause.
|
|
23
|
+
*/
|
|
24
|
+
const BATCH = 16;
|
|
25
|
+
/**
|
|
26
|
+
* Milliseconds from the start of one pump to the start of the next. A pump
|
|
27
|
+
* that rendered frames and already took longer is followed at once: a timer
|
|
28
|
+
* on top would add a whole Windows tick (15.6 ms; a timer there never fires
|
|
29
|
+
* sooner) to every pump while playing, and the period must stay well inside
|
|
30
|
+
* the lead. A pump that rendered nothing (paused, lead full, a control not
|
|
31
|
+
* yet acknowledged) waits this long after it ends, so an idle stream polls
|
|
32
|
+
* the player no more often than it always did.
|
|
33
|
+
*/
|
|
34
|
+
const PERIOD = 5;
|
|
10
35
|
/** Frames written before the host is asked to select the stream. */
|
|
11
36
|
const PREROLL = 2048;
|
|
12
37
|
class StreamHandle {
|
|
@@ -23,6 +48,7 @@ class StreamHandle {
|
|
|
23
48
|
#sample;
|
|
24
49
|
#sequence;
|
|
25
50
|
#timer;
|
|
51
|
+
#immediate;
|
|
26
52
|
#pumping = Promise.resolve();
|
|
27
53
|
constructor(scope, service, setup) {
|
|
28
54
|
this.#scope = scope;
|
|
@@ -48,6 +74,7 @@ class StreamHandle {
|
|
|
48
74
|
}
|
|
49
75
|
async #pump() {
|
|
50
76
|
const producer = this.#service.runtime.host.producer;
|
|
77
|
+
const begun = performance.now();
|
|
51
78
|
try {
|
|
52
79
|
const state = await producer.state(this.#producer);
|
|
53
80
|
if (!this.#running)
|
|
@@ -67,16 +94,36 @@ class StreamHandle {
|
|
|
67
94
|
if (state.input?.pending)
|
|
68
95
|
await producer.ackInput(state.input.pending);
|
|
69
96
|
const paused = control ? control.paused : state.paused;
|
|
97
|
+
let rendered = 0;
|
|
98
|
+
// Refill the whole lead now, not one frame per pump: a pump period (a
|
|
99
|
+
// round trip, and a timer tick after a short pump: 16–40 ms on Windows)
|
|
100
|
+
// is longer than a 1/60 s frame. No control lands while this loop runs: the next one is
|
|
101
|
+
// read by the next pump, at a frame boundary.
|
|
70
102
|
if (!control &&
|
|
71
103
|
state.acknowledged &&
|
|
72
104
|
state.epoch === this.#epoch &&
|
|
73
|
-
!paused
|
|
74
|
-
this.#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
!paused)
|
|
106
|
+
while (this.#running &&
|
|
107
|
+
rendered < BATCH &&
|
|
108
|
+
this.#sample - state.consumedFrames < AHEAD) {
|
|
109
|
+
const before = this.#sample;
|
|
110
|
+
await this.#render();
|
|
111
|
+
// A frame without audio does not move the clock: wait for the next
|
|
112
|
+
// pump, after a full PERIOD (it does not count as rendered).
|
|
113
|
+
if (this.#sample === before)
|
|
114
|
+
break;
|
|
115
|
+
rendered++;
|
|
116
|
+
}
|
|
117
|
+
if (!this.#running)
|
|
118
|
+
return;
|
|
119
|
+
const next = () => {
|
|
120
|
+
this.#pumping = this.#pump();
|
|
121
|
+
};
|
|
122
|
+
const wait = rendered ? PERIOD - (performance.now() - begun) : PERIOD;
|
|
123
|
+
if (wait > 0)
|
|
124
|
+
this.#timer = setTimeout(next, wait);
|
|
125
|
+
else
|
|
126
|
+
this.#immediate = setImmediate(next);
|
|
80
127
|
}
|
|
81
128
|
catch (error) {
|
|
82
129
|
const retired = await retirement(this.#service, this.#generation, error);
|
|
@@ -114,6 +161,7 @@ class StreamHandle {
|
|
|
114
161
|
this.#stopping ??= (async () => {
|
|
115
162
|
this.#running = false;
|
|
116
163
|
clearTimeout(this.#timer);
|
|
164
|
+
clearImmediate(this.#immediate);
|
|
117
165
|
await this.#pumping.catch(() => { });
|
|
118
166
|
try {
|
|
119
167
|
await this.#service.closeIfCurrent(this.#generation);
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -4390,7 +4390,8 @@ export interface SubtitleConditional {
|
|
|
4390
4390
|
}
|
|
4391
4391
|
/**
|
|
4392
4392
|
* `plugin.subtitles`: tracks, uploads and style. Reading needs
|
|
4393
|
-
* `subtitles.read
|
|
4393
|
+
* `subtitles.read`; changes need `subtitles.control` and also
|
|
4394
|
+
* `subtitles.read`, since they read the current selection first.
|
|
4394
4395
|
*
|
|
4395
4396
|
* @example Load an SRT file as the secondary subtitle
|
|
4396
4397
|
* ```ts
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TypeScript language-service plugin `@vestra-player/sdk/ts-plugin`: the
|
|
3
|
+
* capability checker of `vestra-plugin check` in the editor. A use of an API
|
|
4
|
+
* member whose capability `manifest.json` does not declare is an error at
|
|
5
|
+
* the member, with the quick fix "Add '<capability>' to manifest.json".
|
|
6
|
+
*
|
|
7
|
+
* A project enables it in its tsconfig.json:
|
|
8
|
+
*
|
|
9
|
+
* { "compilerOptions": { "plugins": [{ "name": "@vestra-player/sdk/ts-plugin" }] } }
|
|
10
|
+
*
|
|
11
|
+
* The editor must run the project's own TypeScript (VS Code: "TypeScript:
|
|
12
|
+
* Select TypeScript Version" → "Use Workspace Version"); `tsc` ignores
|
|
13
|
+
* language-service plugins, which is why `vestra-plugin check` runs the same
|
|
14
|
+
* checker. The manifest is the nearest `manifest.json` above the file; files
|
|
15
|
+
* in its `tests/`, `node_modules/`, `dist/` and `vendor/` are not checked.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
import type TS from "typescript";
|
|
20
|
+
/** @internal The plugin factory tsserver calls (`ts-plugin/index.js`). */
|
|
21
|
+
export default function init(modules: {
|
|
22
|
+
typescript: typeof TS;
|
|
23
|
+
}): TS.server.PluginModule;
|