@vestra-player/sdk 1.5.25 → 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.
@@ -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
+ };
@@ -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";
@@ -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";
@@ -4390,7 +4390,8 @@ export interface SubtitleConditional {
4390
4390
  }
4391
4391
  /**
4392
4392
  * `plugin.subtitles`: tracks, uploads and style. Reading needs
4393
- * `subtitles.read`, changes need `subtitles.control`.
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;
@@ -0,0 +1,135 @@
1
+ import { addCapabilityEdit, CAPABILITY_DIAGNOSTIC, findUndeclaredCapabilities, isPluginSource, } from "./capability-check.js";
2
+ const FIX_NAME = "vestraAddCapability";
3
+ /** The nearest manifest.json at or above a file, with its capabilities. */
4
+ function manifestFor(ts, file) {
5
+ let directory = file.replaceAll("\\", "/");
6
+ for (;;) {
7
+ const parent = directory.slice(0, directory.lastIndexOf("/"));
8
+ if (!parent || parent === directory)
9
+ return undefined;
10
+ directory = parent;
11
+ const path = `${directory}/manifest.json`;
12
+ if (!ts.sys.fileExists(path))
13
+ continue;
14
+ try {
15
+ const manifest = JSON.parse(ts.sys.readFile(path) ?? "");
16
+ if (!Array.isArray(manifest.capabilities))
17
+ return undefined;
18
+ return {
19
+ path,
20
+ directory,
21
+ capabilities: manifest.capabilities.filter((each) => typeof each === "string"),
22
+ };
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ }
29
+ /**
30
+ * The manifest's text for the quick fix. The TypeScript server applies a
31
+ * code fix only to files it knows, and editors do not hand it JSON files
32
+ * (VS Code does not), so the manifest is made known first; its text is then
33
+ * the server's: an open buffer when the editor shares one, else the file on
34
+ * disk (VS Code: the saved manifest, not unsaved edits).
35
+ */
36
+ function manifestText(ts, info, path) {
37
+ const service = info.project?.projectService;
38
+ const script = service && ts.server?.toNormalizedPath
39
+ ? service.getOrCreateScriptInfoForNormalizedPath(ts.server.toNormalizedPath(path), false)
40
+ : undefined;
41
+ const snapshot = script?.getSnapshot();
42
+ return snapshot
43
+ ? snapshot.getText(0, snapshot.getLength())
44
+ : ts.sys.readFile(path);
45
+ }
46
+ /** @internal The plugin factory tsserver calls (`ts-plugin/index.js`). */
47
+ export default function init(modules) {
48
+ const ts = modules.typescript;
49
+ return {
50
+ create(info) {
51
+ const service = info.languageService;
52
+ const log = (message) => info.project?.projectService?.logger?.info(`[@vestra-player/sdk/ts-plugin] ${message}`);
53
+ const findings = (file) => {
54
+ const manifest = manifestFor(ts, file);
55
+ const program = service.getProgram();
56
+ if (!manifest || !program || !isPluginSource(manifest.directory, file))
57
+ return [];
58
+ try {
59
+ return findUndeclaredCapabilities(ts, program, {
60
+ files: [file],
61
+ capabilities: manifest.capabilities,
62
+ });
63
+ }
64
+ catch (error) {
65
+ log(`check failed: ${error.stack ?? String(error)}`);
66
+ return [];
67
+ }
68
+ };
69
+ const proxy = Object.create(null);
70
+ for (const key of Object.keys(service)) {
71
+ const member = service[key];
72
+ proxy[key] =
73
+ typeof member === "function" ? member.bind(service) : member;
74
+ }
75
+ proxy.getSemanticDiagnostics = (file) => {
76
+ const prior = service.getSemanticDiagnostics(file);
77
+ const source = service.getProgram()?.getSourceFile(file);
78
+ return [
79
+ ...prior,
80
+ ...findings(file).map((finding) => ({
81
+ file: source,
82
+ start: finding.start,
83
+ length: finding.length,
84
+ messageText: finding.message,
85
+ category: ts.DiagnosticCategory.Error,
86
+ code: CAPABILITY_DIAGNOSTIC,
87
+ source: "vestra",
88
+ })),
89
+ ];
90
+ };
91
+ proxy.getSupportedCodeFixes = (file) => [
92
+ ...service.getSupportedCodeFixes(file),
93
+ String(CAPABILITY_DIAGNOSTIC),
94
+ ];
95
+ proxy.getCodeFixesAtPosition = (file, start, end, errorCodes, format, preferences) => {
96
+ const prior = service.getCodeFixesAtPosition(file, start, end, errorCodes, format, preferences);
97
+ if (!errorCodes.includes(CAPABILITY_DIAGNOSTIC))
98
+ return prior;
99
+ const manifest = manifestFor(ts, file);
100
+ if (!manifest)
101
+ return prior;
102
+ const text = manifestText(ts, info, manifest.path);
103
+ if (text === undefined)
104
+ return prior;
105
+ const wanted = new Set(findings(file)
106
+ .filter((finding) => finding.start <= end && finding.start + finding.length >= start)
107
+ .map((finding) => finding.capability));
108
+ const fixes = [];
109
+ for (const capability of wanted) {
110
+ const edit = addCapabilityEdit(ts, text, capability);
111
+ if (!edit)
112
+ continue;
113
+ fixes.push({
114
+ fixName: FIX_NAME,
115
+ description: `Add '${capability}' to manifest.json`,
116
+ changes: [
117
+ {
118
+ fileName: manifest.path,
119
+ textChanges: [
120
+ {
121
+ span: { start: edit.start, length: edit.length },
122
+ newText: edit.newText,
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ });
128
+ }
129
+ return [...prior, ...fixes];
130
+ };
131
+ log("enabled");
132
+ return proxy;
133
+ },
134
+ };
135
+ }