@vestra-player/sdk 1.5.24 → 1.5.26
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 +19 -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 +2 -1
- package/dist/player-runtime.js +2 -1
- package/dist/plugin/services.d.ts +35 -0
- package/dist/plugin/services.js +87 -2
- package/dist/plugin/testing.d.ts +24 -1
- package/dist/plugin/testing.js +71 -1
- package/dist/plugin/types.d.ts +79 -1
- package/dist/ts-plugin.d.ts +23 -0
- package/dist/ts-plugin.js +135 -0
- package/dist/wire.d.ts +12 -0
- package/dist/wire.js +10 -0
- package/llms-full.txt +201 -8
- 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,10 +12,11 @@ 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";
|
|
18
|
-
export { SECRET_LIMITS } from "./plugin/services.ts";
|
|
19
|
+
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.ts";
|
|
19
20
|
export { THEME_PALETTES } from "./palettes.ts";
|
|
20
21
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.ts";
|
|
21
22
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
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";
|
|
16
|
-
export { SECRET_LIMITS } from "./plugin/services.js";
|
|
17
|
+
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.js";
|
|
17
18
|
export { THEME_PALETTES } from "./palettes.js";
|
|
18
19
|
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.js";
|
|
19
20
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.js";
|
|
@@ -4,6 +4,41 @@ export declare const clipboard: (scope: ScopeImpl) => Clipboard;
|
|
|
4
4
|
export declare const host: (scope: ScopeImpl) => Host;
|
|
5
5
|
export declare const power: (scope: ScopeImpl) => Power;
|
|
6
6
|
export declare function tools(scope: ScopeImpl): Tools;
|
|
7
|
+
/**
|
|
8
|
+
* The player's limits on {@link SessionStorage}. A key outside them fails
|
|
9
|
+
* with code `invalid`; a `set` that would make the plugin's entries larger
|
|
10
|
+
* than `bytes` fails with `limit` and changes nothing. A single `set` must
|
|
11
|
+
* also fit in one message to the player
|
|
12
|
+
* ({@link "@vestra-player/sdk/testing"!MESSAGE_LIMIT | MESSAGE_LIMIT}, 1 MiB
|
|
13
|
+
* with the key and the request around the value). The player's tests check
|
|
14
|
+
* that these agree with the player.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import assert from "node:assert/strict";
|
|
19
|
+
* import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
|
|
20
|
+
*
|
|
21
|
+
* const key = "https://example.com/a.mkv";
|
|
22
|
+
* assert.ok(Buffer.byteLength(key) <= SESSION_STORAGE_LIMITS.keyBytes);
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @since 1.5.25
|
|
26
|
+
*/
|
|
27
|
+
export declare const SESSION_STORAGE_LIMITS: Readonly<{
|
|
28
|
+
/** UTF-8 bytes of a key (1024, at least 1; no NUL character). */
|
|
29
|
+
readonly keyBytes: 1024;
|
|
30
|
+
/**
|
|
31
|
+
* Bytes one plugin may hold at the same time: for every key, its UTF-8
|
|
32
|
+
* bytes, its value written as compact JSON (`JSON.stringify(value)`) and
|
|
33
|
+
* `entryBytes`.
|
|
34
|
+
*/
|
|
35
|
+
readonly bytes: number;
|
|
36
|
+
/**
|
|
37
|
+
* What each key costs on top of its text and value (128): the player's
|
|
38
|
+
* own bookkeeping, so that many tiny entries stay within `bytes` of memory.
|
|
39
|
+
*/
|
|
40
|
+
readonly entryBytes: 128;
|
|
41
|
+
}>;
|
|
7
42
|
export declare const storage: (scope: ScopeImpl) => Storage;
|
|
8
43
|
export declare function system(scope: ScopeImpl): System;
|
|
9
44
|
/**
|
package/dist/plugin/services.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared services: the clipboard, sleep prevention, showing and opening files
|
|
3
|
-
* and links, and
|
|
4
|
-
* run in this plugin's own process.
|
|
3
|
+
* and links, secrets and session storage go to the host; external tools and
|
|
4
|
+
* settings files run in this plugin's own process.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
7
7
|
import { mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
@@ -238,6 +238,90 @@ class Store {
|
|
|
238
238
|
this.#scope.forget(this);
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* The player's limits on {@link SessionStorage}. A key outside them fails
|
|
243
|
+
* with code `invalid`; a `set` that would make the plugin's entries larger
|
|
244
|
+
* than `bytes` fails with `limit` and changes nothing. A single `set` must
|
|
245
|
+
* also fit in one message to the player
|
|
246
|
+
* ({@link "@vestra-player/sdk/testing"!MESSAGE_LIMIT | MESSAGE_LIMIT}, 1 MiB
|
|
247
|
+
* with the key and the request around the value). The player's tests check
|
|
248
|
+
* that these agree with the player.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* import assert from "node:assert/strict";
|
|
253
|
+
* import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
|
|
254
|
+
*
|
|
255
|
+
* const key = "https://example.com/a.mkv";
|
|
256
|
+
* assert.ok(Buffer.byteLength(key) <= SESSION_STORAGE_LIMITS.keyBytes);
|
|
257
|
+
* ```
|
|
258
|
+
*
|
|
259
|
+
* @since 1.5.25
|
|
260
|
+
*/
|
|
261
|
+
export const SESSION_STORAGE_LIMITS = Object.freeze({
|
|
262
|
+
/** UTF-8 bytes of a key (1024, at least 1; no NUL character). */
|
|
263
|
+
keyBytes: 1024,
|
|
264
|
+
/**
|
|
265
|
+
* Bytes one plugin may hold at the same time: for every key, its UTF-8
|
|
266
|
+
* bytes, its value written as compact JSON (`JSON.stringify(value)`) and
|
|
267
|
+
* `entryBytes`.
|
|
268
|
+
*/
|
|
269
|
+
bytes: 1024 * 1024,
|
|
270
|
+
/**
|
|
271
|
+
* What each key costs on top of its text and value (128): the player's
|
|
272
|
+
* own bookkeeping, so that many tiny entries stay within `bytes` of memory.
|
|
273
|
+
*/
|
|
274
|
+
entryBytes: 128,
|
|
275
|
+
});
|
|
276
|
+
/** A lone surrogate: the player's JSON parser refuses the whole message. */
|
|
277
|
+
const LONE_SURROGATE = /[\uD800-\uDFFF]/u;
|
|
278
|
+
/** Whether a parsed JSON value has a string or a property name with a lone surrogate. */
|
|
279
|
+
function hasLoneSurrogate(value) {
|
|
280
|
+
if (typeof value === "string")
|
|
281
|
+
return LONE_SURROGATE.test(value);
|
|
282
|
+
if (Array.isArray(value))
|
|
283
|
+
return value.some(hasLoneSurrogate);
|
|
284
|
+
if (value && typeof value === "object")
|
|
285
|
+
return Object.entries(value).some(([key, item]) => LONE_SURROGATE.test(key) || hasLoneSurrogate(item));
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
function sessionStorage(scope) {
|
|
289
|
+
const keyed = (key) => {
|
|
290
|
+
if (typeof key !== "string" ||
|
|
291
|
+
!key ||
|
|
292
|
+
key.includes("\0") ||
|
|
293
|
+
LONE_SURROGATE.test(key) ||
|
|
294
|
+
Buffer.byteLength(key) > SESSION_STORAGE_LIMITS.keyBytes)
|
|
295
|
+
throw failure("invalid", `session storage keys are 1–${SESSION_STORAGE_LIMITS.keyBytes} bytes of well-formed text without NUL`);
|
|
296
|
+
return key;
|
|
297
|
+
};
|
|
298
|
+
const host = () => scope.runtime.host.storageSession;
|
|
299
|
+
return {
|
|
300
|
+
async get(key) {
|
|
301
|
+
const found = await host().get(keyed(key));
|
|
302
|
+
return "value" in found ? found.value : undefined;
|
|
303
|
+
},
|
|
304
|
+
async set(key, value) {
|
|
305
|
+
const name = keyed(key);
|
|
306
|
+
let text;
|
|
307
|
+
try {
|
|
308
|
+
text = JSON.stringify(value);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
text = undefined; // a cycle or a BigInt
|
|
312
|
+
}
|
|
313
|
+
if (text === undefined)
|
|
314
|
+
throw failure("invalid", "session storage values must be JSON; delete() removes one");
|
|
315
|
+
// What the player stores, and what `get` will return.
|
|
316
|
+
const json = JSON.parse(text);
|
|
317
|
+
if (hasLoneSurrogate(json))
|
|
318
|
+
throw failure("invalid", "session storage values must be well-formed text: a string has a lone surrogate");
|
|
319
|
+
await host().set(name, json);
|
|
320
|
+
},
|
|
321
|
+
delete: async (key) => host().delete(keyed(key)),
|
|
322
|
+
keys: async () => host().keys(),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
241
325
|
export const storage = (scope) => ({
|
|
242
326
|
json(name, options) {
|
|
243
327
|
if (typeof name !== "string" || !NAME.test(name))
|
|
@@ -247,6 +331,7 @@ export const storage = (scope) => ({
|
|
|
247
331
|
throw failure("invalid", "debounceMs must be a non-negative number");
|
|
248
332
|
return new Store(scope, scope.dataDirectory, name, options);
|
|
249
333
|
},
|
|
334
|
+
session: sessionStorage(scope),
|
|
250
335
|
});
|
|
251
336
|
export function system(scope) {
|
|
252
337
|
const exists = async (path) => {
|