@vestra-player/sdk 1.5.29 → 1.5.31
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 +23 -0
- package/dist/capabilities.d.ts +13 -3
- package/dist/capabilities.js +13 -3
- package/dist/commands.d.ts +49 -9
- package/dist/commands.js +49 -9
- package/dist/index.d.ts +6 -1
- package/dist/manifest.js +1 -1
- package/dist/palettes.d.ts +16 -2
- package/dist/palettes.js +16 -2
- package/dist/plugin/bindings.d.ts +27 -3
- package/dist/plugin/bindings.js +27 -3
- package/dist/plugin/osd.d.ts +13 -1
- package/dist/plugin/osd.js +13 -1
- package/dist/plugin/services.d.ts +47 -6
- package/dist/plugin/services.js +47 -6
- package/dist/plugin/testing.d.ts +353 -62
- package/dist/plugin/testing.js +344 -62
- package/dist/plugin/types.d.ts +2260 -234
- package/dist/plugin/values.d.ts +29 -5
- package/dist/plugin/values.js +29 -5
- package/dist/processing.d.ts +16 -4
- package/dist/processing.js +1 -1
- package/dist/source-resolvers.d.ts +33 -8
- package/dist/subtitles.d.ts +19 -9
- package/dist/wire.d.ts +12 -3
- package/dist/wire.js +12 -3
- package/llms-full.txt +4490 -71
- package/llms.txt +25 -25
- package/package.json +1 -1
package/dist/plugin/types.d.ts
CHANGED
|
@@ -33,7 +33,11 @@ export type { Json, AudioNode, VideoNode, SubtitleStyle, ResolvedMedia };
|
|
|
33
33
|
* unsubscribed. A {@link Media} is the exception: disposing its scope does
|
|
34
34
|
* **not** close the media; call {@link Media.close} for that.
|
|
35
35
|
*
|
|
36
|
-
* @example
|
|
36
|
+
* @example Stop listening once the file is ready
|
|
37
|
+
* The listener reacts to the first time the source becomes ready and then
|
|
38
|
+
* stops being called. Every registration returns a `Disposable`;
|
|
39
|
+
* disposing it removes just that listener, before the scope that owns it
|
|
40
|
+
* ends.
|
|
37
41
|
* ```ts
|
|
38
42
|
* const subscription = plugin.player.onChange((state) => {
|
|
39
43
|
* if (state.phase === "ready") void subscription.dispose();
|
|
@@ -100,7 +104,11 @@ export interface Disposable {
|
|
|
100
104
|
* the plugin process. Requests have no client-side timeout: the player
|
|
101
105
|
* answers every request or stops the plugin.
|
|
102
106
|
*
|
|
103
|
-
* @example
|
|
107
|
+
* @example Ignore a seek that lost a race, rethrow anything else
|
|
108
|
+
* The plugin seeks to 0:30 and stays quiet when another file was opened in
|
|
109
|
+
* between. Every failure is an `Error` with a stable `code`; `stale` means
|
|
110
|
+
* the source changed since the state the call was based on and nothing
|
|
111
|
+
* was done, so it is safe to drop. Other codes are real failures.
|
|
104
112
|
* ```ts
|
|
105
113
|
* try {
|
|
106
114
|
* await plugin.player.seek(30);
|
|
@@ -129,7 +137,11 @@ export type PluginError = Error & {
|
|
|
129
137
|
* `.theme`, {@link Scope.ui}, {@link Scope.tools}, {@link Scope.storage} and
|
|
130
138
|
* {@link Scope.system} needs none.
|
|
131
139
|
*
|
|
132
|
-
* @example
|
|
140
|
+
* @example Rebuild a dynamic submenu with a child scope
|
|
141
|
+
* Subtitles ▸ Track lists the current file's subtitle tracks and is
|
|
142
|
+
* rebuilt whenever they change. Commands added through a child scope
|
|
143
|
+
* belong to it; disposing the scope removes all of them at once, and a
|
|
144
|
+
* new child scope collects the next set.
|
|
133
145
|
* ```ts
|
|
134
146
|
* let tracks = plugin.scope();
|
|
135
147
|
* plugin.subtitles.onChange(async (state) => {
|
|
@@ -263,9 +275,12 @@ export interface Scope extends Disposable {
|
|
|
263
275
|
* @returns A handle that cancels the registration early (without
|
|
264
276
|
* running `cleanup`).
|
|
265
277
|
*
|
|
266
|
-
* @example
|
|
278
|
+
* @example Stop a timer when the plugin stops
|
|
279
|
+
* A once-a-second refresh runs as long as the plugin does. `onDispose`
|
|
280
|
+
* runs the cleanup when the scope is disposed, which for `plugin` is
|
|
281
|
+
* when the player stops the plugin, so the timer never outlives it.
|
|
267
282
|
* ```ts
|
|
268
|
-
* const timer = setInterval(() => plugin.update(), 1000);
|
|
283
|
+
* const timer = setInterval(() => plugin.update(), 1000); // re-evaluate bindings
|
|
269
284
|
* plugin.onDispose(() => clearInterval(timer));
|
|
270
285
|
* ```
|
|
271
286
|
*/
|
|
@@ -279,10 +294,14 @@ export interface Scope extends Disposable {
|
|
|
279
294
|
* registrations made after activation (a command whose `ready` rejects,
|
|
280
295
|
* a property write the player refused).
|
|
281
296
|
*
|
|
282
|
-
* @example
|
|
297
|
+
* @example Show errors from handlers on the OSD
|
|
298
|
+
* When a menu command, a window callback or a listener of the plugin
|
|
299
|
+
* fails, the user sees why. The SDK catches what those handlers throw
|
|
300
|
+
* (and their rejected promises) and passes it here instead of only
|
|
301
|
+
* logging it; the message is data in a translated template.
|
|
283
302
|
* ```ts
|
|
284
303
|
* plugin.onError((error) => {
|
|
285
|
-
* void plugin.osd.show(
|
|
304
|
+
* void plugin.osd.show("Bookmarks: {}", [error.message]);
|
|
286
305
|
* });
|
|
287
306
|
* ```
|
|
288
307
|
*/
|
|
@@ -315,7 +334,11 @@ export interface Scope extends Disposable {
|
|
|
315
334
|
* process that has not exited after 300 ms is killed. Media the plugin
|
|
316
335
|
* opened with {@link Player.open} keeps playing.
|
|
317
336
|
*
|
|
318
|
-
* @example
|
|
337
|
+
* @example Write a minimal plugin with one menu item
|
|
338
|
+
* Tools ▸ Say hello, or Ctrl+H, shows "Hello from" and the plugin's id.
|
|
339
|
+
* The player imports `main.ts`, calls its default export with the
|
|
340
|
+
* `Plugin` object, and keeps the registrations made during activation;
|
|
341
|
+
* the manifest declares the capabilities `commands` and `ui.osd`.
|
|
319
342
|
* ```ts
|
|
320
343
|
* import type { Plugin } from "@vestra-player/sdk";
|
|
321
344
|
*
|
|
@@ -323,8 +346,8 @@ export interface Scope extends Disposable {
|
|
|
323
346
|
* plugin.commands.add({
|
|
324
347
|
* id: "hello",
|
|
325
348
|
* path: ["Tools", "Say hello"],
|
|
326
|
-
* bindings: ["Ctrl+H"],
|
|
327
|
-
* run: () => plugin.osd.show(
|
|
349
|
+
* bindings: ["Ctrl+H"], // a default the user can change
|
|
350
|
+
* run: () => plugin.osd.show("Hello from {}", [plugin.id]),
|
|
328
351
|
* });
|
|
329
352
|
* }
|
|
330
353
|
* ```
|
|
@@ -349,7 +372,13 @@ export interface Plugin extends Scope {
|
|
|
349
372
|
*/
|
|
350
373
|
readonly dataDirectory: string;
|
|
351
374
|
}
|
|
352
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* The state of a command's menu item, as the player shows it.
|
|
377
|
+
*
|
|
378
|
+
* @see {@link Command.set}, {@link CommandOptions}
|
|
379
|
+
* @since 1.0.0
|
|
380
|
+
* @category Commands
|
|
381
|
+
*/
|
|
353
382
|
export interface CommandState {
|
|
354
383
|
/** False greys the item out; the player then never calls `run` (bound keys are swallowed). Default true. */
|
|
355
384
|
enabled: boolean;
|
|
@@ -367,7 +396,11 @@ export interface CommandState {
|
|
|
367
396
|
* A value, or a function the SDK re-evaluates on every update pass (see
|
|
368
397
|
* {@link Scope.update}); only changes are sent to the player.
|
|
369
398
|
*
|
|
370
|
-
* @example
|
|
399
|
+
* @example Mix a computed state with a fixed one
|
|
400
|
+
* The Loop item carries a check mark while a loop is set and is always
|
|
401
|
+
* enabled. A function is evaluated again in every update pass (after each
|
|
402
|
+
* handler and player state change) and only changes are sent; a plain
|
|
403
|
+
* value never changes.
|
|
371
404
|
* ```ts
|
|
372
405
|
* plugin.commands.add({
|
|
373
406
|
* id: "loop",
|
|
@@ -377,6 +410,9 @@ export interface CommandState {
|
|
|
377
410
|
* run: () => plugin.player.setLoop(null),
|
|
378
411
|
* });
|
|
379
412
|
* ```
|
|
413
|
+
*
|
|
414
|
+
* @since 1.0.0
|
|
415
|
+
* @category Commands
|
|
380
416
|
*/
|
|
381
417
|
export type Bindable<T> = T | (() => T);
|
|
382
418
|
/**
|
|
@@ -384,6 +420,9 @@ export type Bindable<T> = T | (() => T);
|
|
|
384
420
|
* `@<id>` or by its English key (see {@link CommandOptions.path}): Open,
|
|
385
421
|
* Playback, Subtitles, Video, Audio, Screen, Settings. The list is
|
|
386
422
|
* `MENU_IDS` in `@vestra-player/sdk/testing`.
|
|
423
|
+
*
|
|
424
|
+
* @since 1.3.0
|
|
425
|
+
* @category Commands
|
|
387
426
|
*/
|
|
388
427
|
export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
|
|
389
428
|
/**
|
|
@@ -392,6 +431,8 @@ export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "sc
|
|
|
392
431
|
* `@vestra-player/sdk/testing`.
|
|
393
432
|
*
|
|
394
433
|
* @since 1.5.29
|
|
434
|
+
*
|
|
435
|
+
* @category Commands
|
|
395
436
|
*/
|
|
396
437
|
export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "open" | "volume" | "mute" | "playlist" | "detach" | "attach" | "close" | "failed" | "move-top" | "move-up" | "move-down" | "move-bottom" | "search" | "chapter" | "bookmark" | "check-task" | "check-box" | "menu-check" | "submenu" | "scroll-up" | "scroll-down" | "caret-down" | "sort-asc" | "sort-desc" | "tag-remove" | "preview-play" | "add" | "remove" | "sort" | "fullscreen" | "exit-fullscreen" | "store" | "ai";
|
|
397
438
|
/**
|
|
@@ -424,12 +465,17 @@ export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "op
|
|
|
424
465
|
* with its command: disposing the command, or disabling, stopping or
|
|
425
466
|
* uninstalling the plugin, removes it.
|
|
426
467
|
*
|
|
427
|
-
* @example
|
|
468
|
+
* @example Give a command a button in the bottom bar
|
|
469
|
+
* A Store icon appears in the bottom bar's button strip; a click opens
|
|
470
|
+
* the store window, exactly like the menu item. The player draws the
|
|
471
|
+
* button from the command's title, keys and state, and places it by
|
|
472
|
+
* `order` (200 is where the player's own Plugin Store button sits); a
|
|
473
|
+
* bar that does not place `BarButtons` shows only the menu item.
|
|
428
474
|
* ```ts
|
|
429
475
|
* const store = plugin.windows.create("ui/store.slint");
|
|
430
476
|
* plugin.commands.add({
|
|
431
477
|
* id: "open",
|
|
432
|
-
* path: ["Plugin Store…"],
|
|
478
|
+
* path: ["Plugin Store…"], // a button needs a menu path
|
|
433
479
|
* button: { icon: "store", order: 200 },
|
|
434
480
|
* run: () => store.show(),
|
|
435
481
|
* });
|
|
@@ -438,6 +484,8 @@ export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "op
|
|
|
438
484
|
* @player 0.5.10; older players show the menu entry without a button
|
|
439
485
|
* (declare `"minPlayerVersion": "0.5.10"` when the button matters).
|
|
440
486
|
* @since 1.5.29
|
|
487
|
+
*
|
|
488
|
+
* @category Commands
|
|
441
489
|
*/
|
|
442
490
|
export interface CommandButton {
|
|
443
491
|
/**
|
|
@@ -486,6 +534,10 @@ export interface CommandButton {
|
|
|
486
534
|
* {@link Commands.add} throws `invalid` synchronously for a bad path,
|
|
487
535
|
* order, group or binding. The same path registered twice, or a path that
|
|
488
536
|
* is both an item and a submenu, fails with `conflict` (through `ready`).
|
|
537
|
+
*
|
|
538
|
+
* @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
539
|
+
* @since 1.0.0
|
|
540
|
+
* @category Commands
|
|
489
541
|
*/
|
|
490
542
|
export interface CommandOptions {
|
|
491
543
|
/** Local id, unique within the plugin (the global id is `<manifest id>/<id>`). */
|
|
@@ -515,10 +567,19 @@ export interface CommandOptions {
|
|
|
515
567
|
* ({})", [n, codec])`. The menu shows it translated and filled in; it
|
|
516
568
|
* merges by its English rendering ("Track 2 (opus)").
|
|
517
569
|
*
|
|
518
|
-
* @example
|
|
570
|
+
* @example Put items into built-in menus, one with a track number
|
|
571
|
+
* "Blur" appears in the Video menu and "Track 2 (opus)" under Audio ▸
|
|
572
|
+
* Audio Track, both in the user's language. `@video` and `@audio` name
|
|
573
|
+
* the built-in menus by id; "Audio Track" is a submenu key that merges
|
|
574
|
+
* with any other plugin's same key; the `text()` segment is translated
|
|
575
|
+
* with the plugin's catalog and then filled in with the number and codec.
|
|
519
576
|
* ```ts
|
|
577
|
+
* declare function toggle(): void;
|
|
578
|
+
* declare function select(): void;
|
|
579
|
+
* // ---cut---
|
|
520
580
|
* plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run: toggle });
|
|
521
581
|
* const { text } = plugin.i18n;
|
|
582
|
+
* // A segment with data: translated as "Track {} ({})", then filled in.
|
|
522
583
|
* plugin.commands.add({
|
|
523
584
|
* id: "track-2",
|
|
524
585
|
* path: ["@audio", "Audio Track", text("Track {} ({})", [2, "opus"])],
|
|
@@ -603,6 +664,38 @@ export interface CommandOptions {
|
|
|
603
664
|
/**
|
|
604
665
|
* A registered command, from {@link Commands.add}. Disposing it removes it
|
|
605
666
|
* from the menu and the binding table; the id can be added again right away.
|
|
667
|
+
*
|
|
668
|
+
* @remarks
|
|
669
|
+
* The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
670
|
+
* `busy`, `reason`) is bound: functions given to {@link Commands.add} or
|
|
671
|
+
* {@link Command.set} are evaluated again in every update pass and only
|
|
672
|
+
* changes reach the player. Its title, path, bindings and button cannot
|
|
673
|
+
* change in place; dispose the command (or a child {@link Scope}) and add it
|
|
674
|
+
* again. Disposing it, its scope or stopping the plugin removes the menu
|
|
675
|
+
* item, its keys and its bar button.
|
|
676
|
+
*
|
|
677
|
+
* @capability commands
|
|
678
|
+
*
|
|
679
|
+
* @example Enable an item only while a file is open
|
|
680
|
+
* Video ▸ Export… is greyed out until a file is open. `set` replaces the
|
|
681
|
+
* `enabled` field with a function; the SDK evaluates it again after every
|
|
682
|
+
* player state change and sends the new state when it changes, so the
|
|
683
|
+
* item follows the player without further code.
|
|
684
|
+
* ```ts
|
|
685
|
+
* declare function runExport(): Promise<void>;
|
|
686
|
+
* // ---cut---
|
|
687
|
+
* const exportCommand = plugin.commands.add({
|
|
688
|
+
* id: "export",
|
|
689
|
+
* path: ["@video", "Export…"],
|
|
690
|
+
* run: runExport,
|
|
691
|
+
* });
|
|
692
|
+
* // A function: re-evaluated in every update pass.
|
|
693
|
+
* exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
694
|
+
* ```
|
|
695
|
+
*
|
|
696
|
+
* @see {@link Commands.add}, [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
697
|
+
* @since 1.0.0
|
|
698
|
+
* @category Commands
|
|
606
699
|
*/
|
|
607
700
|
export interface Command extends Disposable {
|
|
608
701
|
/** The local id given in {@link CommandOptions.id}. */
|
|
@@ -610,10 +703,30 @@ export interface Command extends Disposable {
|
|
|
610
703
|
/**
|
|
611
704
|
* Replace some state fields (values or functions). Last write wins.
|
|
612
705
|
*
|
|
613
|
-
* @example
|
|
706
|
+
* @example Show a command as busy while it works
|
|
707
|
+
* The Export item shows as busy with "Exporting…" as its reason while the
|
|
708
|
+
* export runs, then returns to normal. `set` replaces only the fields it
|
|
709
|
+
* names and the SDK sends the new state to the player; a busy command's
|
|
710
|
+
* menu item and key do not run it again until it is set back.
|
|
614
711
|
* ```ts
|
|
615
|
-
*
|
|
712
|
+
* declare function runExport(): Promise<void>;
|
|
713
|
+
* // ---cut---
|
|
714
|
+
* const exportCommand = plugin.commands.add({
|
|
715
|
+
* id: "export",
|
|
716
|
+
* path: ["@video", "Export…"],
|
|
717
|
+
* run: async () => {
|
|
718
|
+
* exportCommand.set({ busy: true, reason: "Exporting…" });
|
|
719
|
+
* try {
|
|
720
|
+
* await runExport();
|
|
721
|
+
* } finally {
|
|
722
|
+
* exportCommand.set({ busy: false, reason: null }); // also on failure
|
|
723
|
+
* }
|
|
724
|
+
* },
|
|
725
|
+
* });
|
|
616
726
|
* ```
|
|
727
|
+
*
|
|
728
|
+
* @capability commands
|
|
729
|
+
* @since 1.0.0
|
|
617
730
|
*/
|
|
618
731
|
set(state: Partial<{
|
|
619
732
|
[K in keyof CommandState]: Bindable<CommandState[K]>;
|
|
@@ -624,10 +737,49 @@ export interface Command extends Disposable {
|
|
|
624
737
|
* commands in this plugin, or 1024 in the whole player). You rarely need
|
|
625
738
|
* to await it: during activation a failure fails activation, later it
|
|
626
739
|
* goes to {@link Scope.onError}.
|
|
740
|
+
*
|
|
741
|
+
* @throws `conflict` (rejects) Another command already has this menu path,
|
|
742
|
+
* or the path would be both a menu item and a submenu.
|
|
743
|
+
* @throws `capability` (rejects) The manifest does not declare `commands`.
|
|
744
|
+
* @throws `invalid` (rejects) The player refused the declaration.
|
|
745
|
+
* @throws `limit` (rejects) More than 160 commands in this plugin, or 1024 in
|
|
746
|
+
* the player.
|
|
627
747
|
*/
|
|
628
748
|
readonly ready: Promise<void>;
|
|
629
749
|
}
|
|
630
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* `plugin.commands`: menu commands with default bindings. Needs `commands`.
|
|
752
|
+
*
|
|
753
|
+
* @remarks
|
|
754
|
+
* A command is one action of the plugin with a menu item (`path`), default
|
|
755
|
+
* keys (`bindings`), optionally a bottom-bar button (`button`) and a state
|
|
756
|
+
* the menu shows (`enabled`, `checked`, `busy`, `reason`). The player owns
|
|
757
|
+
* the menu, the binding table and the user's key overrides: it calls `run`
|
|
758
|
+
* when the user picks the item, presses a key or clicks the button.
|
|
759
|
+
* Commands of all plugins merge into the player's menus by their English
|
|
760
|
+
* path keys. Registration is asynchronous: `add` returns at once, and
|
|
761
|
+
* {@link Command.ready} reports the player's answer (during activation a
|
|
762
|
+
* refusal fails activation).
|
|
763
|
+
*
|
|
764
|
+
* @capability commands
|
|
765
|
+
*
|
|
766
|
+
* @example Add a menu item with a default key
|
|
767
|
+
* Tools ▸ Say hello, or Ctrl+H, shows a greeting. `add` declares the item;
|
|
768
|
+
* the player calls `run` when the user picks it or presses its key, and the
|
|
769
|
+
* user can change the key in the player's key settings.
|
|
770
|
+
* ```ts
|
|
771
|
+
* plugin.commands.add({
|
|
772
|
+
* id: "hello",
|
|
773
|
+
* path: ["Tools", "Say hello"],
|
|
774
|
+
* bindings: ["Ctrl+H"], // a default; the user may override it
|
|
775
|
+
* run: () => plugin.osd.show("Hello"),
|
|
776
|
+
* });
|
|
777
|
+
* ```
|
|
778
|
+
*
|
|
779
|
+
* @see {@link Command}, {@link CommandOptions}, [Commands, menus and key bindings](../../docs/concepts/commands.md)
|
|
780
|
+
* @since 1.0.0
|
|
781
|
+
* @category Commands
|
|
782
|
+
*/
|
|
631
783
|
export interface Commands {
|
|
632
784
|
/**
|
|
633
785
|
* Declare a command. It is registered and kept in sync; `dispose()` removes
|
|
@@ -640,18 +792,26 @@ export interface Commands {
|
|
|
640
792
|
* unknown `@` menu id), order, group or binding; `conflict` for an id this
|
|
641
793
|
* plugin has already declared and not disposed.
|
|
642
794
|
*
|
|
643
|
-
* @example
|
|
795
|
+
* @example Pause playback from a menu item
|
|
796
|
+
* Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
|
|
797
|
+
* while paused and is greyed out when nothing can be paused. The player
|
|
798
|
+
* calls `run` when the user picks the item or presses its key; `enabled`
|
|
799
|
+
* and `checked` are functions the SDK evaluates again after every
|
|
800
|
+
* handler and player state change, sending only what changed.
|
|
644
801
|
* ```ts
|
|
645
802
|
* const toggle = plugin.commands.add({
|
|
646
803
|
* id: "toggle-pause",
|
|
647
804
|
* path: ["Playback", "Pause"],
|
|
648
|
-
* order: 210,
|
|
805
|
+
* order: 210, // position among the Playback menu's items
|
|
649
806
|
* bindings: ["Ctrl+P"],
|
|
650
807
|
* enabled: () => plugin.player.state.can.pause,
|
|
651
808
|
* checked: () => plugin.player.state.paused,
|
|
652
809
|
* run: () => plugin.player.setPaused(!plugin.player.state.paused),
|
|
653
810
|
* });
|
|
654
811
|
* ```
|
|
812
|
+
*
|
|
813
|
+
* @capability commands
|
|
814
|
+
* @since 1.0.0
|
|
655
815
|
*/
|
|
656
816
|
add(options: CommandOptions): Command;
|
|
657
817
|
}
|
|
@@ -663,8 +823,8 @@ export interface Commands {
|
|
|
663
823
|
* {@link View.bind}: the whole value of an `image` property or, from SDK
|
|
664
824
|
* 1.5.19 (player 0.5.1), an `image` field at any depth, such as the logo of
|
|
665
825
|
* each row of a list. Making the reference does not read the file: the
|
|
666
|
-
* player checks and decodes it each time
|
|
667
|
-
* has the rules and limits).
|
|
826
|
+
* player checks and decodes it each time it receives the value (a property
|
|
827
|
+
* write, a window opening; {@link Ui.image} has the rules and limits).
|
|
668
828
|
*
|
|
669
829
|
* A reference belongs to the {@link Scope} that created it. Once that scope
|
|
670
830
|
* is disposed, writing the reference fails with `invalid` (reported to
|
|
@@ -672,13 +832,24 @@ export interface Commands {
|
|
|
672
832
|
* property changes or the view closes.
|
|
673
833
|
*
|
|
674
834
|
* @example Show a cover image from the data directory
|
|
835
|
+
* A window opens showing `cover.png`, which the plugin saved in its data
|
|
836
|
+
* directory. `ui.image` only makes a reference, without reading the file;
|
|
837
|
+
* the player checks and decodes the file when it receives the value, so a
|
|
838
|
+
* missing file fails that write with `missing` (here: `show` rejects, as
|
|
839
|
+
* it is an initial property). A missing image nested inside a value is
|
|
840
|
+
* drawn empty instead.
|
|
675
841
|
* ```ts
|
|
676
842
|
* const cover = plugin.windows.create("ui/cover.slint");
|
|
843
|
+
* // `art` is an `image` property in ui/cover.slint.
|
|
677
844
|
* cover.set({ art: plugin.ui.image(`${plugin.dataDirectory}/cover.png`) });
|
|
678
845
|
* await cover.show();
|
|
679
846
|
* ```
|
|
680
847
|
*
|
|
681
|
-
* @example
|
|
848
|
+
* @example Show a logo in each row of a list
|
|
849
|
+
* Each row of the list draws its own small logo next to its name. An
|
|
850
|
+
* `ImageRef` may sit in any field of a property value (SDK 1.5.19, player
|
|
851
|
+
* 0.5.1), so the rows carry their icons; `maxSide: 64` decodes each logo
|
|
852
|
+
* at most 64 px wide, which keeps the view's image memory small.
|
|
682
853
|
* ```slint
|
|
683
854
|
* export struct Row { key: string, name: string, icon: image }
|
|
684
855
|
* ```
|
|
@@ -689,6 +860,7 @@ export interface Commands {
|
|
|
689
860
|
* rows: items.map((item) => ({
|
|
690
861
|
* key: item.id,
|
|
691
862
|
* name: item.name,
|
|
863
|
+
* // About twice the drawn size (32 px) stays sharp on dense screens.
|
|
692
864
|
* icon: plugin.ui.image(item.iconPath, { maxSide: 64 }),
|
|
693
865
|
* })),
|
|
694
866
|
* }));
|
|
@@ -739,7 +911,11 @@ export interface ImageOptions {
|
|
|
739
911
|
* scope that created it: once that scope is disposed, writing it fails with
|
|
740
912
|
* `invalid`, wherever it is in the value.
|
|
741
913
|
*
|
|
742
|
-
* @example
|
|
914
|
+
* @example Show chat messages, one document per row
|
|
915
|
+
* The chat window shows a formatted assistant message. `ui.markdown`
|
|
916
|
+
* resolves once the document is ready (here without downloading images);
|
|
917
|
+
* the reference goes into the `body` field of a row, whose Slint type is
|
|
918
|
+
* `[MarkdownBlock]`, and the player parses and draws it.
|
|
743
919
|
* ```ts
|
|
744
920
|
* const chat = plugin.windows.create("ui/chat.slint");
|
|
745
921
|
* const body = await plugin.ui.markdown("**Hello**", { remoteImages: false });
|
|
@@ -833,7 +1009,11 @@ export type PropValue = PropData | ImageRef;
|
|
|
833
1009
|
* JSON whose values may also be Markdown documents and (SDK 1.5.19) images,
|
|
834
1010
|
* at any depth: the part of {@link PropValue} that nests.
|
|
835
1011
|
*
|
|
836
|
-
* @example
|
|
1012
|
+
* @example Type a message list whose rows carry a document
|
|
1013
|
+
* A property value for a chat's message list, typed so that TypeScript
|
|
1014
|
+
* accepts a Markdown document inside each row. `PropData` is the JSON a
|
|
1015
|
+
* property takes plus documents and images at any depth; the player turns
|
|
1016
|
+
* each object into a Slint struct by its field names.
|
|
837
1017
|
* ```ts
|
|
838
1018
|
* const messages: PropData = [
|
|
839
1019
|
* { id: "m1", role: "user", body: await plugin.ui.markdown("Hi") },
|
|
@@ -855,7 +1035,11 @@ export type PropData = null | boolean | number | string | MarkdownRef | ImageRef
|
|
|
855
1035
|
* kebab-case are kept. Only the property names are converted, not the
|
|
856
1036
|
* fields of struct values.
|
|
857
1037
|
*
|
|
858
|
-
* @example
|
|
1038
|
+
* @example Prepare a window's initial properties
|
|
1039
|
+
* An object of property values by name, as `windows.create` and
|
|
1040
|
+
* `View.set` take them. Names match the Slint file (`canMark` for
|
|
1041
|
+
* `can-mark`), and each value becomes the Slint type of its property: a
|
|
1042
|
+
* boolean, and an array of structs for a model.
|
|
859
1043
|
* ```ts
|
|
860
1044
|
* const props: Props = { canMark: true, rows: [{ rowId: "a", title: "First" }] };
|
|
861
1045
|
* ```
|
|
@@ -879,7 +1063,13 @@ export type Props = Record<string, PropValue>;
|
|
|
879
1063
|
*
|
|
880
1064
|
* @capability None.
|
|
881
1065
|
*
|
|
882
|
-
* @example
|
|
1066
|
+
* @example Show a Markdown file in a window (examples/markdown)
|
|
1067
|
+
* A menu item opens a window with a formatted Markdown file that ships
|
|
1068
|
+
* with the plugin. The plugin reads the file, turns it into a document
|
|
1069
|
+
* with `ui.markdown` (relative images resolve against the plugin's
|
|
1070
|
+
* directory through `base`) and writes it to the window's `doc` property,
|
|
1071
|
+
* which the Slint file hands to `MarkdownView`.
|
|
1072
|
+
*
|
|
883
1073
|
* {@includeCode ../../examples/markdown/main.ts#document}
|
|
884
1074
|
*
|
|
885
1075
|
* `ui/document.slint`:
|
|
@@ -896,8 +1086,8 @@ export interface Ui {
|
|
|
896
1086
|
*
|
|
897
1087
|
* @remarks
|
|
898
1088
|
* Returns at once without touching the file. The player checks the file
|
|
899
|
-
* each time
|
|
900
|
-
* the main window being rebuilt): it must be inside this plugin's package
|
|
1089
|
+
* each time it receives the reference (a property write, a window or
|
|
1090
|
+
* settings page opening, the main window being rebuilt): it must be inside this plugin's package
|
|
901
1091
|
* directory or {@link Plugin.dataDirectory} after following links, at most
|
|
902
1092
|
* 16 MB and 4096×4096 pixels, and PNG or JPEG by content (the extension
|
|
903
1093
|
* does not matter). A failed check of a whole-value image is a failed
|
|
@@ -909,9 +1099,10 @@ export interface Ui {
|
|
|
909
1099
|
* the value is shown, as a Markdown image shows its alt text.
|
|
910
1100
|
*
|
|
911
1101
|
* The player caches decoded images per plugin (up to 64 MB, least recently
|
|
912
|
-
* used first)
|
|
913
|
-
*
|
|
914
|
-
* write the new reference
|
|
1102
|
+
* used first) by path, size and modification time, and does not watch
|
|
1103
|
+
* files. After rewriting a file under the same path, call `image()` again
|
|
1104
|
+
* and write the new reference: the SDK does not send a value that did not
|
|
1105
|
+
* change, so the player would not look at the file again.
|
|
915
1106
|
*
|
|
916
1107
|
* @capability None.
|
|
917
1108
|
*
|
|
@@ -925,6 +1116,12 @@ export interface Ui {
|
|
|
925
1116
|
* `maxSide` is not an integer 1–4096.
|
|
926
1117
|
*
|
|
927
1118
|
* @example Show a thumbnail the plugin rendered
|
|
1119
|
+
* A window shows a PNG the plugin wrote into its data directory. The file
|
|
1120
|
+
* must be inside the package or data directory; `image` returns at once.
|
|
1121
|
+
* The player decodes the file when it receives the value and notices a
|
|
1122
|
+
* rewritten file by its size and modification time; the plugin still
|
|
1123
|
+
* calls `image()` again after rewriting it, because the SDK does not send
|
|
1124
|
+
* a value that did not change.
|
|
928
1125
|
* ```ts
|
|
929
1126
|
* const window = plugin.windows.create("ui/thumbnail.slint");
|
|
930
1127
|
* const file = `${plugin.dataDirectory}/thumbnail.png`;
|
|
@@ -962,7 +1159,12 @@ export interface Ui {
|
|
|
962
1159
|
* large, or is not a PNG or JPEG image; or the plugin has no data
|
|
963
1160
|
* directory.
|
|
964
1161
|
*
|
|
965
|
-
* @example
|
|
1162
|
+
* @example Show search results with downloaded thumbnails
|
|
1163
|
+
* A results window lists each hit with its thumbnail, or a placeholder
|
|
1164
|
+
* when a thumbnail cannot be fetched. `remoteImage` downloads the image
|
|
1165
|
+
* in the plugin's process into the data directory's cache (at most 4 at a
|
|
1166
|
+
* time) and resolves with a local reference; a failed download rejects
|
|
1167
|
+
* with `unavailable`, which the `catch` turns into the placeholder.
|
|
966
1168
|
* ```ts
|
|
967
1169
|
* const results = [{ title: "A", thumbnailUrl: "https://example.com/a.jpg" }];
|
|
968
1170
|
* const placeholder = plugin.ui.image(`${plugin.dataDirectory}/blank.png`);
|
|
@@ -971,7 +1173,7 @@ export interface Ui {
|
|
|
971
1173
|
* title: r.title,
|
|
972
1174
|
* thumb: await plugin.ui
|
|
973
1175
|
* .remoteImage(r.thumbnailUrl, { maxSide: 320 })
|
|
974
|
-
* .catch(() => placeholder),
|
|
1176
|
+
* .catch(() => placeholder), // `unavailable`: download failed
|
|
975
1177
|
* })),
|
|
976
1178
|
* );
|
|
977
1179
|
* plugin.windows.create("ui/results.slint").set({ rows });
|
|
@@ -1024,7 +1226,14 @@ export interface Ui {
|
|
|
1024
1226
|
* `base` is neither an `http(s):` address nor an absolute path.
|
|
1025
1227
|
* @throws `unsupported` (rejects) The player is older than 0.2.0.
|
|
1026
1228
|
*
|
|
1027
|
-
* @example
|
|
1229
|
+
* @example Build a chat with one Markdown document per row
|
|
1230
|
+
* A chat window lists messages as formatted text, with a Copy button on
|
|
1231
|
+
* each code block that puts the code on the clipboard. Each row's `body`
|
|
1232
|
+
* is a document from `ui.markdown`; the whole list is written again on
|
|
1233
|
+
* each change, and the player parses only the documents that changed.
|
|
1234
|
+
* `MarkdownBlocks` with `code-copy` calls `copy-code`, which the plugin
|
|
1235
|
+
* handles with `on("copyCode")`.
|
|
1236
|
+
*
|
|
1028
1237
|
* `ui/chat.slint`:
|
|
1029
1238
|
*
|
|
1030
1239
|
* ```slint
|
|
@@ -1051,6 +1260,7 @@ export interface Ui {
|
|
|
1051
1260
|
* const chat = plugin.windows.create("ui/chat.slint");
|
|
1052
1261
|
* const history: { id: string; role: string; text: string }[] = [];
|
|
1053
1262
|
* chat.on("copyCode", (code: string) => plugin.clipboard.writeText(code));
|
|
1263
|
+
* // Re-send the whole list: unchanged documents cost the player nothing.
|
|
1054
1264
|
* async function render() {
|
|
1055
1265
|
* const messages = [];
|
|
1056
1266
|
* for (const { id, role, text } of history)
|
|
@@ -1063,10 +1273,19 @@ export interface Ui {
|
|
|
1063
1273
|
* ```
|
|
1064
1274
|
*
|
|
1065
1275
|
* @example Show a README from the web
|
|
1276
|
+
* A window shows a README downloaded from the web, its images included,
|
|
1277
|
+
* and clicked links open in the browser. `base` makes relative links and
|
|
1278
|
+
* images resolve against the README's address; `markdown` resolves after
|
|
1279
|
+
* its `https:` images are downloaded. A click on a link makes
|
|
1280
|
+
* `MarkdownView` call `link-clicked`, which `ui/readme.slint` forwards as
|
|
1281
|
+
* `open-link`; the plugin opens the address with `openUrl`.
|
|
1066
1282
|
* ```ts
|
|
1067
1283
|
* const readmeUrl = "https://example.com/plugin/README.md";
|
|
1068
|
-
* const
|
|
1284
|
+
* const response = await fetch(readmeUrl);
|
|
1285
|
+
* if (!response.ok) throw new Error(`README: HTTP ${response.status}`);
|
|
1286
|
+
* const text = await response.text();
|
|
1069
1287
|
* const window = plugin.windows.create("ui/readme.slint");
|
|
1288
|
+
* // Bound before the first `show`, as a tool window requires.
|
|
1070
1289
|
* window.on("openLink", (url: string) => plugin.system.openUrl(url));
|
|
1071
1290
|
* window.set({ doc: await plugin.ui.markdown(text, { base: readmeUrl }) });
|
|
1072
1291
|
* await window.show();
|
|
@@ -1109,7 +1328,13 @@ export interface Ui {
|
|
|
1109
1328
|
*
|
|
1110
1329
|
* @capability That of the view: `ui.window`, `ui.mount` or `ui.settings`.
|
|
1111
1330
|
*
|
|
1112
|
-
* @example
|
|
1331
|
+
* @example Wire a window's label and buttons to the plugin (examples/bookmark)
|
|
1332
|
+
* A Bookmark window shows the saved position and two buttons that save
|
|
1333
|
+
* it and jump back to it. `bind` keeps the label and the buttons' enabled
|
|
1334
|
+
* state equal to the plugin's state: the SDK evaluates the function again
|
|
1335
|
+
* after every handler and player change and sends only what changed. The
|
|
1336
|
+
* buttons' Slint callbacks reach the plugin through `on`.
|
|
1337
|
+
*
|
|
1113
1338
|
* {@includeCode ../../examples/bookmark/main.ts#window}
|
|
1114
1339
|
*
|
|
1115
1340
|
* `ui/bookmark.slint`:
|
|
@@ -1154,10 +1379,16 @@ export interface View {
|
|
|
1154
1379
|
* @throws `Error` without a code (throws) On an open {@link Window}, for a
|
|
1155
1380
|
* name that has no handler yet.
|
|
1156
1381
|
*
|
|
1157
|
-
* @example
|
|
1382
|
+
* @example Keep a slider and the plugin's value in step
|
|
1383
|
+
* Moving the window's brightness slider updates the plugin's value, and
|
|
1384
|
+
* the slider shows it back. The Slint callback `brightness-changed`
|
|
1385
|
+
* reaches the handler with the slider's value as JSON; after the handler
|
|
1386
|
+
* returns, the SDK runs an update pass and `bind` sends the new value.
|
|
1387
|
+
* Callbacks of a tool window must be bound before its first `show`.
|
|
1158
1388
|
* ```ts
|
|
1159
1389
|
* const picture = plugin.windows.create("ui/picture.slint");
|
|
1160
1390
|
* let brightness = 0;
|
|
1391
|
+
* // camelCase here, `brightness-changed` in the Slint file.
|
|
1161
1392
|
* picture.on("brightnessChanged", (value: number) => {
|
|
1162
1393
|
* brightness = value;
|
|
1163
1394
|
* });
|
|
@@ -1196,6 +1427,10 @@ export interface View {
|
|
|
1196
1427
|
* @param props - Properties by name; see {@link Props}.
|
|
1197
1428
|
*
|
|
1198
1429
|
* @example Put a drop-down back when the user picks an entry the plugin refuses
|
|
1430
|
+
* Choosing "Custom" snaps the drop-down back to the last real preset.
|
|
1431
|
+
* The plugin cannot read a property back, only hear the callback; `set`
|
|
1432
|
+
* sends the value even though it equals the one sent before, which is
|
|
1433
|
+
* what pulls a control the user moved back to the plugin's value.
|
|
1199
1434
|
* ```ts
|
|
1200
1435
|
* const window = plugin.windows.create("ui/presets.slint");
|
|
1201
1436
|
* let presetIndex = 0;
|
|
@@ -1231,7 +1466,11 @@ export interface View {
|
|
|
1231
1466
|
* @param props - Returns the properties, by name.
|
|
1232
1467
|
* @returns A handle that stops this binding (the values last sent stay).
|
|
1233
1468
|
*
|
|
1234
|
-
* @example
|
|
1469
|
+
* @example Show what is playing and whether it can seek
|
|
1470
|
+
* The status window always shows the current file and enables its seek
|
|
1471
|
+
* controls only for seekable sources. The SDK calls the function right
|
|
1472
|
+
* away and again in every update pass (after player state changes and
|
|
1473
|
+
* after each handler) and sends only the values that changed.
|
|
1235
1474
|
* ```ts
|
|
1236
1475
|
* const status = plugin.windows.create("ui/status.slint");
|
|
1237
1476
|
* status.bind(() => ({
|
|
@@ -1281,10 +1520,19 @@ export interface View {
|
|
|
1281
1520
|
*
|
|
1282
1521
|
* @capability ui.window
|
|
1283
1522
|
*
|
|
1284
|
-
* @example
|
|
1523
|
+
* @example Open a window from a menu command (examples/bookmark)
|
|
1524
|
+
* Playback ▸ Bookmark… opens the Bookmark window. `create` only makes a
|
|
1525
|
+
* handle; the command's `run` calls `show`, which compiles the Slint file
|
|
1526
|
+
* and opens the window with the bound properties the first time, and
|
|
1527
|
+
* brings back the same window afterwards.
|
|
1528
|
+
*
|
|
1285
1529
|
* {@includeCode ../../examples/bookmark/main.ts#window,command}
|
|
1286
1530
|
*
|
|
1287
1531
|
* @example Stop work when the user closes the window
|
|
1532
|
+
* A clock window ticks every second while it is open and stops costing
|
|
1533
|
+
* anything once the user closes it. `plugin.update()` makes the SDK
|
|
1534
|
+
* evaluate the bound time again from a timer; `onClose` runs only for the
|
|
1535
|
+
* user's close, so the plugin stops the timer there.
|
|
1288
1536
|
* ```ts
|
|
1289
1537
|
* const clock = plugin.windows.create("ui/clock.slint");
|
|
1290
1538
|
* let timer: NodeJS.Timeout | undefined;
|
|
@@ -1298,6 +1546,7 @@ export interface View {
|
|
|
1298
1546
|
* path: ["@screen", "Clock…"],
|
|
1299
1547
|
* run: async () => {
|
|
1300
1548
|
* await clock.show();
|
|
1549
|
+
* // Re-evaluate `bind` once a second: bound values change only then.
|
|
1301
1550
|
* timer ??= setInterval(() => plugin.update(), 1000);
|
|
1302
1551
|
* },
|
|
1303
1552
|
* });
|
|
@@ -1339,12 +1588,17 @@ export interface Window extends View, Disposable {
|
|
|
1339
1588
|
* JPEG.
|
|
1340
1589
|
*
|
|
1341
1590
|
* @example Tell the user why a window did not open
|
|
1591
|
+
* When the inspector cannot open, the user sees why on the OSD instead
|
|
1592
|
+
* of nothing happening. `show` compiles the Slint file and opens the
|
|
1593
|
+
* window; it rejects with a code, such as `host` for a Slint file that
|
|
1594
|
+
* does not compile or `unavailable` without a user interface.
|
|
1342
1595
|
* ```ts
|
|
1343
1596
|
* const window = plugin.windows.create("ui/inspector.slint");
|
|
1344
1597
|
* try {
|
|
1345
1598
|
* await window.show();
|
|
1346
1599
|
* } catch (error) {
|
|
1347
|
-
*
|
|
1600
|
+
* // The code is data; `message` has the details for the log.
|
|
1601
|
+
* await plugin.osd.show("Cannot open the inspector ({})", [(error as PluginError).code]);
|
|
1348
1602
|
* }
|
|
1349
1603
|
* ```
|
|
1350
1604
|
*
|
|
@@ -1366,10 +1620,14 @@ export interface Window extends View, Disposable {
|
|
|
1366
1620
|
* @throws `missing` (rejects) The user closed the window just before (the
|
|
1367
1621
|
* close had not reached the plugin yet).
|
|
1368
1622
|
*
|
|
1369
|
-
* @example
|
|
1623
|
+
* @example Show and hide a window from a checkable menu item
|
|
1624
|
+
* Audio ▸ Levels shows the window with a check mark and hides it again.
|
|
1625
|
+
* `hide` keeps the window's instance and state, so showing it again is
|
|
1626
|
+
* instant; the check mark follows `visible`, and `onClose` resets it when
|
|
1627
|
+
* the user closes the window with its title-bar button.
|
|
1370
1628
|
* ```ts
|
|
1371
1629
|
* const levels = plugin.windows.create("ui/levels.slint");
|
|
1372
|
-
* let visible = false;
|
|
1630
|
+
* let visible = false; // what the check mark shows
|
|
1373
1631
|
* plugin.commands.add({
|
|
1374
1632
|
* id: "levels",
|
|
1375
1633
|
* path: ["@audio", "Levels"],
|
|
@@ -1402,6 +1660,10 @@ export interface Window extends View, Disposable {
|
|
|
1402
1660
|
* @throws `missing` (rejects) The user closed the window just before.
|
|
1403
1661
|
*
|
|
1404
1662
|
* @example Close the window from its own button
|
|
1663
|
+
* The editor's Done button closes the editor window. `close` destroys
|
|
1664
|
+
* the window instance but keeps the handle, its handlers and bindings,
|
|
1665
|
+
* so the next `show` opens a fresh window; `onClose` is not called for
|
|
1666
|
+
* the plugin's own close.
|
|
1405
1667
|
* ```ts
|
|
1406
1668
|
* const editor = plugin.windows.create("ui/editor.slint");
|
|
1407
1669
|
* editor.on("done", () => editor.close());
|
|
@@ -1434,7 +1696,12 @@ export interface Window extends View, Disposable {
|
|
|
1434
1696
|
* @param listener - Called with no arguments; may be async.
|
|
1435
1697
|
* @returns A handle that removes the listener.
|
|
1436
1698
|
*
|
|
1437
|
-
* @example Remember that the user closed
|
|
1699
|
+
* @example Remember that the user closed the window
|
|
1700
|
+
* The plugin records that the notes window was closed, so it does not
|
|
1701
|
+
* reopen it at the next start. `onClose` runs for the title-bar button,
|
|
1702
|
+
* the player's close keys (Cmd+W, Ctrl+W, Ctrl+F4) and the system's own
|
|
1703
|
+
* close request such as Alt+F4 (not for the plugin's own `close`); the
|
|
1704
|
+
* store writes the change to `window.json` shortly after.
|
|
1438
1705
|
* ```ts
|
|
1439
1706
|
* const store = plugin.storage.json("window", { defaults: { open: false } });
|
|
1440
1707
|
* const window = plugin.windows.create("ui/notes.slint");
|
|
@@ -1460,7 +1727,11 @@ export interface Window extends View, Disposable {
|
|
|
1460
1727
|
*
|
|
1461
1728
|
* @capability ui.window
|
|
1462
1729
|
*
|
|
1463
|
-
* @example
|
|
1730
|
+
* @example Create a window and open it from a menu (examples/bookmark)
|
|
1731
|
+
* A Bookmark window opens from Playback ▸ Bookmark…. `create` returns a
|
|
1732
|
+
* handle without opening anything; its bindings and callbacks are set up
|
|
1733
|
+
* first, and `show` opens the window when the command runs.
|
|
1734
|
+
*
|
|
1464
1735
|
* {@includeCode ../../examples/bookmark/main.ts#window,command}
|
|
1465
1736
|
*
|
|
1466
1737
|
* @see {@link Window}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
|
|
@@ -1495,7 +1766,10 @@ export interface Windows {
|
|
|
1495
1766
|
* @throws `conflict` (throws) This plugin already has a window with this id
|
|
1496
1767
|
* (SDK 1.3.0; before, a plain `Error` without `code`).
|
|
1497
1768
|
*
|
|
1498
|
-
* @example
|
|
1769
|
+
* @example Open two windows from one Slint file
|
|
1770
|
+
* Two compare windows open side by side, one labelled A and one B. A
|
|
1771
|
+
* window's id defaults to its file name, so the second window of the
|
|
1772
|
+
* same file needs its own `id`; each handle has its own properties.
|
|
1499
1773
|
* ```ts
|
|
1500
1774
|
* const left = plugin.windows.create("ui/compare.slint", { id: "compare-left" });
|
|
1501
1775
|
* const right = plugin.windows.create("ui/compare.slint", { id: "compare-right" });
|
|
@@ -1596,12 +1870,18 @@ export interface PanelState {
|
|
|
1596
1870
|
*
|
|
1597
1871
|
* @capability ui.mount; `events` to follow changes made elsewhere.
|
|
1598
1872
|
*
|
|
1599
|
-
* @example
|
|
1873
|
+
* @example Add a "Show sidebar" item that works with any sidebar panel
|
|
1874
|
+
* Tools ▸ Show sidebar shows or hides the sidebar, whichever plugin's
|
|
1875
|
+
* panel is in it, with a check mark while it is shown. The position does
|
|
1876
|
+
* not belong to the plugin that mounted the panel: `state` is `null`
|
|
1877
|
+
* while nobody has a panel there (the item is disabled then), and
|
|
1878
|
+
* `toggle` flips the user's remembered preference.
|
|
1600
1879
|
* ```ts
|
|
1601
1880
|
* const sidebar = plugin.panels.sidebar;
|
|
1602
1881
|
* plugin.commands.add({
|
|
1603
1882
|
* id: "sidebar",
|
|
1604
1883
|
* path: ["Tools", "Show sidebar"],
|
|
1884
|
+
* // `state` is null while no plugin has a panel in the sidebar.
|
|
1605
1885
|
* checked: () => sidebar.state?.preferred ?? false,
|
|
1606
1886
|
* enabled: () => sidebar.state !== null,
|
|
1607
1887
|
* run: () => sidebar.toggle(),
|
|
@@ -1644,9 +1924,14 @@ export interface PanelSlot {
|
|
|
1644
1924
|
* @param listener - Receives the new state.
|
|
1645
1925
|
* @returns A handle that removes the listener.
|
|
1646
1926
|
*
|
|
1647
|
-
* @example
|
|
1927
|
+
* @example Tell the user when the window is too narrow for the sidebar
|
|
1928
|
+
* When the sidebar should show but the window has no room for it, the
|
|
1929
|
+
* OSD says so. With `events`, the listener runs on every change of the
|
|
1930
|
+
* position, whoever caused it. The player reports `reason: "space"` when
|
|
1931
|
+
* the preference is to show the sidebar but the window is too small.
|
|
1648
1932
|
* ```ts
|
|
1649
1933
|
* plugin.panels.sidebar.onChange((state) => {
|
|
1934
|
+
* // `null`: no panel is mounted in the sidebar any more.
|
|
1650
1935
|
* if (state?.reason === "space") void plugin.osd.show("Widen the window to see the playlist");
|
|
1651
1936
|
* });
|
|
1652
1937
|
* ```
|
|
@@ -1672,9 +1957,15 @@ export interface PanelSlot {
|
|
|
1672
1957
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1673
1958
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1674
1959
|
*
|
|
1675
|
-
* @example
|
|
1960
|
+
* @example Hide the bottom bar for a clean picture
|
|
1961
|
+
* The control bar disappears, as if the user had hidden it from the menu,
|
|
1962
|
+
* and stays hidden at the next start because the preference is
|
|
1963
|
+
* remembered. `setVisible` rejects with `missing` when no panel is in the
|
|
1964
|
+
* bottom bar, which this plugin treats as nothing to hide.
|
|
1676
1965
|
* ```ts
|
|
1677
|
-
* await plugin.panels.bottom.setVisible(false)
|
|
1966
|
+
* await plugin.panels.bottom.setVisible(false).catch((error: PluginError) => {
|
|
1967
|
+
* if (error.code !== "missing") throw error; // no bar mounted: nothing to do
|
|
1968
|
+
* });
|
|
1678
1969
|
* ```
|
|
1679
1970
|
*
|
|
1680
1971
|
* @since 1.0.0
|
|
@@ -1695,11 +1986,16 @@ export interface PanelSlot {
|
|
|
1695
1986
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1696
1987
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1697
1988
|
*
|
|
1698
|
-
* @example
|
|
1989
|
+
* @example Toggle the control bar from a checkable menu item
|
|
1990
|
+
* Screen ▸ Control bar shows or hides the bottom bar and carries a check
|
|
1991
|
+
* mark while it is shown. `toggle` reads the player's current preference
|
|
1992
|
+
* first, so it is right even without `events`; `checked` is evaluated
|
|
1993
|
+
* again after the command runs and follows `state`.
|
|
1699
1994
|
* ```ts
|
|
1700
1995
|
* plugin.commands.add({
|
|
1701
1996
|
* id: "toggle-bar",
|
|
1702
1997
|
* path: ["@screen", "Control bar"],
|
|
1998
|
+
* // The user's preference, remembered across launches.
|
|
1703
1999
|
* checked: () => plugin.panels.bottom.state?.preferred ?? false,
|
|
1704
2000
|
* run: () => plugin.panels.bottom.toggle(),
|
|
1705
2001
|
* });
|
|
@@ -1731,7 +2027,11 @@ export interface PanelSlot {
|
|
|
1731
2027
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1732
2028
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1733
2029
|
*
|
|
1734
|
-
* @example
|
|
2030
|
+
* @example Switch the sidebar to the playlist's tab
|
|
2031
|
+
* A command makes the playlist the sidebar's current tab without showing
|
|
2032
|
+
* or hiding the sidebar, like a click on its tab. `activate` takes the
|
|
2033
|
+
* manifest id of the plugin whose tab to show and rejects with
|
|
2034
|
+
* `missing` when that plugin has no panel there.
|
|
1735
2035
|
* ```ts
|
|
1736
2036
|
* plugin.commands.add({
|
|
1737
2037
|
* id: "to-playlist",
|
|
@@ -1757,12 +2057,17 @@ export interface PanelSlot {
|
|
|
1757
2057
|
*
|
|
1758
2058
|
* @capability ui.mount
|
|
1759
2059
|
*
|
|
1760
|
-
* @example
|
|
2060
|
+
* @example Add a "Detach sidebar" item
|
|
2061
|
+
* Tools ▸ Detach sidebar floats the sidebar in its own window and docks
|
|
2062
|
+
* it again, with a check mark while it floats. `setDetached` moves all
|
|
2063
|
+
* the sidebar's tabs; the floating window holds new instances of the
|
|
2064
|
+
* panels, which keep the plugins' properties and callbacks.
|
|
1761
2065
|
* ```ts
|
|
1762
2066
|
* const sidebar = plugin.panels.sidebar;
|
|
1763
2067
|
* plugin.commands.add({
|
|
1764
2068
|
* id: "detach",
|
|
1765
2069
|
* path: ["Tools", "Detach sidebar"],
|
|
2070
|
+
* // Detaching moves every tab of the sidebar into the floating window.
|
|
1766
2071
|
* checked: () => sidebar.state?.detached ?? false,
|
|
1767
2072
|
* enabled: () => sidebar.state !== null,
|
|
1768
2073
|
* run: () => sidebar.setDetached(!sidebar.state?.detached),
|
|
@@ -1785,7 +2090,10 @@ export interface SidebarSlot extends PanelSlot {
|
|
|
1785
2090
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1786
2091
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1787
2092
|
*
|
|
1788
|
-
* @example
|
|
2093
|
+
* @example Float the sidebar or dock it back
|
|
2094
|
+
* The sidebar moves into its own window, or back into the main window
|
|
2095
|
+
* when it already floats. `state` says whether it is detached now;
|
|
2096
|
+
* `setDetached` resolves once the layout changed, with `state` refreshed.
|
|
1789
2097
|
* ```ts
|
|
1790
2098
|
* const sidebar = plugin.panels.sidebar;
|
|
1791
2099
|
* await sidebar.setDetached(!sidebar.state?.detached);
|
|
@@ -1818,7 +2126,13 @@ export interface SidebarSlot extends PanelSlot {
|
|
|
1818
2126
|
*
|
|
1819
2127
|
* @capability ui.mount
|
|
1820
2128
|
*
|
|
1821
|
-
* @example
|
|
2129
|
+
* @example Mount a sidebar list with a right-click menu (examples/panel-menu)
|
|
2130
|
+
* The sidebar gets an "Example list" tab whose rows can be selected and
|
|
2131
|
+
* have a context menu. `mount` returns at once and the player adds the
|
|
2132
|
+
* panel when it rebuilds its main window; `bind` keeps the rows and the
|
|
2133
|
+
* selection on the panel's bridge global, and the `context-menu` callback
|
|
2134
|
+
* opens a menu of the plugin's commands where the user clicked.
|
|
2135
|
+
*
|
|
1822
2136
|
* {@includeCode ../../examples/panel-menu/main.ts#mount,menu}
|
|
1823
2137
|
*
|
|
1824
2138
|
* `ui/panel.slint`:
|
|
@@ -1893,7 +2207,12 @@ export interface Panel extends View, Disposable {
|
|
|
1893
2207
|
* @param listener - Receives the new state.
|
|
1894
2208
|
* @returns A handle that removes the listener.
|
|
1895
2209
|
*
|
|
1896
|
-
* @example
|
|
2210
|
+
* @example Notice when the list moves to a background tab
|
|
2211
|
+
* The plugin logs when the user switches the sidebar to another plugin's
|
|
2212
|
+
* tab, for example to pause work the list does. With `events` the
|
|
2213
|
+
* listener runs on every change. `reason: "otherTab"` exists only in a
|
|
2214
|
+
* panel's own state: the SDK sets it when the sidebar's current tab is
|
|
2215
|
+
* another plugin's (the position's state never has it).
|
|
1897
2216
|
* ```ts
|
|
1898
2217
|
* const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1899
2218
|
* panel.onChange((state) => {
|
|
@@ -1923,7 +2242,10 @@ export interface Panel extends View, Disposable {
|
|
|
1923
2242
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1924
2243
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1925
2244
|
*
|
|
1926
|
-
* @example
|
|
2245
|
+
* @example Bring the plugin's sidebar panel to the front
|
|
2246
|
+
* A command shows the List tab: it becomes the current tab and the
|
|
2247
|
+
* sidebar is shown, and the preference is remembered. `show` waits for
|
|
2248
|
+
* the mount first, so it can run right after `mount`.
|
|
1927
2249
|
* ```ts
|
|
1928
2250
|
* const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1929
2251
|
* plugin.commands.add({ id: "show", title: "Show the list", run: () => list.show() });
|
|
@@ -1951,13 +2273,18 @@ export interface Panel extends View, Disposable {
|
|
|
1951
2273
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1952
2274
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1953
2275
|
*
|
|
1954
|
-
* @example
|
|
2276
|
+
* @example Toggle a sidebar panel with F6, like the playlist
|
|
2277
|
+
* F6 and Screen ▸ List show the List tab, or hide the sidebar when the
|
|
2278
|
+
* list is already in front. `toggle` reads the player's state first:
|
|
2279
|
+
* it hides the position only when it is shown and this panel's tab is
|
|
2280
|
+
* the current one, otherwise it shows the panel.
|
|
1955
2281
|
* ```ts
|
|
1956
2282
|
* const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1957
2283
|
* plugin.commands.add({
|
|
1958
2284
|
* id: "list",
|
|
1959
2285
|
* path: ["@screen", "List"],
|
|
1960
2286
|
* bindings: ["F6"],
|
|
2287
|
+
* // Checked while the sidebar is shown with this list in front.
|
|
1961
2288
|
* checked: () => (list.state?.preferred ?? false) && (list.state?.active ?? false),
|
|
1962
2289
|
* run: () => list.toggle(),
|
|
1963
2290
|
* });
|
|
@@ -1986,7 +2313,12 @@ export interface Panel extends View, Disposable {
|
|
|
1986
2313
|
*
|
|
1987
2314
|
* @param locks - The locks to change; omitted ones keep their value.
|
|
1988
2315
|
*
|
|
1989
|
-
* @example Keep the bar while a search field has focus
|
|
2316
|
+
* @example Keep the bar on screen while a search field has focus
|
|
2317
|
+
* In fullscreen the bottom bar stays visible while the user types in
|
|
2318
|
+
* its search field, instead of hiding when the pointer leaves it. The
|
|
2319
|
+
* Slint side reports the field's focus through a callback, and
|
|
2320
|
+
* `setInteraction` sets or clears the `focused` lock; other locks keep
|
|
2321
|
+
* their values.
|
|
1990
2322
|
* ```ts
|
|
1991
2323
|
* const panel = plugin.panels.mount("bottom", "ui/bar.slint");
|
|
1992
2324
|
* panel.on("searchFocused", (focused: boolean) => panel.setInteraction({ focused }));
|
|
@@ -2028,7 +2360,12 @@ export interface Panel extends View, Disposable {
|
|
|
2028
2360
|
* finite.
|
|
2029
2361
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
2030
2362
|
*
|
|
2031
|
-
* @example
|
|
2363
|
+
* @example Open a context menu where the user right-clicked (examples/panel-menu)
|
|
2364
|
+
* Right-clicking the list opens a menu with "Select the first item" and
|
|
2365
|
+
* "Remove the selected item". The Slint file passes the pointer position
|
|
2366
|
+
* in window pixels with its `context-menu` callback; `showMenu` opens the
|
|
2367
|
+
* plugin's commands there, and picking one runs that command.
|
|
2368
|
+
*
|
|
2032
2369
|
* {@includeCode ../../examples/panel-menu/main.ts#menu}
|
|
2033
2370
|
*
|
|
2034
2371
|
* @since 1.0.0
|
|
@@ -2058,12 +2395,16 @@ export interface Panel extends View, Disposable {
|
|
|
2058
2395
|
* @returns A handle that removes the listener.
|
|
2059
2396
|
*
|
|
2060
2397
|
* @example Add dropped files to a list
|
|
2398
|
+
* Files dropped on the Files panel are added to its list. While the
|
|
2399
|
+
* listener exists, drops on the panel come here as one group instead of
|
|
2400
|
+
* going to the drop providers; after the listener returns, the SDK runs
|
|
2401
|
+
* an update pass and `bind` sends the longer list.
|
|
2061
2402
|
* ```ts
|
|
2062
2403
|
* const files: string[] = [];
|
|
2063
2404
|
* const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "Files" });
|
|
2064
2405
|
* panel.bind(() => ({ rows: files }));
|
|
2065
2406
|
* panel.onDrop(({ paths }) => {
|
|
2066
|
-
* files.push(...paths);
|
|
2407
|
+
* files.push(...paths); // in the order the file manager gave them
|
|
2067
2408
|
* });
|
|
2068
2409
|
* ```
|
|
2069
2410
|
*
|
|
@@ -2109,16 +2450,27 @@ export interface Drops {
|
|
|
2109
2450
|
* Take files dropped on the video area. The first provider registered for
|
|
2110
2451
|
* an extension gets it; files nobody takes are ignored.
|
|
2111
2452
|
*
|
|
2112
|
-
* Paths are grouped by extension
|
|
2453
|
+
* Paths are grouped by provider (an extension, case-insensitive, belongs
|
|
2454
|
+
* to the provider that registered it first) in the order the file
|
|
2113
2455
|
* manager gave them, one call per provider. Folders are not expanded.
|
|
2114
2456
|
*
|
|
2115
2457
|
* @throws `invalid` synchronously for malformed extensions.
|
|
2116
2458
|
*
|
|
2117
|
-
* @example
|
|
2459
|
+
* @example Load dropped subtitle files
|
|
2460
|
+
* Dropping .srt or .ass files on the video loads each one as a subtitle
|
|
2461
|
+
* track. Each extension belongs to the provider that registered it first;
|
|
2462
|
+
* the player groups the dropped paths by provider and calls each provider
|
|
2463
|
+
* once with all of its paths, so .srt and .ass files dropped together
|
|
2464
|
+
* arrive in one call. (A panel under the pointer that listens with
|
|
2465
|
+
* `onDrop` takes the whole drop first.) The plugin reads each file itself
|
|
2466
|
+
* and uploads its bytes with `subtitles.load`.
|
|
2118
2467
|
* ```ts
|
|
2468
|
+
* import { readFile } from "node:fs/promises";
|
|
2469
|
+
* // ---cut---
|
|
2119
2470
|
* plugin.drops.provide({
|
|
2120
2471
|
* extensions: ["srt", "ass"],
|
|
2121
2472
|
* drop: async ({ paths }) => {
|
|
2473
|
+
* // Only .srt and .ass files reach this provider.
|
|
2122
2474
|
* for (const path of paths)
|
|
2123
2475
|
* await plugin.subtitles.load(await readFile(path), {
|
|
2124
2476
|
* format: path.endsWith(".ass") ? "ass" : "srt",
|
|
@@ -2164,12 +2516,16 @@ export type ThemeId = "vestra" | "vestra-light";
|
|
|
2164
2516
|
* @capability None to read `theme`; `events` to follow changes; `ui.theme`
|
|
2165
2517
|
* to change it.
|
|
2166
2518
|
*
|
|
2167
|
-
* @example
|
|
2519
|
+
* @example Offer the light palette as a menu item
|
|
2520
|
+
* Settings ▸ Light colours switches every view of the player to the light
|
|
2521
|
+
* palette and carries a radio mark while it is in use. `set` asks the
|
|
2522
|
+
* player to switch (it saves the choice); `theme` holds the palette in use,
|
|
2523
|
+
* and `checked` reads it again after every handler.
|
|
2168
2524
|
* ```ts
|
|
2169
2525
|
* plugin.commands.add({
|
|
2170
2526
|
* id: "light-colours",
|
|
2171
2527
|
* path: ["@settings", "Light colours"],
|
|
2172
|
-
* radio: true,
|
|
2528
|
+
* radio: true, // a round mark instead of a check mark
|
|
2173
2529
|
* checked: () => plugin.appearance.theme === "vestra-light",
|
|
2174
2530
|
* run: () => plugin.appearance.set("vestra-light"),
|
|
2175
2531
|
* });
|
|
@@ -2200,10 +2556,16 @@ export interface Appearance {
|
|
|
2200
2556
|
* @param listener - Receives the new palette.
|
|
2201
2557
|
* @returns A handle that removes the listener.
|
|
2202
2558
|
*
|
|
2203
|
-
* @example
|
|
2559
|
+
* @example Redraw a picture the plugin renders when the palette changes
|
|
2560
|
+
* The plugin renders a chart image in the palette's colours and draws it
|
|
2561
|
+
* again when the user or another plugin switches palettes. Views that use
|
|
2562
|
+
* `Theme` follow on their own; this listener (which needs `events`) is
|
|
2563
|
+
* for what the plugin draws itself.
|
|
2204
2564
|
* ```ts
|
|
2565
|
+
* declare function renderChart(dark: boolean): Promise<void>;
|
|
2566
|
+
* // ---cut---
|
|
2205
2567
|
* plugin.appearance.onChange((theme) => {
|
|
2206
|
-
* void
|
|
2568
|
+
* void renderChart(theme === "vestra"); // the dark default
|
|
2207
2569
|
* });
|
|
2208
2570
|
* ```
|
|
2209
2571
|
*
|
|
@@ -2226,7 +2588,11 @@ export interface Appearance {
|
|
|
2226
2588
|
* @throws `invalid` (rejects) `theme` is not a {@link ThemeId}.
|
|
2227
2589
|
* @throws `capability` (rejects) The manifest does not declare `ui.theme`.
|
|
2228
2590
|
*
|
|
2229
|
-
* @example
|
|
2591
|
+
* @example Switch to the light palette
|
|
2592
|
+
* Every view of the player turns light, and stays light after a restart.
|
|
2593
|
+
* The player applies the palette to every open view and saves the
|
|
2594
|
+
* choice; when the promise resolves, `appearance.theme` already has the
|
|
2595
|
+
* new value. Without `ui.theme` it rejects with `capability`.
|
|
2230
2596
|
* ```ts
|
|
2231
2597
|
* await plugin.appearance.set("vestra-light");
|
|
2232
2598
|
* ```
|
|
@@ -2267,10 +2633,14 @@ export interface Appearance {
|
|
|
2267
2633
|
* @throws `capability` (rejects) The manifest does not declare `ui.theme`.
|
|
2268
2634
|
* @throws `unsupported` (rejects) A player before 0.4.3.
|
|
2269
2635
|
*
|
|
2270
|
-
* @example
|
|
2636
|
+
* @example Choose German for the next start
|
|
2637
|
+
* The player will start in German next time, and the user is told so.
|
|
2638
|
+
* `setLanguage` only saves the choice: the language of this run
|
|
2639
|
+
* (`i18n.locale`) never changes while the player runs. Players before
|
|
2640
|
+
* 0.4.3 reject it with `unsupported`.
|
|
2271
2641
|
* ```ts
|
|
2272
2642
|
* await plugin.appearance.setLanguage("de");
|
|
2273
|
-
* await plugin.osd.show("Takes effect after restart");
|
|
2643
|
+
* await plugin.osd.show("Takes effect after restart"); // translated by the player
|
|
2274
2644
|
* ```
|
|
2275
2645
|
*
|
|
2276
2646
|
* @since 1.5.9
|
|
@@ -2417,6 +2787,10 @@ export type TextValue = string | Text | TextList;
|
|
|
2417
2787
|
* @capability None.
|
|
2418
2788
|
*
|
|
2419
2789
|
* @example Format a date the way the interface language writes it
|
|
2790
|
+
* The OSD shows today's date in the user's language and date format. The
|
|
2791
|
+
* sentence is a template the player translates; only the data in it, the
|
|
2792
|
+
* date, is formatted by the plugin, with `i18n.locale` (the language the
|
|
2793
|
+
* player runs in, fixed for the whole run).
|
|
2420
2794
|
* ```ts
|
|
2421
2795
|
* const day = new Date().toLocaleDateString(plugin.i18n.locale);
|
|
2422
2796
|
* await plugin.osd.show("Today is {}", [day]);
|
|
@@ -2450,17 +2824,25 @@ export interface I18n {
|
|
|
2450
2824
|
* @param args - Values for its placeholders.
|
|
2451
2825
|
* @throws `invalid` A malformed or over-limit value (`TEXT_LIMITS`).
|
|
2452
2826
|
*
|
|
2453
|
-
* @example
|
|
2827
|
+
* @example Fill a drop-down with fixed words and data
|
|
2828
|
+
* The drop-down lists "No Audio" and one "Track 7 · en" entry per track,
|
|
2829
|
+
* in the user's language. A `[string]` model may hold `text()` values: the
|
|
2830
|
+
* player translates each source text with the plugin's catalog and fills
|
|
2831
|
+
* in its arguments, so the plugin never composes translated strings.
|
|
2454
2832
|
* ```ts
|
|
2455
2833
|
* const { text } = plugin.i18n;
|
|
2456
2834
|
* const picker = plugin.windows.create("ui/clip.slint");
|
|
2457
2835
|
* const tracks = [{ id: 7, title: "en" }];
|
|
2836
|
+
* // Each entry is translated, then filled in: "Track 7 · en".
|
|
2458
2837
|
* picker.set({
|
|
2459
2838
|
* labels: [text("No Audio"), ...tracks.map((t) => text("Track {} · {}", [t.id, t.title]))],
|
|
2460
2839
|
* });
|
|
2461
2840
|
* ```
|
|
2462
2841
|
*
|
|
2463
|
-
* @example
|
|
2842
|
+
* @example Build a message from translated parts
|
|
2843
|
+
* The OSD says "Download failed: Connection timed out", both parts in the
|
|
2844
|
+
* user's language. An argument made with `text()` is translated too,
|
|
2845
|
+
* while string arguments are shown as they are.
|
|
2464
2846
|
* ```ts
|
|
2465
2847
|
* await plugin.osd.show("Download failed: {}", [plugin.i18n.text("Connection timed out")]);
|
|
2466
2848
|
* ```
|
|
@@ -2480,7 +2862,11 @@ export interface I18n {
|
|
|
2480
2862
|
* @param style - `"list"` by default.
|
|
2481
2863
|
* @throws `invalid` A malformed or over-limit value.
|
|
2482
2864
|
*
|
|
2483
|
-
* @example
|
|
2865
|
+
* @example List file names the way the language joins them
|
|
2866
|
+
* The OSD says "Removed missing shaders: a.glsl, b.glsl" in English and
|
|
2867
|
+
* joins the names with "、" in Chinese. `list` makes a value the player
|
|
2868
|
+
* joins per interface language; the names themselves are data and stay
|
|
2869
|
+
* as they are.
|
|
2484
2870
|
* ```ts
|
|
2485
2871
|
* const missing = ["a.glsl", "b.glsl"];
|
|
2486
2872
|
* await plugin.osd.show("Removed missing shaders: {}", [plugin.i18n.list(missing)]);
|
|
@@ -2504,13 +2890,25 @@ export interface I18n {
|
|
|
2504
2890
|
* @param args - Values for its placeholders.
|
|
2505
2891
|
* @throws `invalid` A malformed or over-limit value.
|
|
2506
2892
|
*
|
|
2507
|
-
* @example
|
|
2893
|
+
* @example Fail an open with a message the user can read
|
|
2894
|
+
* Opening a `.ch8` file the plugin cannot load shows "The program file
|
|
2895
|
+
* was not found." in the user's language. The player shows the message of
|
|
2896
|
+
* an error thrown from `resolve`; `i18n.error` makes an `Error` whose text
|
|
2897
|
+
* the player translates with this plugin's catalog, while its `message`
|
|
2898
|
+
* stays English for logs and older players.
|
|
2508
2899
|
* ```ts
|
|
2900
|
+
* import { readFile } from "node:fs/promises";
|
|
2901
|
+
* declare function startEmulator(program: Uint8Array): ResolvedStream;
|
|
2902
|
+
* // ---cut---
|
|
2509
2903
|
* plugin.resolvers.add({
|
|
2510
|
-
* id: "
|
|
2511
|
-
* extensions: ["
|
|
2512
|
-
* async resolve() {
|
|
2513
|
-
*
|
|
2904
|
+
* id: "chip8",
|
|
2905
|
+
* extensions: ["ch8"],
|
|
2906
|
+
* async resolve(path, signal) {
|
|
2907
|
+
* const program = await readFile(path, { signal }).catch(() => {
|
|
2908
|
+
* // Shown as the open's failure, translated (locales/<lang>.po).
|
|
2909
|
+
* throw plugin.i18n.error("The program file was not found.");
|
|
2910
|
+
* });
|
|
2911
|
+
* return startEmulator(program);
|
|
2514
2912
|
* },
|
|
2515
2913
|
* });
|
|
2516
2914
|
* ```
|
|
@@ -2536,7 +2934,11 @@ export interface I18n {
|
|
|
2536
2934
|
* @returns The translated string.
|
|
2537
2935
|
* @throws `invalid` A malformed value.
|
|
2538
2936
|
*
|
|
2539
|
-
* @example
|
|
2937
|
+
* @example Copy version information in the user's language
|
|
2938
|
+
* "Copy info" puts two translated lines on the clipboard. Text that
|
|
2939
|
+
* leaves the player cannot be translated by it, so `resolve` turns the
|
|
2940
|
+
* values into the string the player would show, using this plugin's
|
|
2941
|
+
* catalog; inside the player, pass the values themselves instead.
|
|
2540
2942
|
* ```ts
|
|
2541
2943
|
* const { text, list, resolve } = plugin.i18n;
|
|
2542
2944
|
* const lines = list([text("Version {}", ["1.0"]), text("Cores: {}", [8])], "lines");
|
|
@@ -2589,7 +2991,12 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
2589
2991
|
*
|
|
2590
2992
|
* @capability ui.settings
|
|
2591
2993
|
*
|
|
2592
|
-
* @example
|
|
2994
|
+
* @example Put two preferences on a page and keep them in a store
|
|
2995
|
+
* Options ▸ Video ▸ Blur shows two check boxes whose values survive
|
|
2996
|
+
* restarts. The checkboxes are bound two-way (`<=>`) to `in-out`
|
|
2997
|
+
* properties of the bridge global, so the user's clicks show at once; the
|
|
2998
|
+
* `toggled` callbacks tell the plugin, which saves the new value, and
|
|
2999
|
+
* `bind` writes the stored values when the page opens.
|
|
2593
3000
|
* ```ts
|
|
2594
3001
|
* const store = plugin.storage.json("blur", {
|
|
2595
3002
|
* defaults: { restoreOnStart: false, messages: true },
|
|
@@ -2603,6 +3010,7 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
2603
3010
|
* restoreOnStart: store.value.restoreOnStart,
|
|
2604
3011
|
* messages: store.value.messages,
|
|
2605
3012
|
* }));
|
|
3013
|
+
* // The bridge's `restore-toggled(bool)` callback, in camelCase.
|
|
2606
3014
|
* page.on("restoreToggled", (on: boolean) =>
|
|
2607
3015
|
* store.save({ ...store.value, restoreOnStart: on }),
|
|
2608
3016
|
* );
|
|
@@ -2617,6 +3025,7 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
2617
3025
|
* import { CheckBox } from "std-widgets.slint";
|
|
2618
3026
|
* import { Theme } from "@vestra/theme.slint";
|
|
2619
3027
|
*
|
|
3028
|
+
* // The bridge: what the plugin writes and the callbacks it handles.
|
|
2620
3029
|
* export global BlurPage {
|
|
2621
3030
|
* in-out property <bool> restore-on-start;
|
|
2622
3031
|
* in-out property <bool> messages: true;
|
|
@@ -2687,7 +3096,11 @@ export type SettingsPage = View & Disposable & {
|
|
|
2687
3096
|
*
|
|
2688
3097
|
* @capability ui.settings
|
|
2689
3098
|
*
|
|
2690
|
-
* @example
|
|
3099
|
+
* @example Add a page that edits a stored setting
|
|
3100
|
+
* Options ▸ Playback ▸ Resume shows the plugin's Resume switch. `page`
|
|
3101
|
+
* registers the page with the Options window (compiled by the player on
|
|
3102
|
+
* its own), `bind` sends the stored value, and the page's callback saves
|
|
3103
|
+
* the user's change.
|
|
2691
3104
|
* ```ts
|
|
2692
3105
|
* const store = plugin.storage.json("settings", { defaults: { resume: true } });
|
|
2693
3106
|
* const page = plugin.settings.page("ui/settings.slint", {
|
|
@@ -2696,6 +3109,7 @@ export type SettingsPage = View & Disposable & {
|
|
|
2696
3109
|
* title: "Resume",
|
|
2697
3110
|
* });
|
|
2698
3111
|
* page.bind(() => ({ resume: store.value.resume }));
|
|
3112
|
+
* // The bridge's `resume-toggled(bool)`: save, and `bind` sends it back.
|
|
2699
3113
|
* page.on("resumeToggled", (on: boolean) => store.save({ ...store.value, resume: on }));
|
|
2700
3114
|
* ```
|
|
2701
3115
|
*
|
|
@@ -2732,13 +3146,17 @@ export interface Settings {
|
|
|
2732
3146
|
* one component and one global. A file that cannot be read throws Node's
|
|
2733
3147
|
* file system error (`code` `ENOENT` and so on).
|
|
2734
3148
|
*
|
|
2735
|
-
* @example
|
|
3149
|
+
* @example Register a page and report when the player refuses it
|
|
3150
|
+
* An "My plugin" page appears under Plugins, or the reason it could not
|
|
3151
|
+
* be added is logged. `page` returns at once; the player compiles the
|
|
3152
|
+
* page on its own, and `ready` rejects (for example with `ui-prepare`
|
|
3153
|
+
* and the compiler's diagnostics) when it refuses it.
|
|
2736
3154
|
* ```ts
|
|
2737
3155
|
* const page = plugin.settings.page("ui/advanced.slint", {
|
|
2738
3156
|
* id: "advanced",
|
|
2739
3157
|
* category: "plugins",
|
|
2740
3158
|
* title: "My plugin",
|
|
2741
|
-
* order: 10,
|
|
3159
|
+
* order: 10, // after pages with a smaller order in the category
|
|
2742
3160
|
* });
|
|
2743
3161
|
* page.ready.catch((error: PluginError) => console.error(error.message));
|
|
2744
3162
|
* ```
|
|
@@ -2776,7 +3194,10 @@ export interface Settings {
|
|
|
2776
3194
|
* @throws `capability` (rejects) The manifest does not declare
|
|
2777
3195
|
* `ui.settings`.
|
|
2778
3196
|
*
|
|
2779
|
-
* @example
|
|
3197
|
+
* @example Open the plugin's page from an "Options…" button
|
|
3198
|
+
* The button in the plugin's window opens the Options window at the
|
|
3199
|
+
* plugin's Blur page, or brings it to the front. `show` takes the id the
|
|
3200
|
+
* page was registered with and rejects with `invalid` for any other.
|
|
2780
3201
|
* ```ts
|
|
2781
3202
|
* const controls = plugin.windows.create("ui/window.slint");
|
|
2782
3203
|
* controls.on("openOptions", () => plugin.settings.show("blur"));
|
|
@@ -2800,8 +3221,13 @@ export interface Settings {
|
|
|
2800
3221
|
* @capability ui.mount; `events` to follow changes.
|
|
2801
3222
|
*
|
|
2802
3223
|
* @example React when a video picture appears
|
|
3224
|
+
* The OSD says "Video is playing" when the backdrop gives way to a video
|
|
3225
|
+
* picture. The player decides when the backdrop shows (nothing open,
|
|
3226
|
+
* loading, audio without a picture); `effective` turns false when a video
|
|
3227
|
+
* frame replaces it.
|
|
2803
3228
|
* ```ts
|
|
2804
3229
|
* plugin.panels.backdrop.onChange((state) => {
|
|
3230
|
+
* // `null`: no backdrop is mounted.
|
|
2805
3231
|
* if (state?.effective === false) void plugin.osd.show("Video is playing");
|
|
2806
3232
|
* });
|
|
2807
3233
|
* ```
|
|
@@ -2827,7 +3253,10 @@ export interface BackdropSlot {
|
|
|
2827
3253
|
* @param listener - Receives the new state (`null`: no backdrop).
|
|
2828
3254
|
* @returns A handle that removes the listener.
|
|
2829
3255
|
*
|
|
2830
|
-
* @example
|
|
3256
|
+
* @example Log when the backdrop shows or hides
|
|
3257
|
+
* The plugin's log records each change of the backdrop position. The
|
|
3258
|
+
* listener runs when the player starts or stops showing the backdrop
|
|
3259
|
+
* and with `null` when it is unmounted.
|
|
2831
3260
|
* ```ts
|
|
2832
3261
|
* plugin.panels.backdrop.onChange((state) => console.log("backdrop", state));
|
|
2833
3262
|
* ```
|
|
@@ -2863,7 +3292,13 @@ export interface BackdropSlot {
|
|
|
2863
3292
|
*
|
|
2864
3293
|
* @capability ui.mount
|
|
2865
3294
|
*
|
|
2866
|
-
* @example
|
|
3295
|
+
* @example Mount a list in the sidebar (examples/panel-menu)
|
|
3296
|
+
* The sidebar gets an "Example list" tab showing three rows. `mount`
|
|
3297
|
+
* returns at once; the SDK sends the mount with the values bound so far,
|
|
3298
|
+
* and the player rebuilds its main window with the panel. The Slint file
|
|
3299
|
+
* exports the component and the bridge global whose properties `bind`
|
|
3300
|
+
* writes.
|
|
3301
|
+
*
|
|
2867
3302
|
* {@includeCode ../../examples/panel-menu/main.ts#mount}
|
|
2868
3303
|
*
|
|
2869
3304
|
* `ui/panel.slint`:
|
|
@@ -2925,9 +3360,15 @@ export interface Panels {
|
|
|
2925
3360
|
* @throws The errors of {@link Panel.ready} (rejects `ready`) The player
|
|
2926
3361
|
* refused the mount.
|
|
2927
3362
|
*
|
|
2928
|
-
* @example
|
|
3363
|
+
* @example Mount a bottom bar with a seek slider
|
|
3364
|
+
* The plugin's own control bar appears under the video, its slider
|
|
3365
|
+
* following the playback position and seeking when dragged. Bindings and
|
|
3366
|
+
* handlers made right after `mount` are part of the first values;
|
|
3367
|
+
* `ready` resolves once the main window shows the bar, and rejects with
|
|
3368
|
+
* `conflict` when another plugin's panel holds the bottom bar.
|
|
2929
3369
|
* ```ts
|
|
2930
3370
|
* const bar = plugin.panels.mount("bottom", "ui/bar.slint");
|
|
3371
|
+
* // Sent again whenever the position changes.
|
|
2931
3372
|
* bar.bind(() => ({ position: plugin.player.state.position ?? 0 }));
|
|
2932
3373
|
* bar.on("seek", (seconds: number) => plugin.player.seek(seconds));
|
|
2933
3374
|
* await bar.ready;
|
|
@@ -2976,10 +3417,16 @@ export interface TimelineLayer extends Disposable {
|
|
|
2976
3417
|
* the player translates (`text("Chapter {}", [n])`, SDK 1.5.11); other
|
|
2977
3418
|
* plugins read the shown string.
|
|
2978
3419
|
*
|
|
2979
|
-
* @example
|
|
3420
|
+
* @example Show the file's bookmarks on the timeline
|
|
3421
|
+
* Bookmark marks appear on the seek bar at their times, titled with the
|
|
3422
|
+
* user's notes. `set` replaces this plugin's whole layer; with `ifCurrent`
|
|
3423
|
+
* the player drops the write if another file opened since `seen` was
|
|
3424
|
+
* read, so bookmarks of the previous file never land on the new one.
|
|
2980
3425
|
* ```ts
|
|
3426
|
+
* declare const bookmarks: { time: number; note: string }[];
|
|
3427
|
+
* // ---cut---
|
|
2981
3428
|
* const layer = plugin.timeline.layer();
|
|
2982
|
-
* const seen = plugin.player.state;
|
|
3429
|
+
* const seen = plugin.player.state; // the file the bookmarks belong to
|
|
2983
3430
|
* layer.set(
|
|
2984
3431
|
* bookmarks.map((b) => ({ time: b.time, kind: "bookmark", title: b.note })),
|
|
2985
3432
|
* { ifCurrent: seen },
|
|
@@ -3036,7 +3483,13 @@ export interface WindowState {
|
|
|
3036
3483
|
* `onChange`; without it, it updates only at start and after this plugin's
|
|
3037
3484
|
* `setVideoArea`.
|
|
3038
3485
|
*
|
|
3039
|
-
* @example
|
|
3486
|
+
* @example Resize the window to the video's own size
|
|
3487
|
+
* Screen ▸ Original size makes the video area as many logical points as
|
|
3488
|
+
* the video has pixels (one pixel per pixel at a display scale of 100 %).
|
|
3489
|
+
* `stats()` reports the video's size;
|
|
3490
|
+
* `setVideoArea` resizes the window around the video column (bars and a
|
|
3491
|
+
* docked sidebar added), scales it down if it would not fit the screen,
|
|
3492
|
+
* and is refused in fullscreen, hence the `enabled` check.
|
|
3040
3493
|
* ```ts
|
|
3041
3494
|
* plugin.commands.add({
|
|
3042
3495
|
* id: "size-1x",
|
|
@@ -3044,6 +3497,7 @@ export interface WindowState {
|
|
|
3044
3497
|
* enabled: () => !plugin.window.state.fullscreen,
|
|
3045
3498
|
* run: async () => {
|
|
3046
3499
|
* const stats = await plugin.player.stats();
|
|
3500
|
+
* // No video stream (an audio file): nothing to size to.
|
|
3047
3501
|
* if (stats.video?.width && stats.video.height)
|
|
3048
3502
|
* await plugin.window.setVideoArea({ width: stats.video.width, height: stats.video.height });
|
|
3049
3503
|
* },
|
|
@@ -3101,7 +3555,11 @@ export interface MainWindow {
|
|
|
3101
3555
|
* Where it is unsupported (Windows, Linux), `setVideoArea` plus
|
|
3102
3556
|
* `setAlwaysOnTop` make a reasonable "mini player" fallback.
|
|
3103
3557
|
*
|
|
3104
|
-
* @example
|
|
3558
|
+
* @example Toggle picture-in-picture where the system has it
|
|
3559
|
+
* The video moves into a floating picture-in-picture window, or back.
|
|
3560
|
+
* `state.pictureInPicture` says whether the system supports it and
|
|
3561
|
+
* whether it is on; the plugin only calls `setPictureInPicture` where it
|
|
3562
|
+
* is supported.
|
|
3105
3563
|
* ```ts
|
|
3106
3564
|
* if (plugin.window.state.pictureInPicture.supported)
|
|
3107
3565
|
* await plugin.window.setPictureInPicture(!plugin.window.state.pictureInPicture.active);
|
|
@@ -3129,7 +3587,12 @@ export interface AppInfo {
|
|
|
3129
3587
|
* `plugin.app`: the fixed facts plus the system, read on request. No
|
|
3130
3588
|
* capability.
|
|
3131
3589
|
*
|
|
3132
|
-
* @example
|
|
3590
|
+
* @example Describe the player and the system for a bug report
|
|
3591
|
+
* The plugin builds a line such as "Vestra 0.5.10 on macOS 26.2" to copy
|
|
3592
|
+
* into a bug report. `app.version` is known from the start; each
|
|
3593
|
+
* `system()` call asks the player, which reads the displays at that moment.
|
|
3594
|
+
* The operating system's name and version it works out once, at the first
|
|
3595
|
+
* call, and reuses them.
|
|
3133
3596
|
* ```ts
|
|
3134
3597
|
* const system = await plugin.app.system();
|
|
3135
3598
|
* const text = `Vestra ${plugin.app.version} on ${system.name} ${system.version ?? ""}`;
|
|
@@ -3228,18 +3691,28 @@ export interface ConfirmOptions {
|
|
|
3228
3691
|
* at a time: another call meanwhile rejects with `busy`. Playback continues
|
|
3229
3692
|
* while a dialog is open; command key bindings pause.
|
|
3230
3693
|
*
|
|
3694
|
+
* **Platforms:** {@link Dialogs.confirm} needs macOS or Windows (other
|
|
3695
|
+
* systems reject it with `unsupported`); the file dialogs work on every
|
|
3696
|
+
* system.
|
|
3697
|
+
*
|
|
3231
3698
|
* For anything more than a question or a path, use a {@link Windows | tool
|
|
3232
3699
|
* window}.
|
|
3233
3700
|
*
|
|
3234
3701
|
* @capability ui.dialog
|
|
3235
3702
|
*
|
|
3236
3703
|
* @example Ask before deleting, remembering "Don't ask again"
|
|
3704
|
+
* Deleting asks for confirmation in a system alert with a "Don't ask
|
|
3705
|
+
* again" box; once the user ticks it, later deletes go ahead without
|
|
3706
|
+
* asking. `confirm` resolves with the button and the checkbox when the
|
|
3707
|
+
* user answers; the texts are templates the player translates, so the
|
|
3708
|
+
* count goes in `args`. Remembering the box is up to the plugin.
|
|
3237
3709
|
* ```ts
|
|
3238
3710
|
* const store = plugin.storage.json("confirm", { defaults: { askBeforeDelete: true } });
|
|
3239
3711
|
* async function deleteAll(count: number) {
|
|
3240
3712
|
* if (store.value.askBeforeDelete) {
|
|
3241
3713
|
* const { confirmed, checked } = await plugin.dialogs.confirm({
|
|
3242
|
-
* message:
|
|
3714
|
+
* message: "Delete {} bookmarks?",
|
|
3715
|
+
* args: [count],
|
|
3243
3716
|
* detail: "This cannot be undone.",
|
|
3244
3717
|
* confirm: "Delete",
|
|
3245
3718
|
* destructive: true,
|
|
@@ -3286,13 +3759,22 @@ export interface Dialogs {
|
|
|
3286
3759
|
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
3287
3760
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3288
3761
|
*
|
|
3289
|
-
* @example
|
|
3762
|
+
* @example Confirm before replacing the playlist
|
|
3763
|
+
* The user sees "Replace the current playlist?" with a Replace button and
|
|
3764
|
+
* the playlist is replaced only after a yes. The system draws the alert
|
|
3765
|
+
* and the promise resolves when the user answers; only one dialog is
|
|
3766
|
+
* open at a time, so a second one meanwhile rejects with `busy`.
|
|
3290
3767
|
* ```ts
|
|
3291
|
-
*
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
-
*
|
|
3295
|
-
*
|
|
3768
|
+
* try {
|
|
3769
|
+
* const { confirmed } = await plugin.dialogs.confirm({
|
|
3770
|
+
* message: "Replace the current playlist?",
|
|
3771
|
+
* confirm: "Replace",
|
|
3772
|
+
* });
|
|
3773
|
+
* if (confirmed) await plugin.osd.show("Playlist replaced");
|
|
3774
|
+
* } catch (error) {
|
|
3775
|
+
* if ((error as PluginError).code !== "busy") throw error;
|
|
3776
|
+
* // Another dialog is already open: let the user finish that one.
|
|
3777
|
+
* }
|
|
3296
3778
|
* ```
|
|
3297
3779
|
*
|
|
3298
3780
|
* @since 1.0.0
|
|
@@ -3316,10 +3798,13 @@ export interface Dialogs {
|
|
|
3316
3798
|
* @throws `invalid` (rejects) `title` is longer than 256 bytes.
|
|
3317
3799
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3318
3800
|
*
|
|
3319
|
-
* @example
|
|
3801
|
+
* @example Let the user pick a subtitle file
|
|
3802
|
+
* The system's Open dialog appears titled "Choose a subtitle file" and
|
|
3803
|
+
* the chosen path is shown on the OSD. The promise resolves when the
|
|
3804
|
+
* user answers: with the absolute path, or `null` when they cancelled.
|
|
3320
3805
|
* ```ts
|
|
3321
3806
|
* const path = await plugin.dialogs.openFile({ title: "Choose a subtitle file" });
|
|
3322
|
-
* if (path !== null) await plugin.osd.show(
|
|
3807
|
+
* if (path !== null) await plugin.osd.show("Chosen: {}", [path]); // a path is data
|
|
3323
3808
|
* ```
|
|
3324
3809
|
*
|
|
3325
3810
|
* @since 1.0.0
|
|
@@ -3341,10 +3826,13 @@ export interface Dialogs {
|
|
|
3341
3826
|
* @throws `invalid` (rejects) `title` is longer than 256 bytes.
|
|
3342
3827
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3343
3828
|
*
|
|
3344
|
-
* @example
|
|
3829
|
+
* @example Let the user pick several files at once
|
|
3830
|
+
* The Open dialog allows selecting several files; the OSD says how many
|
|
3831
|
+
* were added. The promise resolves with the absolute paths, or with an
|
|
3832
|
+
* empty array when the user cancelled.
|
|
3345
3833
|
* ```ts
|
|
3346
3834
|
* const paths = await plugin.dialogs.openFiles({ title: "Add to the playlist" });
|
|
3347
|
-
* await plugin.osd.show(
|
|
3835
|
+
* if (paths.length) await plugin.osd.show("{} files added", [paths.length]);
|
|
3348
3836
|
* ```
|
|
3349
3837
|
*
|
|
3350
3838
|
* @since 1.0.0
|
|
@@ -3366,7 +3854,10 @@ export interface Dialogs {
|
|
|
3366
3854
|
* @throws `invalid` (rejects) `title` is longer than 256 bytes.
|
|
3367
3855
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3368
3856
|
*
|
|
3369
|
-
* @example
|
|
3857
|
+
* @example Let the user choose a folder for snapshots
|
|
3858
|
+
* The system's folder dialog appears titled "Save snapshots in". It
|
|
3859
|
+
* resolves with the folder's absolute path, or `null` when the user
|
|
3860
|
+
* cancelled, in which case the plugin stops there.
|
|
3370
3861
|
* ```ts
|
|
3371
3862
|
* const folder = await plugin.dialogs.openDirectory({ title: "Save snapshots in" });
|
|
3372
3863
|
* if (folder === null) return; // cancelled
|
|
@@ -3397,7 +3888,10 @@ export interface Dialogs {
|
|
|
3397
3888
|
* or contains `/` or `\`.
|
|
3398
3889
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3399
3890
|
*
|
|
3400
|
-
* @example
|
|
3891
|
+
* @example Ask where to save a snapshot
|
|
3892
|
+
* The system's Save dialog proposes "frame.png" and returns where the
|
|
3893
|
+
* user wants it. The dialog only returns the path (or `null` when
|
|
3894
|
+
* cancelled); writing the file is up to the plugin afterwards.
|
|
3401
3895
|
* ```ts
|
|
3402
3896
|
* const path = await plugin.dialogs.saveFile({ title: "Save snapshot", fileName: "frame.png" });
|
|
3403
3897
|
* if (path === null) return; // cancelled
|
|
@@ -3417,7 +3911,12 @@ export interface Dialogs {
|
|
|
3417
3911
|
fileName?: TextValue;
|
|
3418
3912
|
}): Promise<string | null>;
|
|
3419
3913
|
}
|
|
3420
|
-
/**
|
|
3914
|
+
/**
|
|
3915
|
+
* One audio track of the current media ({@link PlayerState.audioTracks}).
|
|
3916
|
+
*
|
|
3917
|
+
* @since 1.0.0
|
|
3918
|
+
* @category Playback
|
|
3919
|
+
*/
|
|
3421
3920
|
export interface AudioTrack {
|
|
3422
3921
|
/** The backend's track id; pass it to {@link Player.selectAudioTrack}. */
|
|
3423
3922
|
id: number;
|
|
@@ -3439,6 +3938,10 @@ export interface AudioTrack {
|
|
|
3439
3938
|
*
|
|
3440
3939
|
* Without `events`, {@link Player.state} only updates at start and after
|
|
3441
3940
|
* this plugin's own operations.
|
|
3941
|
+
*
|
|
3942
|
+
* @see {@link Player.state}, [Playback state, controls and sources](../../docs/concepts/playback.md)
|
|
3943
|
+
* @since 1.0.0
|
|
3944
|
+
* @category Playback
|
|
3442
3945
|
*/
|
|
3443
3946
|
export interface PlayerState {
|
|
3444
3947
|
/** Changes every time a source is opened, even the same file again. */
|
|
@@ -3516,14 +4019,24 @@ export interface PlayerState {
|
|
|
3516
4019
|
active: string | null;
|
|
3517
4020
|
};
|
|
3518
4021
|
}
|
|
3519
|
-
/**
|
|
4022
|
+
/**
|
|
4023
|
+
* An audio output device (`player.audioDevices`).
|
|
4024
|
+
*
|
|
4025
|
+
* @since 1.0.0
|
|
4026
|
+
* @category Playback
|
|
4027
|
+
*/
|
|
3520
4028
|
export interface AudioDevice {
|
|
3521
4029
|
/** Stable id to pass to `setAudioDevice` (e.g. `coreaudio/<UID>`). */
|
|
3522
4030
|
id: string;
|
|
3523
4031
|
/** What the system calls it. */
|
|
3524
4032
|
name: string;
|
|
3525
4033
|
}
|
|
3526
|
-
/**
|
|
4034
|
+
/**
|
|
4035
|
+
* One chapter of the current media.
|
|
4036
|
+
*
|
|
4037
|
+
* @since 1.0.0
|
|
4038
|
+
* @category Playback
|
|
4039
|
+
*/
|
|
3527
4040
|
export interface Chapter {
|
|
3528
4041
|
/** Chapter title, `null` without one. */
|
|
3529
4042
|
title: string | null;
|
|
@@ -3540,17 +4053,59 @@ export interface Chapter {
|
|
|
3540
4053
|
* consecutive calls do not make each other stale. {@link Player.open} is
|
|
3541
4054
|
* unconditional unless `ifCurrent` is given.
|
|
3542
4055
|
*
|
|
3543
|
-
* @example
|
|
4056
|
+
* @example Play the next file only if nothing else was opened
|
|
4057
|
+
* When the plugin's file ends, the next one starts, unless the user or
|
|
4058
|
+
* another plugin opened something in the meantime. `onEnd` passes the
|
|
4059
|
+
* state at the end; `open` with `ifCurrent: ended` is refused with `stale`
|
|
4060
|
+
* when the source changed after that state, so nothing is replaced.
|
|
3544
4061
|
* ```ts
|
|
3545
|
-
*
|
|
3546
|
-
*
|
|
4062
|
+
* declare const media: Media;
|
|
4063
|
+
* declare const next: string;
|
|
4064
|
+
* // ---cut---
|
|
4065
|
+
* media.onEnd(async (ended) => {
|
|
4066
|
+
* try {
|
|
4067
|
+
* await plugin.player.open(next, { ifCurrent: ended });
|
|
4068
|
+
* } catch (error) {
|
|
4069
|
+
* // Someone opened another file: leave it playing.
|
|
4070
|
+
* if ((error as PluginError).code !== "stale") throw error;
|
|
4071
|
+
* }
|
|
4072
|
+
* });
|
|
3547
4073
|
* ```
|
|
4074
|
+
*
|
|
4075
|
+
* @see [Playback state, controls and sources](../../docs/concepts/playback.md)
|
|
4076
|
+
* @since 1.0.0
|
|
4077
|
+
* @category Playback
|
|
3548
4078
|
*/
|
|
3549
4079
|
export interface Conditional {
|
|
3550
4080
|
/** The state the call is conditional on; default: the current `player.state`. */
|
|
3551
4081
|
ifCurrent?: PlayerState;
|
|
3552
4082
|
}
|
|
3553
|
-
/**
|
|
4083
|
+
/**
|
|
4084
|
+
* A source this plugin opened.
|
|
4085
|
+
*
|
|
4086
|
+
* @remarks
|
|
4087
|
+
* A source this plugin opened with {@link Player.open}. It does not own
|
|
4088
|
+
* the source: the user or another plugin can replace it at any time, and
|
|
4089
|
+
* `current` then turns false. `ready` and `onEnd` follow the player state:
|
|
4090
|
+
* with `events` they react to the player's events, without it `ready`
|
|
4091
|
+
* polls the state every 100 ms (which needs `playback.read`). Disposing the
|
|
4092
|
+
* scope that opened it does not close the media; call `close` for that.
|
|
4093
|
+
*
|
|
4094
|
+
* @capability That of {@link Player.open} (`playback.control`); `ready` and
|
|
4095
|
+
* `onEnd` follow the state through `events` and `playback.read`.
|
|
4096
|
+
*
|
|
4097
|
+
* @example Open a file and say when it ends
|
|
4098
|
+
* The file opens and plays, and the OSD says "Finished" when it reaches its
|
|
4099
|
+
* end on its own. The handle belongs to this source: `onEnd` fires only
|
|
4100
|
+
* for a natural end of this media, not when someone opens another file.
|
|
4101
|
+
* ```ts
|
|
4102
|
+
* const media = await plugin.player.open("/Movies/clip.mp4");
|
|
4103
|
+
* media.onEnd(() => void plugin.osd.show("Finished")); // natural ends only
|
|
4104
|
+
* ```
|
|
4105
|
+
*
|
|
4106
|
+
* @since 1.0.0
|
|
4107
|
+
* @category Playback
|
|
4108
|
+
*/
|
|
3554
4109
|
export interface Media {
|
|
3555
4110
|
/** Still the player's source (a restart keeps it current). */
|
|
3556
4111
|
readonly current: boolean;
|
|
@@ -3558,13 +4113,77 @@ export interface Media {
|
|
|
3558
4113
|
* Resolves when it is playing. Rejects with code `failed` (load error),
|
|
3559
4114
|
* `replaced` (another source took over) or `ended`; with `AbortError` when
|
|
3560
4115
|
* the signal aborts, after closing the media.
|
|
4116
|
+
*
|
|
4117
|
+
* @capability That of {@link Player.open}; `playback.read` or `events` to
|
|
4118
|
+
* see the state change.
|
|
4119
|
+
* @throws `failed` (rejects) The media could not be loaded.
|
|
4120
|
+
* @throws `replaced` (rejects) Another source took over first.
|
|
4121
|
+
* @throws `ended` (rejects) The media already played to the end.
|
|
4122
|
+
* @throws `aborted` (rejects) `signal` aborted (an `AbortError`); the media
|
|
4123
|
+
* is closed.
|
|
4124
|
+
*
|
|
4125
|
+
* @example Wait at most ten seconds for a stream to start
|
|
4126
|
+
* A network stream gets ten seconds to start playing; otherwise it is
|
|
4127
|
+
* closed and the user is told. Aborting the signal makes `ready` reject
|
|
4128
|
+
* with an `AbortError` after closing the media.
|
|
4129
|
+
* ```ts
|
|
4130
|
+
* const media = await plugin.player.open("https://example.com/live.m3u8");
|
|
4131
|
+
* // The timeout's abort closes the media and rejects with an AbortError.
|
|
4132
|
+
* try {
|
|
4133
|
+
* await media.ready({ signal: AbortSignal.timeout(10_000) });
|
|
4134
|
+
* } catch (error) {
|
|
4135
|
+
* await plugin.osd.show("Not playing: {}", [(error as Error).message]);
|
|
4136
|
+
* }
|
|
4137
|
+
* ```
|
|
4138
|
+
*
|
|
4139
|
+
* @since 1.0.0
|
|
3561
4140
|
*/
|
|
3562
4141
|
ready(options?: {
|
|
3563
4142
|
signal?: AbortSignal;
|
|
3564
4143
|
}): Promise<void>;
|
|
3565
|
-
/**
|
|
4144
|
+
/**
|
|
4145
|
+
* Called once each time a playthrough of this media ends naturally.
|
|
4146
|
+
*
|
|
4147
|
+
* @capability That of {@link Player.open}; `events` to hear the end.
|
|
4148
|
+
*
|
|
4149
|
+
* @example Play the next file when this one ends
|
|
4150
|
+
* When the plugin's file ends naturally, the next one opens. The listener
|
|
4151
|
+
* gets the state at the end; passing it as `ifCurrent` makes the next
|
|
4152
|
+
* `open` fail with `stale` (and do nothing) if the user opened something
|
|
4153
|
+
* else in the meantime.
|
|
4154
|
+
* ```ts
|
|
4155
|
+
* declare const media: Media;
|
|
4156
|
+
* declare const next: string;
|
|
4157
|
+
* // ---cut---
|
|
4158
|
+
* media.onEnd((ended) =>
|
|
4159
|
+
* plugin.player.open(next, { ifCurrent: ended }).catch((error: PluginError) => {
|
|
4160
|
+
* if (error.code !== "stale") throw error; // the user opened something else
|
|
4161
|
+
* }),
|
|
4162
|
+
* );
|
|
4163
|
+
* ```
|
|
4164
|
+
*
|
|
4165
|
+
* @since 1.0.0
|
|
4166
|
+
*/
|
|
3566
4167
|
onEnd(listener: (state: PlayerState) => void): Disposable;
|
|
3567
|
-
/**
|
|
4168
|
+
/**
|
|
4169
|
+
* Close it if it is still the player's source; otherwise do nothing.
|
|
4170
|
+
*
|
|
4171
|
+
* @capability That of {@link Player.open} (`playback.control`).
|
|
4172
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4173
|
+
* `playback.control`.
|
|
4174
|
+
*
|
|
4175
|
+
* @example Close the preview the plugin opened
|
|
4176
|
+
* The plugin closes the preview it opened, but leaves alone anything the
|
|
4177
|
+
* user opened since. `close` acts only while this media is still the
|
|
4178
|
+
* player's source and does nothing otherwise.
|
|
4179
|
+
* ```ts
|
|
4180
|
+
* declare const preview: Media;
|
|
4181
|
+
* // ---cut---
|
|
4182
|
+
* await preview.close(); // a no-op when something else is playing now
|
|
4183
|
+
* ```
|
|
4184
|
+
*
|
|
4185
|
+
* @since 1.0.0
|
|
4186
|
+
*/
|
|
3568
4187
|
close(): Promise<void>;
|
|
3569
4188
|
}
|
|
3570
4189
|
/**
|
|
@@ -3572,13 +4191,33 @@ export interface Media {
|
|
|
3572
4191
|
* Any outside change (another plugin opens a file, the user pauses) ends
|
|
3573
4192
|
* the gesture.
|
|
3574
4193
|
*
|
|
3575
|
-
* @example
|
|
4194
|
+
* @example Seek while the user drags a slider
|
|
4195
|
+
* The video follows the slider while it is dragged. `scrub()` starts a
|
|
4196
|
+
* gesture from the current state; each `seek` is conditional on the
|
|
4197
|
+
* previous one and only the newest target is sent while one is running,
|
|
4198
|
+
* so a fast drag does not queue seeks. A change from outside (another file,
|
|
4199
|
+
* the user pausing) ends the gesture and later seeks resolve `false`.
|
|
3576
4200
|
* ```ts
|
|
4201
|
+
* declare const bar: Panel;
|
|
4202
|
+
* // ---cut---
|
|
3577
4203
|
* let scrub: Scrub | null = null;
|
|
3578
4204
|
* bar.on("dragStart", () => (scrub = plugin.player.scrub()));
|
|
4205
|
+
* // Latest value wins: superseded targets resolve false, not an error.
|
|
3579
4206
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds));
|
|
3580
4207
|
* bar.on("dragEnd", () => scrub?.end());
|
|
3581
4208
|
* ```
|
|
4209
|
+
*
|
|
4210
|
+
* @remarks
|
|
4211
|
+
* A drag gesture on a seek slider. Each `seek` is conditional on the
|
|
4212
|
+
* previous one and only the newest target is sent while a seek is
|
|
4213
|
+
* running, so a fast drag never queues seeks. Any outside change (another
|
|
4214
|
+
* file, the user pausing) ends the gesture: `active` turns false and later
|
|
4215
|
+
* seeks resolve `false`.
|
|
4216
|
+
*
|
|
4217
|
+
* @capability playback.control
|
|
4218
|
+
* @see [Keep a slider responsive while dragging](../../docs/how-to/slider.md)
|
|
4219
|
+
* @since 1.0.0
|
|
4220
|
+
* @category Playback
|
|
3582
4221
|
*/
|
|
3583
4222
|
export interface Scrub {
|
|
3584
4223
|
/**
|
|
@@ -3599,18 +4238,52 @@ export interface Scrub {
|
|
|
3599
4238
|
* as before. Older players ignore the option and seek exactly: set
|
|
3600
4239
|
* `"minPlayerVersion": "0.5.3"` when you rely on it.
|
|
3601
4240
|
*
|
|
3602
|
-
* @example
|
|
4241
|
+
* @example Scrub on keyframes for a fast picture
|
|
4242
|
+
* While dragging, the picture jumps to the nearest earlier keyframe at
|
|
4243
|
+
* once instead of decoding up to the exact frame. With `keyframes: true`
|
|
4244
|
+
* the player lands each target on the last keyframe at or before it; the
|
|
4245
|
+
* plugin follows the user's keyframe-seek setting.
|
|
3603
4246
|
* ```ts
|
|
4247
|
+
* declare const bar: Panel;
|
|
4248
|
+
* declare const scrub: Scrub | null;
|
|
4249
|
+
* // ---cut---
|
|
3604
4250
|
* const keyframes = true; // the plugin's own setting
|
|
3605
4251
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
|
|
3606
4252
|
* ```
|
|
4253
|
+
*
|
|
4254
|
+
* @capability playback.control
|
|
4255
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4256
|
+
* `playback.control`.
|
|
4257
|
+
* @throws `stale` (rejects) The source changed from outside while this
|
|
4258
|
+
* seek was being sent (a superseded target or an ended gesture resolves
|
|
4259
|
+
* `false` instead).
|
|
4260
|
+
* @throws `unsupported` (rejects) The source is a generated stream.
|
|
4261
|
+
* @throws `backend` (rejects) The player failed the seek.
|
|
4262
|
+
* @since 1.0.0
|
|
3607
4263
|
*/
|
|
3608
4264
|
seek(seconds: number, options?: {
|
|
3609
4265
|
keyframes?: boolean;
|
|
3610
4266
|
}): Promise<boolean>;
|
|
3611
4267
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
3612
4268
|
readonly active: boolean;
|
|
3613
|
-
/**
|
|
4269
|
+
/**
|
|
4270
|
+
* End the gesture; later seeks resolve `false`.
|
|
4271
|
+
*
|
|
4272
|
+
* @capability playback.control
|
|
4273
|
+
*
|
|
4274
|
+
* @example End the gesture when the user lets go
|
|
4275
|
+
* Releasing the slider ends the gesture, so a late `dragMove` cannot move
|
|
4276
|
+
* playback any more. After `end`, `seek` resolves `false` without doing
|
|
4277
|
+
* anything.
|
|
4278
|
+
* ```ts
|
|
4279
|
+
* declare const bar: Panel;
|
|
4280
|
+
* declare const scrub: Scrub;
|
|
4281
|
+
* // ---cut---
|
|
4282
|
+
* bar.on("dragEnd", () => scrub.end()); // later seeks resolve false
|
|
4283
|
+
* ```
|
|
4284
|
+
*
|
|
4285
|
+
* @since 1.0.0
|
|
4286
|
+
*/
|
|
3614
4287
|
end(): void;
|
|
3615
4288
|
}
|
|
3616
4289
|
/**
|
|
@@ -3624,6 +4297,9 @@ export interface Scrub {
|
|
|
3624
4297
|
*
|
|
3625
4298
|
* The SDK copies the frame once before `frame()` is called again, so you
|
|
3626
4299
|
* may reuse the same buffers for every frame.
|
|
4300
|
+
*
|
|
4301
|
+
* @since 1.0.0
|
|
4302
|
+
* @category Playback
|
|
3627
4303
|
*/
|
|
3628
4304
|
export interface StreamFrame {
|
|
3629
4305
|
/** `width × height × 4` bytes of RGBA, at most 4 MiB. */
|
|
@@ -3635,7 +4311,13 @@ export interface StreamFrame {
|
|
|
3635
4311
|
*/
|
|
3636
4312
|
audio: Float32Array;
|
|
3637
4313
|
}
|
|
3638
|
-
/**
|
|
4314
|
+
/**
|
|
4315
|
+
* Options of {@link Player.openStream}.
|
|
4316
|
+
*
|
|
4317
|
+
* @see {@link Player.openStream}
|
|
4318
|
+
* @since 1.0.0
|
|
4319
|
+
* @category Playback
|
|
4320
|
+
*/
|
|
3639
4321
|
export interface StreamOptions {
|
|
3640
4322
|
/** Frame width in pixels. */
|
|
3641
4323
|
width: number;
|
|
@@ -3650,12 +4332,48 @@ export interface StreamOptions {
|
|
|
3650
4332
|
* `openStream` start-up timeouts: code `timeout` with the step that ran out
|
|
3651
4333
|
* of time, the first buffers (`prefill`) or the host's activation.
|
|
3652
4334
|
* Both limits are 10 seconds.
|
|
4335
|
+
*
|
|
4336
|
+
* @since 1.0.0
|
|
4337
|
+
* @category Playback
|
|
3653
4338
|
*/
|
|
3654
4339
|
export type StreamStartError = PluginError & {
|
|
3655
4340
|
/** The start-up step that timed out. */
|
|
3656
4341
|
stage: "prefill" | "activation";
|
|
3657
4342
|
};
|
|
3658
|
-
/**
|
|
4343
|
+
/**
|
|
4344
|
+
* A generated source, from {@link Player.openStream}. Disposing it stops it.
|
|
4345
|
+
*
|
|
4346
|
+
* @remarks
|
|
4347
|
+
* A source whose frames this plugin generates, from
|
|
4348
|
+
* {@link Player.openStream}. The SDK pulls frames on the audio clock over
|
|
4349
|
+
* a local connection and handles prefill, activation and pausing; the plugin
|
|
4350
|
+
* only supplies `frame()`. A stream ends when the plugin stops it, when
|
|
4351
|
+
* another source replaces it or the user closes it (`onStop` with `null`),
|
|
4352
|
+
* or when it fails (`onStop` with the error).
|
|
4353
|
+
*
|
|
4354
|
+
* @capability producer.av and playback.control
|
|
4355
|
+
*
|
|
4356
|
+
* @example Stop the stream from a menu item
|
|
4357
|
+
* A menu item ends the plugin's generated stream and closes it as the
|
|
4358
|
+
* player's source. `stop` closes the source first and then the frame
|
|
4359
|
+
* connection, so the user never sees a playback error; `onStop` is not
|
|
4360
|
+
* called for the plugin's own stop.
|
|
4361
|
+
* ```ts
|
|
4362
|
+
* declare const stream: Stream;
|
|
4363
|
+
* // ---cut---
|
|
4364
|
+
* // Enabled while the stream produces frames.
|
|
4365
|
+
* plugin.commands.add({
|
|
4366
|
+
* id: "stop-generator",
|
|
4367
|
+
* path: ["@playback", "Stop generator"],
|
|
4368
|
+
* enabled: () => stream.running,
|
|
4369
|
+
* run: () => stream.stop(),
|
|
4370
|
+
* });
|
|
4371
|
+
* ```
|
|
4372
|
+
*
|
|
4373
|
+
* @see {@link Player.openStream}
|
|
4374
|
+
* @since 1.0.0
|
|
4375
|
+
* @category Playback
|
|
4376
|
+
*/
|
|
3659
4377
|
export interface Stream extends Disposable {
|
|
3660
4378
|
/** The stream is producing frames. */
|
|
3661
4379
|
readonly running: boolean;
|
|
@@ -3671,6 +4389,23 @@ export interface Stream extends Disposable {
|
|
|
3671
4389
|
* (before 1.5.7 a replaced or closed stream could get `unavailable`
|
|
3672
4390
|
* "producer absent"). Without `playback.read` a failure the SDK has not
|
|
3673
4391
|
* seen yet is reported as null.
|
|
4392
|
+
*
|
|
4393
|
+
* @capability producer.av
|
|
4394
|
+
*
|
|
4395
|
+
* @example Tell the user why the stream stopped
|
|
4396
|
+
* The OSD explains a failure, and nothing is said when the user simply
|
|
4397
|
+
* opened another file. The listener runs only for stops from outside the
|
|
4398
|
+
* plugin: `null` for a replacement or the user's close, an error (such as
|
|
4399
|
+
* `stream` with the player's text) for a failure.
|
|
4400
|
+
* ```ts
|
|
4401
|
+
* declare const stream: Stream;
|
|
4402
|
+
* // ---cut---
|
|
4403
|
+
* stream.onStop((error) => {
|
|
4404
|
+
* if (error) void plugin.osd.show("Generator stopped: {}", [error.message]);
|
|
4405
|
+
* });
|
|
4406
|
+
* ```
|
|
4407
|
+
*
|
|
4408
|
+
* @since 1.0.0
|
|
3674
4409
|
*/
|
|
3675
4410
|
onStop(listener: (error: PluginError | null) => void): Disposable;
|
|
3676
4411
|
/**
|
|
@@ -3678,10 +4413,34 @@ export interface Stream extends Disposable {
|
|
|
3678
4413
|
* The source closes first, then the frame connection, then the producer,
|
|
3679
4414
|
* so stopping never shows as a playback failure. `dispose()` and
|
|
3680
4415
|
* deactivation do the same.
|
|
4416
|
+
*
|
|
4417
|
+
* @capability producer.av and playback.control
|
|
4418
|
+
* @throws Whatever code closing the player's source failed with (the
|
|
4419
|
+
* frame connection and the producer are released either way). A
|
|
4420
|
+
* stream that is no longer the source just resolves.
|
|
4421
|
+
*
|
|
4422
|
+
* @example Stop the stream when the plugin's window closes
|
|
4423
|
+
* Closing the generator window stops the stream it feeds. `stop` closes
|
|
4424
|
+
* the player's source if it is still this stream and then the frame
|
|
4425
|
+
* connection; it resolves once both are closed.
|
|
4426
|
+
* ```ts
|
|
4427
|
+
* declare const stream: Stream;
|
|
4428
|
+
* declare const window: Window;
|
|
4429
|
+
* // ---cut---
|
|
4430
|
+
* window.onClose(() => stream.stop());
|
|
4431
|
+
* ```
|
|
4432
|
+
*
|
|
4433
|
+
* @since 1.0.0
|
|
3681
4434
|
*/
|
|
3682
4435
|
stop(): Promise<void>;
|
|
3683
4436
|
}
|
|
3684
|
-
/**
|
|
4437
|
+
/**
|
|
4438
|
+
* The plugin that owns "next / previous" (a playlist).
|
|
4439
|
+
*
|
|
4440
|
+
* @see {@link Player.provideSequence}
|
|
4441
|
+
* @since 1.0.0
|
|
4442
|
+
* @category Playback
|
|
4443
|
+
*/
|
|
3685
4444
|
export interface SequenceProvider {
|
|
3686
4445
|
/** Go to the next item (called in the provider's scope). */
|
|
3687
4446
|
next(): unknown;
|
|
@@ -3695,6 +4454,10 @@ export interface SequenceProvider {
|
|
|
3695
4454
|
/**
|
|
3696
4455
|
* What {@link Player.stats} reports. Every field is the backend's own value
|
|
3697
4456
|
* (libmpv properties), `null` when it has none; nothing is converted.
|
|
4457
|
+
*
|
|
4458
|
+
* @see {@link Player.stats}
|
|
4459
|
+
* @since 1.0.0
|
|
4460
|
+
* @category Playback
|
|
3698
4461
|
*/
|
|
3699
4462
|
export interface PlaybackStats {
|
|
3700
4463
|
/**
|
|
@@ -3796,23 +4559,103 @@ export interface PlaybackStats {
|
|
|
3796
4559
|
* `stale` and do nothing when the source changed since the state you saw.
|
|
3797
4560
|
*
|
|
3798
4561
|
* @example Open a file and wait until it plays
|
|
4562
|
+
* The file opens and the plugin learns whether it started playing within
|
|
4563
|
+
* ten seconds. `open` resolves once the player accepted the load and
|
|
4564
|
+
* returns a `Media` handle; `ready` waits for playback and rejects with
|
|
4565
|
+
* `failed` (load error), `replaced` (another source took over) or an
|
|
4566
|
+
* `AbortError` when the timeout fires, which closes the media.
|
|
3799
4567
|
* ```ts
|
|
3800
|
-
* const media = await plugin.player.open("/Movies/clip.mp4");
|
|
4568
|
+
* const media = await plugin.player.open("/Movies/clip.mp4"); // load accepted
|
|
3801
4569
|
* try {
|
|
3802
4570
|
* await media.ready({ signal: AbortSignal.timeout(10_000) });
|
|
3803
4571
|
* } catch (error) {
|
|
3804
|
-
* void plugin.osd.show(
|
|
4572
|
+
* void plugin.osd.show("Cannot play: {}", [(error as Error).message]);
|
|
3805
4573
|
* }
|
|
3806
4574
|
* ```
|
|
4575
|
+
*
|
|
4576
|
+
* @remarks
|
|
4577
|
+
* `plugin.player` is the player's one source: what is open, its phase and
|
|
4578
|
+
* position, and the controls. `state` is a snapshot the SDK keeps current
|
|
4579
|
+
* from the player's events (with `events`) and refreshes after this
|
|
4580
|
+
* plugin's own calls; `onChange` runs after it changed. Every control is
|
|
4581
|
+
* conditional: it acts on the state the plugin last saw and fails with
|
|
4582
|
+
* `stale` when the source changed meanwhile: another file, a close, or any
|
|
4583
|
+
* pause, seek or speed change by the user or another plugin (see
|
|
4584
|
+
* {@link Conditional}). The player keeps the source when a plugin stops;
|
|
4585
|
+
* only the plugin's own handles (a stream, its sequence provider, its
|
|
4586
|
+
* output device) are withdrawn.
|
|
4587
|
+
*
|
|
4588
|
+
* @capability `playback.read` to read; `playback.control` to control;
|
|
4589
|
+
* `events` to follow changes made elsewhere; `producer.av` for
|
|
4590
|
+
* {@link Player.openStream}.
|
|
4591
|
+
*
|
|
4592
|
+
* @see [Playback state, controls and sources](../../docs/concepts/playback.md), [Control playback](../../docs/how-to/control-playback.md)
|
|
4593
|
+
* @since 1.0.0
|
|
4594
|
+
* @category Playback
|
|
3807
4595
|
*/
|
|
3808
4596
|
export interface Player {
|
|
3809
4597
|
/** Latest state; always newer than anything delivered before. */
|
|
3810
4598
|
readonly state: PlayerState;
|
|
3811
|
-
/**
|
|
4599
|
+
/**
|
|
4600
|
+
* The state changed (needs `events` for outside changes).
|
|
4601
|
+
*
|
|
4602
|
+
* @capability events (for changes made elsewhere; without it the listener
|
|
4603
|
+
* runs only after this plugin's own calls)
|
|
4604
|
+
*
|
|
4605
|
+
* @example Show the position while it plays
|
|
4606
|
+
* The OSD block shows the position, updated as playback moves. The SDK
|
|
4607
|
+
* updates `player.state` from the player's events and then calls the
|
|
4608
|
+
* listener with the new snapshot; the handler only reads it.
|
|
4609
|
+
* ```ts
|
|
4610
|
+
* const info = plugin.osd.panel();
|
|
4611
|
+
* plugin.player.onChange((state) => {
|
|
4612
|
+
* // `position` is null while nothing is open.
|
|
4613
|
+
* info.set(state.position === null ? null : "{} s", [state.position?.toFixed(0) ?? ""]);
|
|
4614
|
+
* });
|
|
4615
|
+
* ```
|
|
4616
|
+
*
|
|
4617
|
+
* @since 1.0.0
|
|
4618
|
+
*/
|
|
3812
4619
|
onChange(listener: (state: PlayerState) => void): Disposable;
|
|
3813
|
-
/**
|
|
4620
|
+
/**
|
|
4621
|
+
* Read the state from the player now (`playback.read`).
|
|
4622
|
+
*
|
|
4623
|
+
* @capability playback.read
|
|
4624
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4625
|
+
* `playback.read`.
|
|
4626
|
+
*
|
|
4627
|
+
* @example Read the state before deciding
|
|
4628
|
+
* A command reads the newest state from the player before acting, even
|
|
4629
|
+
* without `events`. `refresh` asks the player and resolves with the new
|
|
4630
|
+
* snapshot; `player.state` holds it afterwards.
|
|
4631
|
+
* ```ts
|
|
4632
|
+
* const state = await plugin.player.refresh();
|
|
4633
|
+
* if (state.phase === "ready") await plugin.osd.show("{} s left", [((state.duration ?? 0) - (state.position ?? 0)).toFixed(0)]);
|
|
4634
|
+
* ```
|
|
4635
|
+
*
|
|
4636
|
+
* @since 1.0.0
|
|
4637
|
+
*/
|
|
3814
4638
|
refresh(): Promise<PlayerState>;
|
|
3815
|
-
/**
|
|
4639
|
+
/**
|
|
4640
|
+
* True if nothing changed the source since `state` was observed.
|
|
4641
|
+
*
|
|
4642
|
+
* @capability None.
|
|
4643
|
+
*
|
|
4644
|
+
* @example Drop a result that arrives after the file changed
|
|
4645
|
+
* A slow lookup for the current file is shown only if that file is still
|
|
4646
|
+
* open. `isCurrent` compares the snapshot taken before the work with the
|
|
4647
|
+
* SDK's newest state; it is false once the source changed in any way:
|
|
4648
|
+
* another file, a close, or a pause, seek or speed change by anyone.
|
|
4649
|
+
* ```ts
|
|
4650
|
+
* declare function lookUpTitle(path: string): Promise<string>;
|
|
4651
|
+
* // ---cut---
|
|
4652
|
+
* const seen = plugin.player.state;
|
|
4653
|
+
* const title = await lookUpTitle(seen.source ?? "");
|
|
4654
|
+
* if (plugin.player.isCurrent(seen)) await plugin.osd.show("{}", [title]); // still the same file
|
|
4655
|
+
* ```
|
|
4656
|
+
*
|
|
4657
|
+
* @since 1.0.0
|
|
4658
|
+
*/
|
|
3816
4659
|
isCurrent(state: PlayerState): boolean;
|
|
3817
4660
|
/**
|
|
3818
4661
|
* Open a path or URL, replacing the current source (unless `ifCurrent` no
|
|
@@ -3823,6 +4666,37 @@ export interface Player {
|
|
|
3823
4666
|
* {@link Resolver} are resolved first, and so are local files of an
|
|
3824
4667
|
* extension a file resolver claims (the source is then a stream that
|
|
3825
4668
|
* plugin generates; `paused` reaches it).
|
|
4669
|
+
*
|
|
4670
|
+
* @capability playback.control
|
|
4671
|
+
* @throws `stale` (rejects) `ifCurrent` no longer holds; nothing was opened.
|
|
4672
|
+
* @throws `replaced` (rejects) Accepted, then another source took over
|
|
4673
|
+
* before it finished (for example while a URL was still being resolved).
|
|
4674
|
+
* @throws `invalid` (rejects) An empty `source`.
|
|
4675
|
+
* @throws `aborted` (rejects) `signal` aborted (an `AbortError`); the
|
|
4676
|
+
* source is closed if it was opened.
|
|
4677
|
+
* @throws `busy` (rejects) The address needs a resolver plugin and the
|
|
4678
|
+
* player is already resolving 32 addresses.
|
|
4679
|
+
* @throws `backend` (rejects) A resolver plugin handled the address or
|
|
4680
|
+
* file and failed (its message), or the resolution was cancelled or
|
|
4681
|
+
* expired.
|
|
4682
|
+
* @throws `unsupported` (rejects) The player refused what a resolver
|
|
4683
|
+
* returned (for example split streams with different headers).
|
|
4684
|
+
* @throws `unavailable` (rejects) The stream a file resolver prepared was
|
|
4685
|
+
* not ready to play.
|
|
4686
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4687
|
+
* `playback.control`.
|
|
4688
|
+
*
|
|
4689
|
+
* @example Open a file paused
|
|
4690
|
+
* The file opens at its start without playing. `open` resolves once the
|
|
4691
|
+
* player accepted the load and returns a {@link Media} handle for that
|
|
4692
|
+
* source; `ready` then waits until it can play (here: until it is loaded
|
|
4693
|
+
* and paused).
|
|
4694
|
+
* ```ts
|
|
4695
|
+
* const media = await plugin.player.open("/Movies/clip.mp4", { paused: true });
|
|
4696
|
+
* await media.ready(); // rejects `failed` when the file cannot be loaded
|
|
4697
|
+
* ```
|
|
4698
|
+
*
|
|
4699
|
+
* @since 1.0.0
|
|
3826
4700
|
*/
|
|
3827
4701
|
open(source: string, options?: {
|
|
3828
4702
|
paused?: boolean;
|
|
@@ -3840,74 +4714,395 @@ export interface Player {
|
|
|
3840
4714
|
* (`timeout`), `stream`, `device`, or `replaced` when another source
|
|
3841
4715
|
* replaced it (or it was closed) before it ran.
|
|
3842
4716
|
*
|
|
3843
|
-
* @example
|
|
4717
|
+
* @example Play pictures the plugin draws itself
|
|
4718
|
+
* The player shows a 320×240 picture the plugin renders, with silence as
|
|
4719
|
+
* its sound, as the current source. The SDK pulls frames on the audio
|
|
4720
|
+
* clock: each `frame()` returns one picture (RGBA bytes) and the audio
|
|
4721
|
+
* that plays with it, here 1/60 s, so the picture runs at 60 frames a
|
|
4722
|
+
* second; `frame()` may run several times back to back to refill the
|
|
4723
|
+
* lead. `onStop` tells the plugin when and why the stream ended.
|
|
3844
4724
|
* ```ts
|
|
3845
|
-
*
|
|
3846
|
-
*
|
|
4725
|
+
* declare function render(into: Uint8Array): Uint8Array;
|
|
4726
|
+
* // ---cut---
|
|
4727
|
+
* const frame = new Uint8Array(320 * 240 * 4); // RGBA, reused per frame
|
|
4728
|
+
* const silence = new Float32Array(2 * 800); // 1/60 s of stereo at 48 kHz
|
|
3847
4729
|
* const stream = await plugin.player.openStream({
|
|
3848
4730
|
* width: 320,
|
|
3849
4731
|
* height: 240,
|
|
3850
4732
|
* frame: () => ({ video: render(frame), audio: silence }),
|
|
3851
4733
|
* });
|
|
4734
|
+
* // `error` is null when the stream stopped normally.
|
|
3852
4735
|
* stream.onStop((error) => error && plugin.osd.show(error.message));
|
|
3853
4736
|
* ```
|
|
4737
|
+
*
|
|
4738
|
+
* @capability producer.av and playback.control
|
|
4739
|
+
* @throws `timeout` (rejects) A {@link StreamStartError}: the stream did not
|
|
4740
|
+
* start in time.
|
|
4741
|
+
* @throws `stream` (rejects) The player failed to start the stream.
|
|
4742
|
+
* @throws `device` (rejects) The audio device was not ready.
|
|
4743
|
+
* @throws `replaced` (rejects) Another source replaced it, or it was closed,
|
|
4744
|
+
* before it ran.
|
|
4745
|
+
* @throws `invalid` (rejects) Bad options (a size or a frame function).
|
|
4746
|
+
* @throws `aborted` (rejects) `signal` aborted, or the scope was disposed,
|
|
4747
|
+
* while the stream was starting (an `AbortError`).
|
|
4748
|
+
* @since 1.0.0
|
|
3854
4749
|
*/
|
|
3855
4750
|
openStream(options: StreamOptions): Promise<Stream>;
|
|
3856
|
-
/**
|
|
4751
|
+
/**
|
|
4752
|
+
* Pause or resume.
|
|
4753
|
+
*
|
|
4754
|
+
* @capability playback.control
|
|
4755
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
4756
|
+
* based on (another file, a close, or any pause, seek
|
|
4757
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
4758
|
+
* @throws `invalid` (rejects) Nothing is open.
|
|
4759
|
+
* @throws `busy` (rejects) A generated stream is still handling a previous
|
|
4760
|
+
* control.
|
|
4761
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4762
|
+
* `playback.control`.
|
|
4763
|
+
*
|
|
4764
|
+
* @example Pause, but only the file the user was watching
|
|
4765
|
+
* A "pause on lock" helper pauses the current file and does nothing when
|
|
4766
|
+
* another file was opened in between. The call is conditional on
|
|
4767
|
+
* `player.state` by default; `stale` means the source changed and the
|
|
4768
|
+
* pause was not applied, which is the intended outcome here.
|
|
4769
|
+
* ```ts
|
|
4770
|
+
* try {
|
|
4771
|
+
* await plugin.player.setPaused(true);
|
|
4772
|
+
* } catch (error) {
|
|
4773
|
+
* if ((error as PluginError).code !== "stale") throw error; // another file now
|
|
4774
|
+
* }
|
|
4775
|
+
* ```
|
|
4776
|
+
*
|
|
4777
|
+
* @since 1.0.0
|
|
4778
|
+
*/
|
|
3857
4779
|
setPaused(paused: boolean, options?: Conditional): Promise<void>;
|
|
3858
|
-
/** Jump to `seconds`. */
|
|
3859
|
-
seek(seconds: number, options?: Conditional): Promise<void>;
|
|
3860
|
-
/** Set the playback speed. */
|
|
3861
|
-
setRate(rate: number, options?: Conditional): Promise<void>;
|
|
3862
|
-
/** Play from the start (the "replay" after the end). */
|
|
3863
|
-
restart(options?: Conditional): Promise<void>;
|
|
3864
|
-
/** Close the current source. */
|
|
3865
|
-
close(options?: Conditional): Promise<void>;
|
|
3866
|
-
/** Start a drag gesture from the current state. */
|
|
3867
|
-
scrub(): Scrub;
|
|
3868
4780
|
/**
|
|
3869
|
-
*
|
|
3870
|
-
*
|
|
3871
|
-
*
|
|
4781
|
+
* Jump to `seconds`.
|
|
4782
|
+
*
|
|
4783
|
+
* @capability playback.control
|
|
4784
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
4785
|
+
* based on (another file, a close, or any pause, seek
|
|
4786
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
4787
|
+
* @throws `invalid` (rejects) Nothing is open, or `seconds` is negative or
|
|
4788
|
+
* not finite.
|
|
4789
|
+
* @throws `unsupported` (rejects) The source is a generated stream, which
|
|
4790
|
+
* cannot seek.
|
|
4791
|
+
* @throws `busy` (rejects) A generated stream is still handling a previous
|
|
4792
|
+
* control.
|
|
4793
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4794
|
+
* `playback.control`.
|
|
4795
|
+
*
|
|
4796
|
+
* @example Jump back ten seconds
|
|
4797
|
+
* Playback jumps ten seconds back, never before the start. The target is
|
|
4798
|
+
* computed from the snapshot the call is conditional on, so if the user
|
|
4799
|
+
* opened another file meanwhile the seek fails with `stale` instead of
|
|
4800
|
+
* landing in the wrong file.
|
|
4801
|
+
* ```ts
|
|
4802
|
+
* const { position } = plugin.player.state;
|
|
4803
|
+
* if (position !== null && plugin.player.state.can.seek)
|
|
4804
|
+
* await plugin.player.seek(Math.max(0, position - 10)); // seconds
|
|
4805
|
+
* ```
|
|
4806
|
+
*
|
|
4807
|
+
* @since 1.0.0
|
|
3872
4808
|
*/
|
|
3873
|
-
|
|
3874
|
-
volume?: number;
|
|
3875
|
-
muted?: boolean;
|
|
3876
|
-
}): Promise<boolean>;
|
|
4809
|
+
seek(seconds: number, options?: Conditional): Promise<void>;
|
|
3877
4810
|
/**
|
|
3878
|
-
*
|
|
3879
|
-
*
|
|
4811
|
+
* Set the playback speed.
|
|
4812
|
+
*
|
|
4813
|
+
* @capability playback.control
|
|
4814
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
4815
|
+
* based on (another file, a close, or any pause, seek
|
|
4816
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
4817
|
+
* @throws `invalid` (rejects) Nothing is open, or `rate` is outside
|
|
4818
|
+
* 0.05–10.
|
|
4819
|
+
* @throws `unsupported` (rejects) The source is a generated stream, which
|
|
4820
|
+
* cannot change speed.
|
|
4821
|
+
* @throws `busy` (rejects) A generated stream is still handling a previous
|
|
4822
|
+
* control.
|
|
4823
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4824
|
+
* `playback.control`.
|
|
3880
4825
|
*
|
|
3881
|
-
* @example
|
|
4826
|
+
* @example Play at one and a half times the speed
|
|
4827
|
+
* Playback speeds up to 1.5×, with the speed on the OSD. `setRate` sets
|
|
4828
|
+
* the speed directly (the player's own keys step it by 0.1 within
|
|
4829
|
+
* 0.25–4); `state.can.rate` is false for sources that cannot change speed.
|
|
3882
4830
|
* ```ts
|
|
3883
|
-
* plugin.player.
|
|
3884
|
-
*
|
|
3885
|
-
*
|
|
3886
|
-
*
|
|
3887
|
-
* canPrevious: () => index > 0,
|
|
3888
|
-
* });
|
|
4831
|
+
* if (plugin.player.state.can.rate) {
|
|
4832
|
+
* await plugin.player.setRate(1.5);
|
|
4833
|
+
* await plugin.osd.show("Speed {}×", [1.5]);
|
|
4834
|
+
* }
|
|
3889
4835
|
* ```
|
|
4836
|
+
*
|
|
4837
|
+
* @since 1.0.0
|
|
3890
4838
|
*/
|
|
3891
|
-
|
|
3892
|
-
/** Ask the provider to go forward / back. Rejects `unavailable` when it cannot. */
|
|
3893
|
-
next(): Promise<void>;
|
|
3894
|
-
/** Ask the provider to go back. Rejects `unavailable` when it cannot. */
|
|
3895
|
-
previous(): Promise<void>;
|
|
4839
|
+
setRate(rate: number, options?: Conditional): Promise<void>;
|
|
3896
4840
|
/**
|
|
3897
|
-
*
|
|
3898
|
-
*
|
|
4841
|
+
* Play from the start (the "replay" after the end).
|
|
4842
|
+
*
|
|
4843
|
+
* @capability playback.control
|
|
4844
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
4845
|
+
* based on (another file, a close, or any pause, seek
|
|
4846
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
4847
|
+
* @throws `invalid` (rejects) Nothing is open.
|
|
4848
|
+
* @throws `unsupported` (rejects) The source is a generated stream, which
|
|
4849
|
+
* cannot restart.
|
|
4850
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4851
|
+
* `playback.control`.
|
|
4852
|
+
*
|
|
4853
|
+
* @example Replay a file after it ended
|
|
4854
|
+
* A Replay button starts the file again from its start once it has ended.
|
|
4855
|
+
* `restart` plays the current source from the beginning and keeps it the
|
|
4856
|
+
* same source, so a {@link Media} handle stays current.
|
|
4857
|
+
* ```ts
|
|
4858
|
+
* if (plugin.player.state.phase === "ended") await plugin.player.restart();
|
|
4859
|
+
* ```
|
|
4860
|
+
*
|
|
4861
|
+
* @since 1.0.0
|
|
3899
4862
|
*/
|
|
3900
|
-
|
|
4863
|
+
restart(options?: Conditional): Promise<void>;
|
|
4864
|
+
/**
|
|
4865
|
+
* Close the current source.
|
|
4866
|
+
*
|
|
4867
|
+
* @capability playback.control
|
|
4868
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
4869
|
+
* based on (another file, a close, or any pause, seek
|
|
4870
|
+
* or speed change by the user or another plugin); nothing was done.
|
|
4871
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4872
|
+
* `playback.control`.
|
|
4873
|
+
*
|
|
4874
|
+
* @example Close the file when the sleep timer runs out
|
|
4875
|
+
* After 30 minutes the open file is closed, unless the user opened another
|
|
4876
|
+
* one meanwhile. The snapshot taken when the timer started is passed as
|
|
4877
|
+
* `ifCurrent`, so the close applies only to that source; `stale` means it
|
|
4878
|
+
* was replaced.
|
|
4879
|
+
* ```ts
|
|
4880
|
+
* const seen = plugin.player.state;
|
|
4881
|
+
* setTimeout(() => {
|
|
4882
|
+
* // Only the source that was open when the timer started.
|
|
4883
|
+
* plugin.player.close({ ifCurrent: seen }).catch((error: PluginError) => {
|
|
4884
|
+
* if (error.code !== "stale") plugin.osd.show("Could not close: {}", [error.code]);
|
|
4885
|
+
* });
|
|
4886
|
+
* }, 30 * 60 * 1000);
|
|
4887
|
+
* ```
|
|
4888
|
+
*
|
|
4889
|
+
* @since 1.0.0
|
|
4890
|
+
*/
|
|
4891
|
+
close(options?: Conditional): Promise<void>;
|
|
4892
|
+
/**
|
|
4893
|
+
* Start a drag gesture from the current state.
|
|
4894
|
+
*
|
|
4895
|
+
* @capability playback.control
|
|
4896
|
+
*
|
|
4897
|
+
* @example Start a gesture when the user grabs the slider
|
|
4898
|
+
* A drag on the plugin's seek slider becomes one gesture whose seeks never
|
|
4899
|
+
* pile up. `scrub()` captures the current state; the gesture's seeks are
|
|
4900
|
+
* conditional on it and on each other, and it ends on its own when
|
|
4901
|
+
* someone else changes the source.
|
|
4902
|
+
* ```ts
|
|
4903
|
+
* declare const bar: Panel;
|
|
4904
|
+
* // ---cut---
|
|
4905
|
+
* let scrub: Scrub | null = null;
|
|
4906
|
+
* bar.on("dragStart", () => (scrub = plugin.player.scrub()));
|
|
4907
|
+
* bar.on("dragEnd", () => scrub?.end());
|
|
4908
|
+
* ```
|
|
4909
|
+
*
|
|
4910
|
+
* @see [Keep a slider responsive while dragging](../../docs/how-to/slider.md)
|
|
4911
|
+
* @since 1.0.0
|
|
4912
|
+
*/
|
|
4913
|
+
scrub(): Scrub;
|
|
4914
|
+
/**
|
|
4915
|
+
* Device output; last write wins and volume is applied before mute.
|
|
4916
|
+
* Resolves `true` when this request was applied, `false` if superseded.
|
|
4917
|
+
* `volume` is 0–200.
|
|
4918
|
+
*
|
|
4919
|
+
* @capability playback.control
|
|
4920
|
+
* @throws `invalid` (rejects) `volume` outside 0–200, or `muted` not a
|
|
4921
|
+
* boolean.
|
|
4922
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4923
|
+
* `playback.control`.
|
|
4924
|
+
*
|
|
4925
|
+
* @example Duck the volume during an announcement
|
|
4926
|
+
* The volume drops to 30 while the plugin's announcement plays, then
|
|
4927
|
+
* returns. Writes are "latest wins": the promise resolves `true` when this
|
|
4928
|
+
* write was applied and `false` when a newer one replaced it first, which
|
|
4929
|
+
* is not an error.
|
|
4930
|
+
* ```ts
|
|
4931
|
+
* const before = plugin.player.state.volume; // 0–200
|
|
4932
|
+
* await plugin.player.setOutput({ volume: 30 });
|
|
4933
|
+
* // … the announcement …
|
|
4934
|
+
* await plugin.player.setOutput({ volume: before });
|
|
4935
|
+
* ```
|
|
4936
|
+
*
|
|
4937
|
+
* @since 1.0.0
|
|
4938
|
+
*/
|
|
4939
|
+
setOutput(output: {
|
|
4940
|
+
volume?: number;
|
|
4941
|
+
muted?: boolean;
|
|
4942
|
+
}): Promise<boolean>;
|
|
4943
|
+
/**
|
|
4944
|
+
* Become the sequence provider, first come first served: while another
|
|
4945
|
+
* plugin holds it (the built-in playlist registers at activation), this
|
|
4946
|
+
* one is refused with `conflict` (`sequence-taken`). That does not fail
|
|
4947
|
+
* activation: the player shows a warning ({@link PluginInfo.warnings});
|
|
4948
|
+
* a refusal after activation goes to the scope's `onError`. Dispose to
|
|
4949
|
+
* stop.
|
|
4950
|
+
*
|
|
4951
|
+
* @example Drive the player's Next and Previous from a playlist
|
|
4952
|
+
* The player's Next and Previous items and keys play the plugin's list.
|
|
4953
|
+
* The player calls `next` and `previous` when the user asks; `canNext`
|
|
4954
|
+
* and `canPrevious` are evaluated again after every handler the SDK runs,
|
|
4955
|
+
* and while they return false the player disables its items ("No next
|
|
4956
|
+
* item").
|
|
4957
|
+
* ```ts
|
|
4958
|
+
* declare const items: string[];
|
|
4959
|
+
* let index = 0;
|
|
4960
|
+
* // ---cut---
|
|
4961
|
+
* const playAt = async (at: number) => {
|
|
4962
|
+
* index = at;
|
|
4963
|
+
* await plugin.player.open(items[at]!);
|
|
4964
|
+
* };
|
|
4965
|
+
* // Called by the player's Next / Previous items, keys and buttons.
|
|
4966
|
+
* plugin.player.provideSequence({
|
|
4967
|
+
* next: () => playAt(index + 1),
|
|
4968
|
+
* previous: () => playAt(index - 1),
|
|
4969
|
+
* canNext: () => index + 1 < items.length,
|
|
4970
|
+
* canPrevious: () => index > 0,
|
|
4971
|
+
* });
|
|
4972
|
+
* ```
|
|
4973
|
+
*
|
|
4974
|
+
* @capability playback.control
|
|
4975
|
+
* @since 1.0.0
|
|
4976
|
+
*/
|
|
4977
|
+
provideSequence(provider: SequenceProvider): Disposable;
|
|
4978
|
+
/**
|
|
4979
|
+
* Ask the provider to go forward / back. Rejects `unavailable` when it cannot.
|
|
4980
|
+
*
|
|
4981
|
+
* @capability playback.control
|
|
4982
|
+
* @throws `unavailable` (rejects) There is no sequence provider, or it has
|
|
4983
|
+
* no next item.
|
|
4984
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
4985
|
+
* `playback.control`.
|
|
4986
|
+
*
|
|
4987
|
+
* @example Skip to the next item from a plugin button
|
|
4988
|
+
* The plugin's own Next button plays the next item of whichever plugin
|
|
4989
|
+
* provides the sequence (the playlist, usually). `next` asks the
|
|
4990
|
+
* provider, the same as the player's Next key; `unavailable` means there
|
|
4991
|
+
* is no next item, which the button treats as the end of the list.
|
|
4992
|
+
* ```ts
|
|
4993
|
+
* try {
|
|
4994
|
+
* await plugin.player.next(); // as the player's Next key does
|
|
4995
|
+
* } catch (error) {
|
|
4996
|
+
* if ((error as PluginError).code !== "unavailable") throw error;
|
|
4997
|
+
* await plugin.osd.show("End of the list");
|
|
4998
|
+
* }
|
|
4999
|
+
* ```
|
|
5000
|
+
*
|
|
5001
|
+
* @since 1.0.0
|
|
5002
|
+
*/
|
|
5003
|
+
next(): Promise<void>;
|
|
5004
|
+
/**
|
|
5005
|
+
* Ask the provider to go back. Rejects `unavailable` when it cannot.
|
|
5006
|
+
*
|
|
5007
|
+
* @capability playback.control
|
|
5008
|
+
* @throws `unavailable` (rejects) There is no sequence provider, or it has
|
|
5009
|
+
* no previous item.
|
|
5010
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5011
|
+
* `playback.control`.
|
|
5012
|
+
*
|
|
5013
|
+
* @example Go back to the previous item
|
|
5014
|
+
* The plugin's Previous button asks the sequence provider for the item
|
|
5015
|
+
* before the current one, like the player's PageUp. It is disabled while
|
|
5016
|
+
* `state.can.previous` is false, which the provider's `canPrevious`
|
|
5017
|
+
* decides.
|
|
5018
|
+
* ```ts
|
|
5019
|
+
* plugin.commands.add({
|
|
5020
|
+
* id: "previous",
|
|
5021
|
+
* title: "Previous item",
|
|
5022
|
+
* // The provider's `canPrevious`, as the player reports it.
|
|
5023
|
+
* enabled: () => plugin.player.state.can.previous,
|
|
5024
|
+
* run: () => plugin.player.previous(),
|
|
5025
|
+
* });
|
|
5026
|
+
* ```
|
|
5027
|
+
*
|
|
5028
|
+
* @since 1.0.0
|
|
5029
|
+
*/
|
|
5030
|
+
previous(): Promise<void>;
|
|
5031
|
+
/**
|
|
5032
|
+
* Codec, size, frame counts and backend of the current playback.
|
|
5033
|
+
* `playback.read`; `video` and `audio` are `null` without a source.
|
|
5034
|
+
*
|
|
5035
|
+
* @capability playback.read
|
|
5036
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5037
|
+
* `playback.read`.
|
|
5038
|
+
*
|
|
5039
|
+
* @example Show the video codec and size
|
|
5040
|
+
* The OSD shows something like "h264 1920×1080". `stats` asks the player
|
|
5041
|
+
* for the backend's own values at that moment; `video` is `null` without
|
|
5042
|
+
* a video stream (nothing open, an audio file).
|
|
5043
|
+
* ```ts
|
|
5044
|
+
* const { video } = await plugin.player.stats();
|
|
5045
|
+
* if (video) await plugin.osd.show("{} {}×{}", [video.codec ?? "?", video.width ?? 0, video.height ?? 0]);
|
|
5046
|
+
* ```
|
|
5047
|
+
*
|
|
5048
|
+
* @since 1.0.0
|
|
5049
|
+
*/
|
|
5050
|
+
stats(): Promise<PlaybackStats>;
|
|
3901
5051
|
/**
|
|
3902
5052
|
* Add an external audio file (absolute local path) to the current media and,
|
|
3903
5053
|
* unless `select: false`, select it. Resolves with the new track (`external`,
|
|
3904
5054
|
* not exportable). It stays until the source changes. Rejects `invalid` for a
|
|
3905
5055
|
* bad path, `unavailable` without ready media, `backend` when it cannot load.
|
|
5056
|
+
*
|
|
5057
|
+
* @capability playback.control
|
|
5058
|
+
* @throws `invalid` (rejects) Not an absolute path to a file.
|
|
5059
|
+
* @throws `unavailable` (rejects) No ready media.
|
|
5060
|
+
* @throws `backend` (rejects) The player could not load the file.
|
|
5061
|
+
* @throws `stale` (rejects) `ifCurrent` no longer holds.
|
|
5062
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5063
|
+
* `playback.control`.
|
|
5064
|
+
*
|
|
5065
|
+
* @example Add a dubbed audio file and switch to it
|
|
5066
|
+
* A separate audio file plays with the film instead of its own sound.
|
|
5067
|
+
* `addAudioTrack` attaches the file to the current media and selects it;
|
|
5068
|
+
* the track stays until another source opens.
|
|
5069
|
+
* ```ts
|
|
5070
|
+
* declare const dubPath: string;
|
|
5071
|
+
* // ---cut---
|
|
5072
|
+
* const track = await plugin.player.addAudioTrack(dubPath); // selected too
|
|
5073
|
+
* await plugin.osd.show("Audio: {}", [track.title ?? dubPath]);
|
|
5074
|
+
* ```
|
|
5075
|
+
*
|
|
5076
|
+
* @since 1.0.0
|
|
3906
5077
|
*/
|
|
3907
5078
|
addAudioTrack(path: string, options?: {
|
|
3908
5079
|
select?: boolean;
|
|
3909
5080
|
} & Conditional): Promise<AudioTrack>;
|
|
3910
|
-
/**
|
|
5081
|
+
/**
|
|
5082
|
+
* Select one of `state.audioTracks` by id; `invalid` for an unknown id.
|
|
5083
|
+
*
|
|
5084
|
+
* @capability playback.control
|
|
5085
|
+
* @throws `invalid` (rejects) No track with this id.
|
|
5086
|
+
* @throws `unavailable` (rejects) No ready media.
|
|
5087
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5088
|
+
* based on (another file, a close, or any pause, seek
|
|
5089
|
+
* or speed change by the user or another plugin).
|
|
5090
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5091
|
+
* `playback.control`.
|
|
5092
|
+
*
|
|
5093
|
+
* @example Switch to the next audio track
|
|
5094
|
+
* Each press of the plugin's key moves to the next audio track of the
|
|
5095
|
+
* file. The tracks and the selected one are in `state.audioTracks`;
|
|
5096
|
+
* `selectAudioTrack` resolves with the new state.
|
|
5097
|
+
* ```ts
|
|
5098
|
+
* const audioTracks = plugin.player.state.audioTracks ?? []; // null: not known yet
|
|
5099
|
+
* const at = audioTracks.findIndex((track) => track.selected);
|
|
5100
|
+
* const next = audioTracks[(at + 1) % audioTracks.length];
|
|
5101
|
+
* if (next) await plugin.player.selectAudioTrack(next.id);
|
|
5102
|
+
* ```
|
|
5103
|
+
*
|
|
5104
|
+
* @since 1.0.0
|
|
5105
|
+
*/
|
|
3911
5106
|
selectAudioTrack(id: number, options?: Conditional): Promise<PlayerState>;
|
|
3912
5107
|
/**
|
|
3913
5108
|
* One frame or to the next / previous keyframe (pauses for frames). Rejects
|
|
@@ -3917,6 +5112,29 @@ export interface Player {
|
|
|
3917
5112
|
* Resolves once the new position is in effect, so `player.state.position`
|
|
3918
5113
|
* is current afterwards. One step at a time: a second call while one is
|
|
3919
5114
|
* in flight rejects `busy`.
|
|
5115
|
+
*
|
|
5116
|
+
* @capability playback.control
|
|
5117
|
+
* @throws `unsupported` (rejects) Nothing playable to step through (no
|
|
5118
|
+
* moving video: audio only, cover art, a generated stream), a backward
|
|
5119
|
+
* frame step on a source that cannot seek, or a keyframe step in either
|
|
5120
|
+
* direction on a source that cannot seek.
|
|
5121
|
+
* @throws `busy` (rejects) A step is still in flight.
|
|
5122
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5123
|
+
* based on (another file, a close, or any pause, seek
|
|
5124
|
+
* or speed change by the user or another plugin).
|
|
5125
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5126
|
+
* `playback.control`.
|
|
5127
|
+
*
|
|
5128
|
+
* @example Step one frame forward for a close look
|
|
5129
|
+
* The picture advances by one frame and playback pauses. `step` resolves
|
|
5130
|
+
* once the new position is in effect, so `state.position` is current
|
|
5131
|
+
* afterwards; a second step while one is running rejects with `busy`.
|
|
5132
|
+
* ```ts
|
|
5133
|
+
* await plugin.player.step("frame", "forward");
|
|
5134
|
+
* await plugin.osd.show("{} s", [plugin.player.state.position?.toFixed(3) ?? "?"]);
|
|
5135
|
+
* ```
|
|
5136
|
+
*
|
|
5137
|
+
* @since 1.0.0
|
|
3920
5138
|
*/
|
|
3921
5139
|
step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
|
|
3922
5140
|
/**
|
|
@@ -3939,6 +5157,23 @@ export interface Player {
|
|
|
3939
5157
|
*
|
|
3940
5158
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
3941
5159
|
* the manifest when you rely on it.
|
|
5160
|
+
*
|
|
5161
|
+
* @capability playback.control
|
|
5162
|
+
* @throws `invalid` (rejects) `seconds` outside 0.1–3600 or not in tenths,
|
|
5163
|
+
* or `keyframes` not a boolean.
|
|
5164
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5165
|
+
* `playback.control`.
|
|
5166
|
+
*
|
|
5167
|
+
* @example Make ← and → jump 10 seconds to keyframes
|
|
5168
|
+
* The arrow keys now jump 10 seconds and land on keyframes, which is fast
|
|
5169
|
+
* on large files. The newest call of any plugin wins and is withdrawn
|
|
5170
|
+
* when that plugin stops, so a plugin offering this as a setting applies
|
|
5171
|
+
* it on activation.
|
|
5172
|
+
* ```ts
|
|
5173
|
+
* await plugin.player.setStepSeek({ seconds: 10, keyframes: true });
|
|
5174
|
+
* ```
|
|
5175
|
+
*
|
|
5176
|
+
* @since 1.0.0
|
|
3942
5177
|
*/
|
|
3943
5178
|
setStepSeek(options: {
|
|
3944
5179
|
seconds: number;
|
|
@@ -3948,6 +5183,23 @@ export interface Player {
|
|
|
3948
5183
|
* The chapters of the current media (empty without media or chapters),
|
|
3949
5184
|
* read on demand; `sourceId` tells which source they belong to.
|
|
3950
5185
|
* `playback.read`.
|
|
5186
|
+
*
|
|
5187
|
+
* @capability playback.read
|
|
5188
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5189
|
+
* `playback.read`.
|
|
5190
|
+
*
|
|
5191
|
+
* @example List the chapters of the open file
|
|
5192
|
+
* The plugin reads the chapter titles of the current file to show them in
|
|
5193
|
+
* its panel. `chapters` asks the player when called; `sourceId` says which
|
|
5194
|
+
* source they belong to, so a reply for a file that was replaced meanwhile
|
|
5195
|
+
* can be dropped.
|
|
5196
|
+
* ```ts
|
|
5197
|
+
* const { sourceId, chapters } = await plugin.player.chapters();
|
|
5198
|
+
* if (sourceId === plugin.player.state.sourceId) // still the same file
|
|
5199
|
+
* console.log(chapters.map((chapter) => chapter.title));
|
|
5200
|
+
* ```
|
|
5201
|
+
*
|
|
5202
|
+
* @since 1.0.0
|
|
3951
5203
|
*/
|
|
3952
5204
|
chapters(): Promise<{
|
|
3953
5205
|
/** The source the chapters belong to (compare with `state.sourceId`). */
|
|
@@ -3961,13 +5213,59 @@ export interface Player {
|
|
|
3961
5213
|
* stops it. After the last jump back `state.loop` becomes null.
|
|
3962
5214
|
* `invalid` out of range, `unsupported` for a source that cannot seek,
|
|
3963
5215
|
* `unavailable` without ready media. A new source clears it.
|
|
5216
|
+
*
|
|
5217
|
+
* @capability playback.control
|
|
5218
|
+
* @throws `invalid` (rejects) `a`/`b` out of range or `count` not a positive
|
|
5219
|
+
* integer.
|
|
5220
|
+
* @throws `unsupported` (rejects) The source cannot seek, or is a generated
|
|
5221
|
+
* stream.
|
|
5222
|
+
* @throws `unavailable` (rejects) No ready media.
|
|
5223
|
+
* @throws `stale` (rejects) The source changed since the state the call is
|
|
5224
|
+
* based on (another file, a close, or any pause, seek
|
|
5225
|
+
* or speed change by the user or another plugin).
|
|
5226
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5227
|
+
* `playback.control`.
|
|
5228
|
+
*
|
|
5229
|
+
* @example Loop a passage three times
|
|
5230
|
+
* Playback repeats 1:00–1:30 three times, then goes on. The player jumps
|
|
5231
|
+
* back to `a` each time it reaches `b`; after the last repeat
|
|
5232
|
+
* `state.loop` becomes `null`, and a new source clears the loop.
|
|
5233
|
+
* ```ts
|
|
5234
|
+
* await plugin.player.setLoop({ a: 60, b: 90, count: 3 }); // seconds
|
|
5235
|
+
* ```
|
|
5236
|
+
*
|
|
5237
|
+
* @since 1.0.0
|
|
3964
5238
|
*/
|
|
3965
5239
|
setLoop(loop: {
|
|
3966
5240
|
a: number;
|
|
3967
5241
|
b: number;
|
|
3968
5242
|
count?: number;
|
|
3969
5243
|
} | null, options?: Conditional): Promise<void>;
|
|
3970
|
-
/**
|
|
5244
|
+
/**
|
|
5245
|
+
* The output devices present now (not the system default entry). `playback.read`.
|
|
5246
|
+
*
|
|
5247
|
+
* @capability playback.read
|
|
5248
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5249
|
+
* `playback.read`.
|
|
5250
|
+
*
|
|
5251
|
+
* @example Offer the audio output devices in a menu
|
|
5252
|
+
* A submenu lists the output devices present now, with a radio mark on the
|
|
5253
|
+
* one in use. `audioDevices` asks the player when called (devices come and
|
|
5254
|
+
* go), and `state.audioOutput` tells which one is active.
|
|
5255
|
+
* ```ts
|
|
5256
|
+
* // The devices present now; build the menu again when they change.
|
|
5257
|
+
* for (const device of await plugin.player.audioDevices())
|
|
5258
|
+
* plugin.commands.add({
|
|
5259
|
+
* id: `device.${device.id}`,
|
|
5260
|
+
* path: ["@audio", "Output", device.name],
|
|
5261
|
+
* radio: true,
|
|
5262
|
+
* checked: () => plugin.player.state.audioOutput?.active === device.id,
|
|
5263
|
+
* run: () => plugin.player.setAudioDevice(device.id),
|
|
5264
|
+
* });
|
|
5265
|
+
* ```
|
|
5266
|
+
*
|
|
5267
|
+
* @since 1.0.0
|
|
5268
|
+
*/
|
|
3971
5269
|
audioDevices(): Promise<AudioDevice[]>;
|
|
3972
5270
|
/**
|
|
3973
5271
|
* Play on this device while it is present, else on the system default
|
|
@@ -3977,6 +5275,23 @@ export interface Player {
|
|
|
3977
5275
|
* Unknown ids are accepted (`state.audioOutput.active` stays `null`); a
|
|
3978
5276
|
* malformed id is `invalid`. Reverts to the default when this plugin
|
|
3979
5277
|
* stops; save the choice yourself to restore it on activation.
|
|
5278
|
+
*
|
|
5279
|
+
* @capability playback.control
|
|
5280
|
+
* @throws `invalid` (rejects) A malformed id.
|
|
5281
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
5282
|
+
* `playback.control`.
|
|
5283
|
+
*
|
|
5284
|
+
* @example Restore the user's output device on start
|
|
5285
|
+
* The plugin plays on the device the user picked last time, or on the
|
|
5286
|
+
* system default when it is not connected. The choice lasts only for
|
|
5287
|
+
* this run and is withdrawn when the plugin stops, so the plugin saves
|
|
5288
|
+
* it itself and sets it again on activation.
|
|
5289
|
+
* ```ts
|
|
5290
|
+
* const store = plugin.storage.json("output", { defaults: { device: null as string | null } });
|
|
5291
|
+
* await plugin.player.setAudioDevice(store.value.device); // null: the system default
|
|
5292
|
+
* ```
|
|
5293
|
+
*
|
|
5294
|
+
* @since 1.0.0
|
|
3980
5295
|
*/
|
|
3981
5296
|
setAudioDevice(id: string | null): Promise<void>;
|
|
3982
5297
|
}
|
|
@@ -4001,9 +5316,45 @@ export interface Player {
|
|
|
4001
5316
|
* (`plugin.window.onChange`), which also changes when the user toggles it.
|
|
4002
5317
|
* Players before 0.5.2 reject it with `invalid`: set
|
|
4003
5318
|
* `"minPlayerVersion": "0.5.2"` when you use it.
|
|
5319
|
+
*
|
|
5320
|
+
* @see {@link Host.run}
|
|
5321
|
+
* @since 1.0.0
|
|
5322
|
+
* @category Commands
|
|
4004
5323
|
*/
|
|
4005
5324
|
export type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute" | "fullscreen";
|
|
4006
|
-
/**
|
|
5325
|
+
/**
|
|
5326
|
+
* `plugin.host`: the player's own core actions.
|
|
5327
|
+
*
|
|
5328
|
+
* @remarks
|
|
5329
|
+
* The player's core actions (play/pause, stop, step seeks, speed, volume,
|
|
5330
|
+
* fullscreen…) are commands of the player itself, `host/<action>` in the
|
|
5331
|
+
* binding table. Running them through `host.run` gives a plugin's own
|
|
5332
|
+
* buttons exactly the player's behaviour: the same checks, the same
|
|
5333
|
+
* accumulation of repeated presses and the same on-screen text, instead of
|
|
5334
|
+
* reimplementing them with {@link Player} calls.
|
|
5335
|
+
*
|
|
5336
|
+
* @capability commands
|
|
5337
|
+
*
|
|
5338
|
+
* @example Give a custom bar the player's own Stop and Mute
|
|
5339
|
+
* The plugin's bar buttons stop playback and mute exactly as the player's
|
|
5340
|
+
* own keys do, with the same on-screen text. Each callback runs a core
|
|
5341
|
+
* action; the player applies its checks and rejects with `unavailable`
|
|
5342
|
+
* while the action is disabled, which the bar ignores.
|
|
5343
|
+
* ```ts
|
|
5344
|
+
* declare const bar: Panel;
|
|
5345
|
+
* // ---cut---
|
|
5346
|
+
* const run = (action: HostAction) =>
|
|
5347
|
+
* plugin.host.run(action).catch((error: PluginError) => {
|
|
5348
|
+
* if (error.code !== "unavailable") throw error; // disabled right now
|
|
5349
|
+
* });
|
|
5350
|
+
* bar.on("stop", () => run("stop"));
|
|
5351
|
+
* bar.on("mute", () => run("mute"));
|
|
5352
|
+
* ```
|
|
5353
|
+
*
|
|
5354
|
+
* @see {@link HostAction}, {@link Player}
|
|
5355
|
+
* @since 1.0.0
|
|
5356
|
+
* @category Commands
|
|
5357
|
+
*/
|
|
4007
5358
|
export interface Host {
|
|
4008
5359
|
/**
|
|
4009
5360
|
* Run a core action exactly like its menu item or key: same checks, same
|
|
@@ -4013,10 +5364,31 @@ export interface Host {
|
|
|
4013
5364
|
*
|
|
4014
5365
|
* Prefer this over reimplementing playback buttons.
|
|
4015
5366
|
*
|
|
4016
|
-
* @example
|
|
5367
|
+
* @example Make a bar button play and pause like Space
|
|
5368
|
+
* The plugin's own Play button behaves exactly like the player's Space
|
|
5369
|
+
* key, including its on-screen text. `host.run` runs the core action
|
|
5370
|
+
* with the player's checks; it resolves when the action finished, and
|
|
5371
|
+
* rejects with `unavailable` while the action is disabled (nothing open).
|
|
4017
5372
|
* ```ts
|
|
4018
|
-
*
|
|
5373
|
+
* declare const bar: Panel;
|
|
5374
|
+
* // ---cut---
|
|
5375
|
+
* bar.on("playPause", async () => {
|
|
5376
|
+
* try {
|
|
5377
|
+
* await plugin.host.run("play-pause");
|
|
5378
|
+
* } catch (error) {
|
|
5379
|
+
* if ((error as PluginError).code !== "unavailable") throw error;
|
|
5380
|
+
* // Nothing to play: the button simply does nothing.
|
|
5381
|
+
* }
|
|
5382
|
+
* });
|
|
4019
5383
|
* ```
|
|
5384
|
+
*
|
|
5385
|
+
* @capability commands
|
|
5386
|
+
* @throws `unavailable` (rejects) The action is disabled right now (nothing
|
|
5387
|
+
* open, no window for `fullscreen`).
|
|
5388
|
+
* @throws `invalid` (rejects) An unknown action, or `seconds` out of range.
|
|
5389
|
+
* @throws `capability` (rejects) The manifest does not declare `commands`.
|
|
5390
|
+
*
|
|
5391
|
+
* @since 1.0.0
|
|
4020
5392
|
*/
|
|
4021
5393
|
run(action: HostAction): Promise<void>;
|
|
4022
5394
|
/**
|
|
@@ -4036,10 +5408,22 @@ export interface Host {
|
|
|
4036
5408
|
* `seconds` and seek by the `setStepSeek` step)
|
|
4037
5409
|
* @since 1.5.19
|
|
4038
5410
|
*
|
|
4039
|
-
* @example
|
|
5411
|
+
* @example Add a 30-second skip that adds up with the arrow keys
|
|
5412
|
+
* The plugin's "+30 s" button seeks forward 30 seconds and shows the same
|
|
5413
|
+
* text as →. Passing `seconds` makes the core step seek use that step
|
|
5414
|
+
* with the player's mode (exact or keyframes); quick presses of it and of
|
|
5415
|
+
* → add up into one seek.
|
|
4040
5416
|
* ```ts
|
|
5417
|
+
* declare const bar: Panel;
|
|
5418
|
+
* // ---cut---
|
|
4041
5419
|
* bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 }));
|
|
4042
5420
|
* ```
|
|
5421
|
+
*
|
|
5422
|
+
* @capability commands
|
|
5423
|
+
* @throws `unavailable` (rejects) The action is disabled right now (nothing
|
|
5424
|
+
* open, no window for `fullscreen`).
|
|
5425
|
+
* @throws `invalid` (rejects) An unknown action, or `seconds` out of range.
|
|
5426
|
+
* @throws `capability` (rejects) The manifest does not declare `commands`.
|
|
4043
5427
|
*/
|
|
4044
5428
|
run(action: "seek-backward" | "seek-forward", options: {
|
|
4045
5429
|
seconds: number;
|
|
@@ -4058,11 +5442,16 @@ export interface Host {
|
|
|
4058
5442
|
*
|
|
4059
5443
|
* @capability ui.osd
|
|
4060
5444
|
*
|
|
4061
|
-
* @example
|
|
5445
|
+
* @example Show live playback numbers over the video
|
|
5446
|
+
* A block over the video shows the position and the duration, updated
|
|
5447
|
+
* as playback moves. `player.onChange` runs after the SDK updated the
|
|
5448
|
+
* state (with `events`); `set` never throws and sends only the newest
|
|
5449
|
+
* text while one is in flight, so calling it on every change is fine.
|
|
4062
5450
|
* ```ts
|
|
4063
5451
|
* const info = plugin.osd.panel();
|
|
4064
5452
|
* plugin.player.onChange((state) =>
|
|
4065
|
-
*
|
|
5453
|
+
* // A template with data arguments; the player translates "{} / {} s".
|
|
5454
|
+
* info.set("{} / {} s", [state.position?.toFixed(1) ?? "–", state.duration?.toFixed(1) ?? "–"]),
|
|
4066
5455
|
* );
|
|
4067
5456
|
* ```
|
|
4068
5457
|
*
|
|
@@ -4089,7 +5478,10 @@ export interface OsdPanel extends Disposable {
|
|
|
4089
5478
|
* omitted for none.
|
|
4090
5479
|
* @param options - `maxColumns`: cut long lines (SDK 1.5.11).
|
|
4091
5480
|
*
|
|
4092
|
-
* @example
|
|
5481
|
+
* @example Show two loop points, then hide the block
|
|
5482
|
+
* The block shows "A: 12.0 s" and "B: 18.5 s" on two lines, then
|
|
5483
|
+
* disappears. The text is a template the player translates and fills
|
|
5484
|
+
* in; `\n` separates lines, and `null` hides the block.
|
|
4093
5485
|
* ```ts
|
|
4094
5486
|
* const info = plugin.osd.panel();
|
|
4095
5487
|
* info.set("A: {} s\nB: {} s", ["12.0", "18.5"]);
|
|
@@ -4148,7 +5540,10 @@ export interface OsdPanelOptions {
|
|
|
4148
5540
|
*
|
|
4149
5541
|
* @capability ui.osd
|
|
4150
5542
|
*
|
|
4151
|
-
* @example
|
|
5543
|
+
* @example Show on-screen messages the user can turn off
|
|
5544
|
+
* "Blur on" appears over the video unless the user turned messages off in
|
|
5545
|
+
* the plugin's settings. `osd.show` asks the player to draw the text for a
|
|
5546
|
+
* moment; the plugin checks its own stored preference before each call.
|
|
4152
5547
|
* ```ts
|
|
4153
5548
|
* const store = plugin.storage.json("prefs", { defaults: { messages: true } });
|
|
4154
5549
|
* const say = (text: string) => {
|
|
@@ -4192,7 +5587,11 @@ export interface Osd {
|
|
|
4192
5587
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
4193
5588
|
* @throws `capability` (rejects) The manifest does not declare `ui.osd`.
|
|
4194
5589
|
*
|
|
4195
|
-
* @example
|
|
5590
|
+
* @example Confirm an action and show a value
|
|
5591
|
+
* The OSD shows "Bookmark added", then "Speed 1.5×" in the user's
|
|
5592
|
+
* language. The text is English source the player translates with the
|
|
5593
|
+
* plugin's catalog; `{}` is filled in with the argument, which stays as
|
|
5594
|
+
* it is because it is data.
|
|
4196
5595
|
* ```ts
|
|
4197
5596
|
* await plugin.osd.show("Bookmark added");
|
|
4198
5597
|
* await plugin.osd.show("Speed {}×", [plugin.player.state.rate]);
|
|
@@ -4214,7 +5613,10 @@ export interface Osd {
|
|
|
4214
5613
|
*
|
|
4215
5614
|
* @throws `Error` without a code (throws) This plugin already has a block.
|
|
4216
5615
|
*
|
|
4217
|
-
* @example
|
|
5616
|
+
* @example Show a status block over the video
|
|
5617
|
+
* A small block over the video says "Loop: off" until the plugin changes
|
|
5618
|
+
* or hides it. Each plugin has one block, stacked with other plugins'
|
|
5619
|
+
* blocks; a second `panel()` call throws.
|
|
4218
5620
|
* ```ts
|
|
4219
5621
|
* const info = plugin.osd.panel();
|
|
4220
5622
|
* info.set("Loop: off");
|
|
@@ -4292,9 +5694,14 @@ interface Filter<T> extends Disposable {
|
|
|
4292
5694
|
* Values out of range fail with `invalid` (see {@link VideoNode}). The
|
|
4293
5695
|
* same chain applies to processed snapshots and exports.
|
|
4294
5696
|
*
|
|
4295
|
-
* @example
|
|
5697
|
+
* @example Brighten the picture and report when it fails
|
|
5698
|
+
* Every video plays a little brighter with more contrast, and the OSD
|
|
5699
|
+
* says why when the player cannot apply it. `set` records the desired
|
|
5700
|
+
* chain (last write wins, one request in flight); the player reapplies it
|
|
5701
|
+
* for each new source, and `onStatus` reports `applied` or `failed`.
|
|
4296
5702
|
* ```ts
|
|
4297
5703
|
* const picture = plugin.video.filter("picture");
|
|
5704
|
+
* // `unsupported` while a generated stream plays: its frames skip filters.
|
|
4298
5705
|
* picture.onStatus((s) => s.state === "failed" && plugin.osd.show(s.error.message));
|
|
4299
5706
|
* picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
|
|
4300
5707
|
* ```
|
|
@@ -4309,10 +5716,13 @@ export type VideoFilter = Filter<VideoNode[]>;
|
|
|
4309
5716
|
* `delayMs` is −10000–10000 in multiples of 10; the positive and the
|
|
4310
5717
|
* negative delays of all plugins each add up to at most 10000.
|
|
4311
5718
|
*
|
|
4312
|
-
* @example
|
|
5719
|
+
* @example Lower the volume and delay the sound
|
|
5720
|
+
* Every file plays 6 dB quieter, with its sound 120 ms later to match a
|
|
5721
|
+
* slow display. `set` sends the plugin's whole chain and delay; the
|
|
5722
|
+
* player applies it to each source and adds the delays of all plugins.
|
|
4313
5723
|
* ```ts
|
|
4314
5724
|
* const audio = plugin.audio.filter("sound");
|
|
4315
|
-
* audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 });
|
|
5725
|
+
* audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 }); // 10 ms steps
|
|
4316
5726
|
* ```
|
|
4317
5727
|
*/
|
|
4318
5728
|
export type AudioFilter = Filter<{
|
|
@@ -4347,7 +5757,11 @@ export interface ViewSettings {
|
|
|
4347
5757
|
* Out-of-range values or unknown fields fail with `invalid` without
|
|
4348
5758
|
* sending anything.
|
|
4349
5759
|
*
|
|
4350
|
-
* @example
|
|
5760
|
+
* @example Fill the window, then zoom in
|
|
5761
|
+
* The video fills the video area (cropped instead of letterboxed), then
|
|
5762
|
+
* zooms to 150 % shifted a little to the right. The first plugin to call
|
|
5763
|
+
* `view()` owns the display layer; each `set` merges into its settings,
|
|
5764
|
+
* which stay across sources until the plugin resets them or stops.
|
|
4351
5765
|
* ```ts
|
|
4352
5766
|
* const view = plugin.video.view();
|
|
4353
5767
|
* view.set({ fit: "fill" });
|
|
@@ -4388,13 +5802,20 @@ export interface RenderSettings {
|
|
|
4388
5802
|
* `unsupported`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
|
|
4389
5803
|
* is accepted but has no effect; output is always SDR.
|
|
4390
5804
|
*
|
|
4391
|
-
* @example
|
|
5805
|
+
* @example Tone-map HDR video and hide the menu where it cannot work
|
|
5806
|
+
* On a GPU-rendering player HDR video is tone-mapped with BT.2390 for a
|
|
5807
|
+
* 250-nit display; under software rendering the plugin hides its HDR menu.
|
|
5808
|
+
* `set` merges the values into the plugin's render options and the
|
|
5809
|
+
* player reports the result through `onStatus`: `failed` with
|
|
5810
|
+
* `unsupported` means the player renders in software (Windows, Linux).
|
|
4392
5811
|
* ```ts
|
|
5812
|
+
* declare function hideHdrMenu(): void;
|
|
5813
|
+
* // ---cut---
|
|
4393
5814
|
* const render = plugin.video.render();
|
|
4394
5815
|
* render.onStatus((s) => {
|
|
4395
5816
|
* if (s.state === "failed" && s.error.code === "unsupported") hideHdrMenu();
|
|
4396
5817
|
* });
|
|
4397
|
-
* render.set({ toneMapping: "bt.2390", targetPeak: 250 });
|
|
5818
|
+
* render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
|
|
4398
5819
|
* ```
|
|
4399
5820
|
*/
|
|
4400
5821
|
export interface RenderOptions extends Disposable {
|
|
@@ -4455,7 +5876,10 @@ export interface SubtitlesState {
|
|
|
4455
5876
|
/** Font family name for display. */
|
|
4456
5877
|
family: string;
|
|
4457
5878
|
}[];
|
|
4458
|
-
/**
|
|
5879
|
+
/**
|
|
5880
|
+
* The effective style: among the plugins that set a layer, the one whose
|
|
5881
|
+
* manifest id sorts last (not the most recent call); `null` without one.
|
|
5882
|
+
*/
|
|
4459
5883
|
readonly style: SubtitleStyle | null;
|
|
4460
5884
|
/** Whether the renderer confirmed the selection. */
|
|
4461
5885
|
readonly trackConfirmation: "waiting" | "applied" | "uncertain";
|
|
@@ -4475,12 +5899,23 @@ export interface SubtitleConditional {
|
|
|
4475
5899
|
* `subtitles.read`, since they read the current selection first.
|
|
4476
5900
|
*
|
|
4477
5901
|
* @example Load an SRT file as the secondary subtitle
|
|
5902
|
+
* An English SRT file shows as a second subtitle line next to the
|
|
5903
|
+
* film's own subtitles, in white with an outline. The plugin reads the
|
|
5904
|
+
* file and uploads its bytes with `load`, which adds a track to the
|
|
5905
|
+
* current source and selects it as the secondary subtitle; `refresh`
|
|
5906
|
+
* reads the subtitle state for its font list, and `setStyle` sends this
|
|
5907
|
+
* plugin's style layer naming one of those fonts.
|
|
4478
5908
|
* ```ts
|
|
5909
|
+
* import { readFile } from "node:fs/promises";
|
|
5910
|
+
* declare const path: string;
|
|
5911
|
+
* // ---cut---
|
|
4479
5912
|
* const bytes = await readFile(path);
|
|
4480
|
-
*
|
|
5913
|
+
* await plugin.subtitles.load(bytes, { title: "English", select: "secondary" });
|
|
5914
|
+
* const { fonts } = await plugin.subtitles.refresh();
|
|
5915
|
+
* if (!fonts.length) return;
|
|
4481
5916
|
* await plugin.subtitles.setStyle({
|
|
4482
|
-
* fontId:
|
|
4483
|
-
* fontSize: 40,
|
|
5917
|
+
* fontId: fonts[0]!.id,
|
|
5918
|
+
* fontSize: 40, // at the 1080p reference height
|
|
4484
5919
|
* color: "white",
|
|
4485
5920
|
* outline: 2,
|
|
4486
5921
|
* marginPercent: 5,
|
|
@@ -4524,7 +5959,12 @@ export interface Subtitles {
|
|
|
4524
5959
|
}): Promise<SubtitleTrack>;
|
|
4525
5960
|
/** Remove a track this plugin loaded. */
|
|
4526
5961
|
remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
4527
|
-
/**
|
|
5962
|
+
/**
|
|
5963
|
+
* Set this plugin's style layer; `null` removes it. The newest call of
|
|
5964
|
+
* this plugin replaces its earlier layer. When several plugins have a
|
|
5965
|
+
* layer, the player uses the one whose manifest id sorts last, whatever
|
|
5966
|
+
* the order of the calls.
|
|
5967
|
+
*/
|
|
4528
5968
|
setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
4529
5969
|
}
|
|
4530
5970
|
/**
|
|
@@ -4627,11 +6067,17 @@ export interface ExportJob extends Disposable {
|
|
|
4627
6067
|
* `subtitles.read`). Exports decode offline and do not disturb playback.
|
|
4628
6068
|
*
|
|
4629
6069
|
* @example Save the current frame as JPEG
|
|
6070
|
+
* The frame on screen, with the plugin's own picture effects, is saved
|
|
6071
|
+
* where the user chooses. `capture` freezes the source as it was in
|
|
6072
|
+
* `seen` (and fails with `stale` if the user opened another file while
|
|
6073
|
+
* the dialog was open); `export` decodes offline without disturbing
|
|
6074
|
+
* playback, and `done` resolves with the job's final status.
|
|
4630
6075
|
* ```ts
|
|
4631
6076
|
* const seen = plugin.player.state;
|
|
4632
6077
|
* const destination = await plugin.dialogs.saveFile({ fileName: "frame.jpg" });
|
|
4633
6078
|
* if (!destination) return;
|
|
4634
6079
|
* const snapshot = await plugin.media.capture({ ifCurrent: seen, processed: true });
|
|
6080
|
+
* // Release the frozen frame whatever happens to the export.
|
|
4635
6081
|
* try {
|
|
4636
6082
|
* const job = await snapshot.export({ format: "jpeg", destination, quality: 85 });
|
|
4637
6083
|
* const end = await job.done;
|
|
@@ -4691,23 +6137,28 @@ export interface WebResolverOptions {
|
|
|
4691
6137
|
* (the player shows a warning). When this plugin stops, its extensions are
|
|
4692
6138
|
* free again and such files go to the player as before.
|
|
4693
6139
|
*
|
|
4694
|
-
* @example
|
|
6140
|
+
* @example Play CHIP-8 programs with the plugin's own emulator
|
|
6141
|
+
* Opening a `.ch8` file anywhere in the player (Open dialog, playlist,
|
|
6142
|
+
* recent list) runs it in the plugin's emulator instead of failing as an
|
|
6143
|
+
* unknown format. The player calls `resolve` with the path; the plugin
|
|
6144
|
+
* reads the program and returns a stream whose `frame()` the SDK pulls on
|
|
6145
|
+
* the audio clock, one 1/60 s frame per call. `started` runs once the
|
|
6146
|
+
* stream plays; `signal` aborts the read when the open is replaced or
|
|
6147
|
+
* closed.
|
|
4695
6148
|
* ```ts
|
|
4696
|
-
*
|
|
4697
|
-
* // The plugin's own emulator: one frame per call.
|
|
4698
|
-
*
|
|
4699
|
-
*
|
|
4700
|
-
* return () => ({ video, audio: new Float32Array(1600) });
|
|
4701
|
-
* };
|
|
6149
|
+
* import { readFile } from "node:fs/promises";
|
|
6150
|
+
* // The plugin's own emulator (made up here): one frame per call.
|
|
6151
|
+
* declare function emulator(program: Uint8Array): () => StreamFrame;
|
|
6152
|
+
* // ---cut---
|
|
4702
6153
|
* plugin.resolvers.add({
|
|
4703
|
-
* id: "
|
|
4704
|
-
* extensions: ["
|
|
6154
|
+
* id: "chip8",
|
|
6155
|
+
* extensions: ["ch8"], // lower case, no dot; matches Game.CH8 too
|
|
4705
6156
|
* async resolve(path, signal) {
|
|
4706
6157
|
* const frame = emulator(await readFile(path, { signal }));
|
|
4707
6158
|
* return {
|
|
4708
6159
|
* kind: "stream",
|
|
4709
|
-
* width:
|
|
4710
|
-
* height:
|
|
6160
|
+
* width: 64, // CHIP-8's display
|
|
6161
|
+
* height: 32,
|
|
4711
6162
|
* frame,
|
|
4712
6163
|
* started: (stream) => stream.onStop((error) => error && plugin.osd.show(error.message)),
|
|
4713
6164
|
* };
|
|
@@ -4772,14 +6223,20 @@ export interface Resolver extends Disposable {
|
|
|
4772
6223
|
* files into streams this plugin generates. Needs `source.resolve` (file
|
|
4773
6224
|
* resolvers also `producer.av`).
|
|
4774
6225
|
*
|
|
4775
|
-
* @example
|
|
6226
|
+
* @example Play pages of a video site
|
|
6227
|
+
* Opening a `https://video.example.com/…` page plays its video. When the
|
|
6228
|
+
* user opens a URL whose scheme and host this resolver declares, the
|
|
6229
|
+
* player asks the plugin first; `resolve` returns the stream's address,
|
|
6230
|
+
* and `signal` aborts the lookup when the open is replaced or closed.
|
|
4776
6231
|
* ```ts
|
|
4777
6232
|
* plugin.resolvers.add({
|
|
4778
6233
|
* id: "example",
|
|
4779
6234
|
* schemes: ["https"],
|
|
4780
6235
|
* hosts: ["video.example.com"],
|
|
4781
6236
|
* async resolve(url, signal) {
|
|
4782
|
-
* const
|
|
6237
|
+
* const response = await fetch(url, { signal }); // the site's JSON for the page
|
|
6238
|
+
* if (!response.ok) throw new Error(`video.example.com answered ${response.status}`);
|
|
6239
|
+
* const page = await response.json();
|
|
4783
6240
|
* return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
|
|
4784
6241
|
* },
|
|
4785
6242
|
* });
|
|
@@ -4802,9 +6259,13 @@ export interface Resolvers {
|
|
|
4802
6259
|
* Use it to accept such files beside the player's own media types, for
|
|
4803
6260
|
* example in a playlist's drop or folder scan.
|
|
4804
6261
|
*
|
|
4805
|
-
* @example
|
|
6262
|
+
* @example Accept files that a resolver plugin can play
|
|
6263
|
+
* A plugin's own file filter also accepts types other plugins turn into
|
|
6264
|
+
* streams. `fileTypes` lists the extensions every running file resolver
|
|
6265
|
+
* claims (lower case, no dot); the SDK keeps it current with `events`.
|
|
4806
6266
|
* ```ts
|
|
4807
6267
|
* const MEDIA = ["mp4", "mkv", "mp3"];
|
|
6268
|
+
* // Extensions compare in lower case, without the dot.
|
|
4808
6269
|
* const accepted = (path: string) => {
|
|
4809
6270
|
* const extension = path.split(".").pop()?.toLowerCase() ?? "";
|
|
4810
6271
|
* return MEDIA.includes(extension) || plugin.resolvers.fileTypes.includes(extension);
|
|
@@ -4823,10 +6284,13 @@ export interface Resolvers {
|
|
|
4823
6284
|
* @param listener - Receives the new list.
|
|
4824
6285
|
* @returns A handle that removes the listener.
|
|
4825
6286
|
*
|
|
4826
|
-
* @example
|
|
6287
|
+
* @example Say which file types became playable
|
|
6288
|
+
* When a plugin with a file resolver starts or stops, the OSD lists the
|
|
6289
|
+
* extra file types now accepted. The listener (which needs `events`)
|
|
6290
|
+
* receives the new list each time it changes.
|
|
4827
6291
|
* ```ts
|
|
4828
6292
|
* plugin.resolvers.onFileTypesChange((types) => {
|
|
4829
|
-
* void plugin.osd.show(
|
|
6293
|
+
* void plugin.osd.show("Also accepted: {}", [plugin.i18n.list(types)]);
|
|
4830
6294
|
* });
|
|
4831
6295
|
* ```
|
|
4832
6296
|
*
|
|
@@ -5049,10 +6513,19 @@ export interface PluginWarning {
|
|
|
5049
6513
|
* `plugin.packages`: the plugins the player knows. `list` and `onChange`
|
|
5050
6514
|
* need no capability; everything else needs `plugins.manage`.
|
|
5051
6515
|
*
|
|
5052
|
-
* @example
|
|
6516
|
+
* @example Install a plugin from an unpacked package
|
|
6517
|
+
* The plugin installs a package directory it downloaded and unpacked, and
|
|
6518
|
+
* reports why when it does not work. `validate` asks the player to check
|
|
6519
|
+
* the package and returns a candidate; `install` starts the change and
|
|
6520
|
+
* `done` resolves with its final status: `committed` only once the new
|
|
6521
|
+
* plugin reported that it is running, otherwise `failed`, `cancelled`,
|
|
6522
|
+
* `failedDegraded` or `tooLate`.
|
|
5053
6523
|
* ```ts
|
|
6524
|
+
* declare const unpackedDirectory: string;
|
|
6525
|
+
* // ---cut---
|
|
5054
6526
|
* const candidate = await plugin.packages.validate(unpackedDirectory);
|
|
5055
6527
|
* const change = await candidate.install();
|
|
6528
|
+
* // `message` says why a change did not commit.
|
|
5056
6529
|
* if ((await change.done) !== "committed") throw new Error(change.message ?? "install failed");
|
|
5057
6530
|
* ```
|
|
5058
6531
|
*/
|
|
@@ -5165,7 +6638,11 @@ export interface BindingInfo extends BindingRef {
|
|
|
5165
6638
|
* `plugin.bindings`: list and override every command's bindings, including
|
|
5166
6639
|
* the player's. Needs `bindings.manage`.
|
|
5167
6640
|
*
|
|
5168
|
-
* @example
|
|
6641
|
+
* @example Unbind the player's Mute key, then restore it
|
|
6642
|
+
* The player's Mute command loses its default key, and later gets it
|
|
6643
|
+
* back. `set` with `[]` saves an override that unbinds the command
|
|
6644
|
+
* (remembered across launches); `reset` removes the override, so the
|
|
6645
|
+
* command's own default bindings apply again.
|
|
5169
6646
|
* ```ts
|
|
5170
6647
|
* await plugin.bindings.set({ plugin: "host", command: "mute" }, []);
|
|
5171
6648
|
* // later
|
|
@@ -5205,20 +6682,122 @@ export interface Bindings {
|
|
|
5205
6682
|
* PNG. Other systems: `unsupported`. Failures: `invalid`, `missing`,
|
|
5206
6683
|
* `unavailable`, `failed` (the system refused).
|
|
5207
6684
|
*
|
|
5208
|
-
* @example
|
|
6685
|
+
* @example Copy the current file and position
|
|
6686
|
+
* The clipboard holds "path @ seconds" for pasting into a note. The
|
|
6687
|
+
* player writes the system clipboard; it rejects with `unsupported` on
|
|
6688
|
+
* systems other than macOS and Windows and `failed` when the system
|
|
6689
|
+
* refuses, which the plugin reports.
|
|
5209
6690
|
* ```ts
|
|
5210
|
-
*
|
|
6691
|
+
* const { source, position } = plugin.player.state;
|
|
6692
|
+
* try {
|
|
6693
|
+
* await plugin.clipboard.writeText(`${source} @ ${position}`); // data, not shown
|
|
6694
|
+
* } catch (error) {
|
|
6695
|
+
* void plugin.osd.show("Could not copy: {}", [(error as PluginError).code]);
|
|
6696
|
+
* }
|
|
5211
6697
|
* ```
|
|
6698
|
+
*
|
|
6699
|
+
* @remarks
|
|
6700
|
+
* The player reads and writes the system clipboard on the plugin's behalf.
|
|
6701
|
+
* Text is plain text; images are written from a PNG or JPEG file.
|
|
6702
|
+
*
|
|
6703
|
+
* @capability clipboard
|
|
6704
|
+
* @since 1.0.0
|
|
6705
|
+
* @category Services
|
|
5212
6706
|
*/
|
|
5213
6707
|
export interface Clipboard {
|
|
5214
|
-
/**
|
|
6708
|
+
/**
|
|
6709
|
+
* Plain text on the clipboard; `null` when it holds none.
|
|
6710
|
+
*
|
|
6711
|
+
* @capability clipboard
|
|
6712
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
6713
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
6714
|
+
* @throws `failed` (rejects) The system refused (Windows).
|
|
6715
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6716
|
+
* `clipboard`.
|
|
6717
|
+
*
|
|
6718
|
+
* @example Open a URL the user copied
|
|
6719
|
+
* A "Paste and play" command opens the address on the clipboard. `readText`
|
|
6720
|
+
* resolves with the clipboard's text, or `null` when it holds none (an
|
|
6721
|
+
* image, nothing).
|
|
6722
|
+
* ```ts
|
|
6723
|
+
* const text = await plugin.clipboard.readText();
|
|
6724
|
+
* if (text?.startsWith("https://")) await plugin.player.open(text.trim());
|
|
6725
|
+
* ```
|
|
6726
|
+
*
|
|
6727
|
+
* @since 1.0.0
|
|
6728
|
+
*/
|
|
5215
6729
|
readText(): Promise<string | null>;
|
|
5216
|
-
/**
|
|
6730
|
+
/**
|
|
6731
|
+
* Replace the clipboard with text (at most 1 MiB of UTF-8).
|
|
6732
|
+
*
|
|
6733
|
+
* @capability clipboard
|
|
6734
|
+
* @throws `invalid` (rejects) More than 1 MiB of UTF-8.
|
|
6735
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
6736
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
6737
|
+
* @throws `failed` (rejects) The system refused.
|
|
6738
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6739
|
+
* `clipboard`.
|
|
6740
|
+
*
|
|
6741
|
+
* @example Copy the playing file's path
|
|
6742
|
+
* The path of the playing file is put on the clipboard, replacing what was
|
|
6743
|
+
* there. It resolves once the system has it.
|
|
6744
|
+
* ```ts
|
|
6745
|
+
* const { source } = plugin.player.state;
|
|
6746
|
+
* if (source) await plugin.clipboard.writeText(source);
|
|
6747
|
+
* ```
|
|
6748
|
+
*
|
|
6749
|
+
* @since 1.0.0
|
|
6750
|
+
*/
|
|
5217
6751
|
writeText(text: string): Promise<void>;
|
|
5218
|
-
/**
|
|
6752
|
+
/**
|
|
6753
|
+
* Replace the clipboard with a PNG or JPEG file (absolute path, at most 16 MB).
|
|
6754
|
+
*
|
|
6755
|
+
* @capability clipboard
|
|
6756
|
+
* @throws `missing` (rejects) The file does not exist.
|
|
6757
|
+
* @throws `invalid` (rejects) Not an absolute path, not PNG or JPEG, over
|
|
6758
|
+
* 16 MB, or (Windows) an image the system cannot decode.
|
|
6759
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
6760
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
6761
|
+
* @throws `failed` (rejects) The system refused.
|
|
6762
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6763
|
+
* `clipboard`.
|
|
6764
|
+
*
|
|
6765
|
+
* @example Copy a snapshot to the clipboard
|
|
6766
|
+
* A frame the plugin saved as PNG is put on the clipboard, ready to paste
|
|
6767
|
+
* into another application. The player reads the file and writes the
|
|
6768
|
+
* image (macOS: its original bytes; Windows: a bitmap plus PNG).
|
|
6769
|
+
* ```ts
|
|
6770
|
+
* declare const pngPath: string;
|
|
6771
|
+
* // ---cut---
|
|
6772
|
+
* await plugin.clipboard.writeImage(pngPath); // absolute path, at most 16 MB
|
|
6773
|
+
* await plugin.osd.show("Snapshot copied");
|
|
6774
|
+
* ```
|
|
6775
|
+
*
|
|
6776
|
+
* @since 1.0.0
|
|
6777
|
+
*/
|
|
5219
6778
|
writeImage(path: string): Promise<void>;
|
|
5220
6779
|
}
|
|
5221
|
-
/**
|
|
6780
|
+
/**
|
|
6781
|
+
* Keeping the computer awake (`playback.control`).
|
|
6782
|
+
*
|
|
6783
|
+
* @remarks
|
|
6784
|
+
* A request, not a command: the player keeps the system awake while any
|
|
6785
|
+
* plugin asks for it and a source plays, and withdraws a plugin's request
|
|
6786
|
+
* when it stops.
|
|
6787
|
+
*
|
|
6788
|
+
* @capability playback.control
|
|
6789
|
+
*
|
|
6790
|
+
* @example Keep the display on while playing
|
|
6791
|
+
* The screen does not dim or sleep while a video plays, and may again when
|
|
6792
|
+
* it is paused or ends. The player takes the system's assertion only while
|
|
6793
|
+
* a source is ready and not paused.
|
|
6794
|
+
* ```ts
|
|
6795
|
+
* await plugin.power.preventSleep("playing"); // "off" withdraws the request
|
|
6796
|
+
* ```
|
|
6797
|
+
*
|
|
6798
|
+
* @since 1.0.0
|
|
6799
|
+
* @category Services
|
|
6800
|
+
*/
|
|
5222
6801
|
export interface Power {
|
|
5223
6802
|
/**
|
|
5224
6803
|
* `"playing"`: while a source is ready and not paused, keep the display
|
|
@@ -5229,10 +6808,32 @@ export interface Power {
|
|
|
5229
6808
|
* take the system assertion are logged by the player, not reported.
|
|
5230
6809
|
* macOS uses `caffeinate`, Windows the thread execution state; elsewhere
|
|
5231
6810
|
* it has no effect.
|
|
6811
|
+
*
|
|
6812
|
+
* @capability playback.control
|
|
6813
|
+
* @throws `invalid` (rejects) A mode other than `"off"` or `"playing"`.
|
|
6814
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
6815
|
+
* `playback.control`.
|
|
6816
|
+
*
|
|
6817
|
+
* @example Follow the plugin's own "Keep awake" setting
|
|
6818
|
+
* The plugin keeps the display awake during playback only while its
|
|
6819
|
+
* setting is on. Each call replaces this plugin's request; `"off"`
|
|
6820
|
+
* withdraws it, and other plugins' requests still count.
|
|
6821
|
+
* ```ts
|
|
6822
|
+
* const store = plugin.storage.json("power", { defaults: { keepAwake: true } });
|
|
6823
|
+
* await plugin.power.preventSleep(store.value.keepAwake ? "playing" : "off");
|
|
6824
|
+
* ```
|
|
6825
|
+
*
|
|
6826
|
+
* @since 1.0.0
|
|
5232
6827
|
*/
|
|
5233
6828
|
preventSleep(mode: "off" | "playing"): Promise<void>;
|
|
5234
6829
|
}
|
|
5235
|
-
/**
|
|
6830
|
+
/**
|
|
6831
|
+
* The output of {@link Tools.run}.
|
|
6832
|
+
*
|
|
6833
|
+
* @see {@link Tools.run}
|
|
6834
|
+
* @since 1.0.0
|
|
6835
|
+
* @category Services
|
|
6836
|
+
*/
|
|
5236
6837
|
export interface ToolResult {
|
|
5237
6838
|
/** Standard output (at most `maxOutput` bytes). */
|
|
5238
6839
|
stdout: Uint8Array;
|
|
@@ -5243,22 +6844,42 @@ export interface ToolResult {
|
|
|
5243
6844
|
* `plugin.tools`: ffmpeg and ffprobe, run in this plugin's process. No
|
|
5244
6845
|
* capability.
|
|
5245
6846
|
*
|
|
5246
|
-
* @example
|
|
6847
|
+
* @example Read a file's format with ffprobe
|
|
6848
|
+
* The plugin gets a media file's container details as JSON. `available`
|
|
6849
|
+
* says whether ffprobe can be found (`VESTRA_FFPROBE`, then `PATH`); `run`
|
|
6850
|
+
* starts it from the plugin's own process without a shell and resolves
|
|
6851
|
+
* with its output as bytes once it exits, or rejects when it fails
|
|
6852
|
+
* (`failed`, with the last stderr line) or takes longer than `timeoutMs`.
|
|
5247
6853
|
* ```ts
|
|
6854
|
+
* declare const path: string;
|
|
6855
|
+
* // ---cut---
|
|
5248
6856
|
* if (await plugin.tools.available("ffprobe")) {
|
|
5249
6857
|
* const { stdout } = await plugin.tools.run(
|
|
5250
6858
|
* "ffprobe",
|
|
5251
6859
|
* ["-v", "error", "-show_format", "-of", "json", path],
|
|
5252
|
-
* { timeoutMs: 10_000 },
|
|
6860
|
+
* { timeoutMs: 10_000 }, // give up on a stuck file after 10 s
|
|
5253
6861
|
* );
|
|
5254
6862
|
* const info = JSON.parse(new TextDecoder().decode(stdout));
|
|
6863
|
+
* void info.format; // container, duration, bit rate…
|
|
5255
6864
|
* }
|
|
5256
6865
|
* ```
|
|
6866
|
+
*
|
|
6867
|
+
* @remarks
|
|
6868
|
+
* ffmpeg and ffprobe run as child processes of the plugin. When
|
|
6869
|
+
* `VESTRA_FFMPEG` / `VESTRA_FFPROBE` is set, only that path is used (an
|
|
6870
|
+
* invalid one means the tool is not found); otherwise the tool is searched
|
|
6871
|
+
* on `PATH`. A found path is kept for the plugin's run, a miss is looked up
|
|
6872
|
+
* again next time. Disposing the
|
|
6873
|
+
* scope ends a running process.
|
|
6874
|
+
*
|
|
6875
|
+
* @capability None.
|
|
6876
|
+
* @since 1.0.0
|
|
6877
|
+
* @category Services
|
|
5257
6878
|
*/
|
|
5258
6879
|
export interface Tools {
|
|
5259
6880
|
/**
|
|
5260
|
-
* Run ffmpeg or ffprobe (
|
|
5261
|
-
*
|
|
6881
|
+
* Run ffmpeg or ffprobe (`VESTRA_FFMPEG` / `VESTRA_FFPROBE` when set, else
|
|
6882
|
+
* found on `PATH`) without a shell. Rejects `unavailable` (not found), `timeout`,
|
|
5262
6883
|
* `failed` (non-zero exit; `message` is the last stderr line), `invalid`
|
|
5263
6884
|
* (output over `maxOutput`, default 16 MiB), `aborted` (`signal`). Timeout,
|
|
5264
6885
|
* `signal` and disposing the scope end the process.
|
|
@@ -5266,28 +6887,171 @@ export interface Tools {
|
|
|
5266
6887
|
* Arguments are passed as-is (no quoting needed). On Windows `PATHEXT` is
|
|
5267
6888
|
* honoured. Ending sends SIGTERM, then SIGKILL after 300 ms (immediate on
|
|
5268
6889
|
* Windows).
|
|
6890
|
+
*
|
|
6891
|
+
* @capability None.
|
|
6892
|
+
* @throws `unavailable` (rejects) The tool was not found.
|
|
6893
|
+
* @throws `timeout` (rejects) It ran longer than `timeoutMs`; the process is
|
|
6894
|
+
* ended.
|
|
6895
|
+
* @throws `failed` (rejects) A non-zero exit; `message` is the last stderr
|
|
6896
|
+
* line.
|
|
6897
|
+
* @throws `invalid` (rejects) Bad arguments or options, or output over
|
|
6898
|
+
* `maxOutput`.
|
|
6899
|
+
* @throws `aborted` (rejects) `signal` aborted, or the scope was disposed.
|
|
6900
|
+
*
|
|
6901
|
+
* @example Cut a clip without re-encoding
|
|
6902
|
+
* The plugin writes the first ten seconds of the file to a new file. `run`
|
|
6903
|
+
* starts ffmpeg without a shell (arguments pass as they are, no quoting)
|
|
6904
|
+
* and resolves with its output once it exits with 0; a failure rejects
|
|
6905
|
+
* with the last line ffmpeg printed.
|
|
6906
|
+
* ```ts
|
|
6907
|
+
* declare const input: string;
|
|
6908
|
+
* declare const output: string;
|
|
6909
|
+
* // ---cut---
|
|
6910
|
+
* await plugin.tools.run(
|
|
6911
|
+
* "ffmpeg",
|
|
6912
|
+
* ["-y", "-i", input, "-t", "10", "-c", "copy", output],
|
|
6913
|
+
* { timeoutMs: 60_000 }, // give up after a minute
|
|
6914
|
+
* );
|
|
6915
|
+
* ```
|
|
6916
|
+
*
|
|
6917
|
+
* @since 1.0.0
|
|
5269
6918
|
*/
|
|
5270
6919
|
run(tool: "ffmpeg" | "ffprobe", args: string[], options: {
|
|
5271
6920
|
timeoutMs: number;
|
|
5272
6921
|
maxOutput?: number;
|
|
5273
6922
|
signal?: AbortSignal;
|
|
5274
6923
|
}): Promise<ToolResult>;
|
|
5275
|
-
/**
|
|
6924
|
+
/**
|
|
6925
|
+
* Whether the tool can be found.
|
|
6926
|
+
*
|
|
6927
|
+
* @capability None.
|
|
6928
|
+
* @throws Never; it resolves `false` when the tool cannot be found.
|
|
6929
|
+
*
|
|
6930
|
+
* @example Grey out a command when ffmpeg is missing
|
|
6931
|
+
* The Export item is disabled with a reason when ffmpeg is not installed.
|
|
6932
|
+
* `available` looks the tool up (a found path is kept, a miss is looked
|
|
6933
|
+
* up again on the next call, so installing it later is noticed).
|
|
6934
|
+
* ```ts
|
|
6935
|
+
* const hasFfmpeg = await plugin.tools.available("ffmpeg");
|
|
6936
|
+
* plugin.commands.add({
|
|
6937
|
+
* id: "export",
|
|
6938
|
+
* path: ["@video", "Export…"],
|
|
6939
|
+
* enabled: hasFfmpeg,
|
|
6940
|
+
* // Shown with the greyed-out item.
|
|
6941
|
+
* reason: hasFfmpeg ? null : "Install ffmpeg to export",
|
|
6942
|
+
* run() {},
|
|
6943
|
+
* });
|
|
6944
|
+
* ```
|
|
6945
|
+
*
|
|
6946
|
+
* @since 1.0.0
|
|
6947
|
+
*/
|
|
5276
6948
|
available(tool: "ffmpeg" | "ffprobe"): Promise<boolean>;
|
|
5277
6949
|
}
|
|
5278
|
-
/**
|
|
6950
|
+
/**
|
|
6951
|
+
* A JSON file `dataDirectory/<name>.json`, loaded once and saved debounced.
|
|
6952
|
+
*
|
|
6953
|
+
* @remarks
|
|
6954
|
+
* One JSON file, `<dataDirectory>/<name>.json`, made by
|
|
6955
|
+
* {@link Storage.json}. `value` is read once when the store is made;
|
|
6956
|
+
* `save` replaces it in memory at once and writes the file `debounceMs`
|
|
6957
|
+
* later (300 by default) through a temporary file and a rename, so a
|
|
6958
|
+
* crash never leaves half a file. Writes to one file run one at a time,
|
|
6959
|
+
* newest last. Disposing the store, its scope or stopping the plugin
|
|
6960
|
+
* writes a pending save first.
|
|
6961
|
+
*
|
|
6962
|
+
* @capability None.
|
|
6963
|
+
*
|
|
6964
|
+
* @example Count how often the plugin was started
|
|
6965
|
+
* The counter grows by one at every start and is kept in `stats.json`. The
|
|
6966
|
+
* store reads the file when it is made (the defaults when there is none),
|
|
6967
|
+
* and `save` writes the new count a moment later.
|
|
6968
|
+
* ```ts
|
|
6969
|
+
* const stats = plugin.storage.json("stats", { defaults: { starts: 0 } });
|
|
6970
|
+
* stats.save({ starts: stats.value.starts + 1 }); // written 300 ms later
|
|
6971
|
+
* ```
|
|
6972
|
+
*
|
|
6973
|
+
* @since 1.0.0
|
|
6974
|
+
* @category Services
|
|
6975
|
+
*/
|
|
5279
6976
|
export interface JsonStore<T> extends Disposable {
|
|
5280
6977
|
/** The loaded value, or `defaults` when the file was missing or broken. */
|
|
5281
6978
|
readonly value: T;
|
|
5282
6979
|
/**
|
|
5283
6980
|
* Replace the value now and write it after `debounceMs`.
|
|
5284
6981
|
* Only `save` writes: mutating `value` in place is not saved.
|
|
6982
|
+
*
|
|
6983
|
+
* @capability None.
|
|
6984
|
+
*
|
|
6985
|
+
* @example Save a slider value without writing on every move
|
|
6986
|
+
* Dragging the plugin's volume slider updates the stored value at once but
|
|
6987
|
+
* writes the file only once the user stops for 300 ms. Each `save`
|
|
6988
|
+
* replaces the value and restarts the debounce timer.
|
|
6989
|
+
* ```ts
|
|
6990
|
+
* declare const window: Window;
|
|
6991
|
+
* // ---cut---
|
|
6992
|
+
* const store = plugin.storage.json("prefs", { defaults: { volume: 100 } });
|
|
6993
|
+
* window.on("volumeChanged", (volume: number) => store.save({ ...store.value, volume }));
|
|
6994
|
+
* ```
|
|
6995
|
+
*
|
|
6996
|
+
* @since 1.0.0
|
|
5285
6997
|
*/
|
|
5286
6998
|
save(next: T): void;
|
|
5287
|
-
/**
|
|
6999
|
+
/**
|
|
7000
|
+
* Write a pending save now.
|
|
7001
|
+
*
|
|
7002
|
+
* @capability None.
|
|
7003
|
+
* @throws `Error` (rejects) Node's file system error when the file cannot
|
|
7004
|
+
* be written; its `code` is Node's (`ENOSPC`, `EACCES` and so on), not a
|
|
7005
|
+
* player error code.
|
|
7006
|
+
*
|
|
7007
|
+
* @example Write the file now before copying it
|
|
7008
|
+
* Before exporting its settings, the plugin makes sure the file on disk has
|
|
7009
|
+
* the latest value. `flush` writes a pending save at once (or waits for
|
|
7010
|
+
* the write in progress) and resolves when the file is written.
|
|
7011
|
+
* ```ts
|
|
7012
|
+
* import { copyFile } from "node:fs/promises";
|
|
7013
|
+
* import { join } from "node:path";
|
|
7014
|
+
* declare const exportPath: string;
|
|
7015
|
+
* // ---cut---
|
|
7016
|
+
* const store = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
7017
|
+
* await store.flush(); // the file now has the newest value
|
|
7018
|
+
* await copyFile(join(plugin.dataDirectory, "settings.json"), exportPath);
|
|
7019
|
+
* ```
|
|
7020
|
+
*
|
|
7021
|
+
* @since 1.0.0
|
|
7022
|
+
*/
|
|
5288
7023
|
flush(): Promise<void>;
|
|
5289
7024
|
}
|
|
5290
|
-
/**
|
|
7025
|
+
/**
|
|
7026
|
+
* `plugin.storage`: JSON settings files. No capability.
|
|
7027
|
+
*
|
|
7028
|
+
* @remarks
|
|
7029
|
+
* Two kinds of storage, both private to the plugin and needing no
|
|
7030
|
+
* capability. {@link Storage.json} keeps settings in JSON files in the
|
|
7031
|
+
* plugin's data directory: they last across player restarts and upgrades,
|
|
7032
|
+
* and are written by the plugin's own process. {@link Storage.session}
|
|
7033
|
+
* keeps values in the player's memory for one player run: they survive a
|
|
7034
|
+
* restart of the plugin's process but not of the player. Secrets belong in
|
|
7035
|
+
* {@link Secrets}, never in either.
|
|
7036
|
+
*
|
|
7037
|
+
* @capability None.
|
|
7038
|
+
*
|
|
7039
|
+
* @example Keep a setting across restarts and a queue for this run
|
|
7040
|
+
* The plugin's step size survives a restart of the player; its work queue
|
|
7041
|
+
* survives a reload of the plugin but starts empty with the next player
|
|
7042
|
+
* run. The JSON store reads its file at once and writes it shortly after
|
|
7043
|
+
* each `save`; session values live in the player process.
|
|
7044
|
+
* ```ts
|
|
7045
|
+
* const settings = plugin.storage.json("settings", { defaults: { step: 5 } });
|
|
7046
|
+
* const queue = (await plugin.storage.session.get<string[]>("queue")) ?? [];
|
|
7047
|
+
* void settings.value.step; // read synchronously, from the file or the defaults
|
|
7048
|
+
* void queue;
|
|
7049
|
+
* ```
|
|
7050
|
+
*
|
|
7051
|
+
* @see [Store a plugin's data](../../docs/how-to/store-data.md), [Remember a window's state between runs](../../docs/how-to/window-state.md)
|
|
7052
|
+
* @since 1.0.0
|
|
7053
|
+
* @category Services
|
|
7054
|
+
*/
|
|
5291
7055
|
export interface Storage {
|
|
5292
7056
|
/**
|
|
5293
7057
|
* Open `<name>.json` (`name`: 1–64 of `a-z 0-9 -`). A file that cannot be
|
|
@@ -5298,16 +7062,29 @@ export interface Storage {
|
|
|
5298
7062
|
* The file is read synchronously on creation; `debounceMs` defaults to
|
|
5299
7063
|
* 300. Files are created with mode 0600. `invalid` for a bad name.
|
|
5300
7064
|
*
|
|
5301
|
-
* @example
|
|
7065
|
+
* @example Keep settings and a recent list in a JSON file
|
|
7066
|
+
* The plugin remembers whether to resume and the last ten files across
|
|
7067
|
+
* restarts. `json` reads `settings.json` from the data directory at once
|
|
7068
|
+
* (or uses `defaults` when it is missing or fails `validate`); `save`
|
|
7069
|
+
* replaces the value and writes the file 300 ms later, and stopping the
|
|
7070
|
+
* plugin writes a pending save first.
|
|
5302
7071
|
* ```ts
|
|
7072
|
+
* declare const path: string;
|
|
7073
|
+
* // ---cut---
|
|
5303
7074
|
* type Settings = { resume: boolean; recent: string[] };
|
|
5304
7075
|
* const store = plugin.storage.json<Settings>("settings", {
|
|
5305
7076
|
* defaults: { resume: true, recent: [] },
|
|
5306
7077
|
* validate: (v): v is Settings =>
|
|
5307
7078
|
* typeof v === "object" && v !== null && typeof (v as Settings).resume === "boolean",
|
|
5308
7079
|
* });
|
|
7080
|
+
* // Only `save` writes: changing `store.value` in place is not saved.
|
|
5309
7081
|
* store.save({ ...store.value, recent: [path, ...store.value.recent].slice(0, 10) });
|
|
5310
7082
|
* ```
|
|
7083
|
+
*
|
|
7084
|
+
* @capability None.
|
|
7085
|
+
* @throws `invalid` (throws) A bad `name`, or a `debounceMs` that is not a
|
|
7086
|
+
* non-negative number.
|
|
7087
|
+
* @since 1.0.0
|
|
5311
7088
|
*/
|
|
5312
7089
|
json<T>(name: string, options: {
|
|
5313
7090
|
defaults: T;
|
|
@@ -5367,7 +7144,11 @@ export interface Storage {
|
|
|
5367
7144
|
* resolves, so a crash right after loses nothing; there is no debounce.
|
|
5368
7145
|
* Calls answer in order.
|
|
5369
7146
|
*
|
|
5370
|
-
* @example
|
|
7147
|
+
* @example Keep a work queue across a plugin restart
|
|
7148
|
+
* A download queue survives the plugin being reloaded or crashing, and is
|
|
7149
|
+
* empty again when the player starts next time. Session storage lives in
|
|
7150
|
+
* the player process for one player run; each `set` is kept once its
|
|
7151
|
+
* promise resolves, so nothing is lost if the plugin dies right after.
|
|
5371
7152
|
* ```ts
|
|
5372
7153
|
* // A queue the plugin is working through: after a reload or a crash it
|
|
5373
7154
|
* // goes on where it stopped, after the player quits it starts afresh.
|
|
@@ -5377,30 +7158,128 @@ export interface Storage {
|
|
|
5377
7158
|
* ```
|
|
5378
7159
|
*
|
|
5379
7160
|
* @since 1.5.25
|
|
7161
|
+
*
|
|
7162
|
+
* @remarks
|
|
7163
|
+
* The player keeps the values in its own memory, per plugin, until it
|
|
7164
|
+
* quits; the plugin's process may restart in between. Each call is one
|
|
7165
|
+
* request to the player, answered in order, with no debounce.
|
|
7166
|
+
*
|
|
7167
|
+
* @capability None.
|
|
7168
|
+
* @category Services
|
|
5380
7169
|
*/
|
|
5381
7170
|
export interface SessionStorage {
|
|
5382
|
-
/**
|
|
7171
|
+
/**
|
|
7172
|
+
* The value stored under `key`, or `undefined` when there is none.
|
|
7173
|
+
*
|
|
7174
|
+
* @capability None.
|
|
7175
|
+
* @throws `invalid` (rejects) A bad key (empty, over 1024 bytes, with NUL
|
|
7176
|
+
* or a lone surrogate).
|
|
7177
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
7178
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
7179
|
+
*
|
|
7180
|
+
* @example Pick up where the plugin stopped after a reload
|
|
7181
|
+
* After a reload the plugin resumes the item it was working on. `get`
|
|
7182
|
+
* asks the player and resolves with the value parsed from JSON, or
|
|
7183
|
+
* `undefined` when nothing is stored; the value is not checked, so its
|
|
7184
|
+
* shape is checked here.
|
|
7185
|
+
* ```ts
|
|
7186
|
+
* const current = await plugin.storage.session.get<{ url: string }>("current");
|
|
7187
|
+
* if (typeof current?.url === "string") await plugin.osd.show("Resuming {}", [current.url]);
|
|
7188
|
+
* ```
|
|
7189
|
+
*
|
|
7190
|
+
* @since 1.5.25
|
|
7191
|
+
*/
|
|
5383
7192
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
5384
7193
|
/**
|
|
5385
7194
|
* Store or replace `value` under `key`. `invalid` for a bad key or a value
|
|
5386
7195
|
* that is not JSON, `limit` when the plugin's values would be over 1 MiB
|
|
5387
7196
|
* or the request over the 1 MiB message limit.
|
|
7197
|
+
*
|
|
7198
|
+
* @capability None.
|
|
7199
|
+
* @throws `invalid` (rejects) A bad key, or a value JSON cannot hold (or a
|
|
7200
|
+
* string with a lone surrogate).
|
|
7201
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
7202
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
7203
|
+
* @throws `limit` (rejects) The plugin's values would pass 1 MiB, or the
|
|
7204
|
+
* request the 1 MiB message limit; nothing changes.
|
|
7205
|
+
*
|
|
7206
|
+
* @example Remember the item being worked on
|
|
7207
|
+
* The plugin records the URL it is processing, so a reload can continue
|
|
7208
|
+
* with it. The value is stored as `JSON.stringify` writes it, and is kept
|
|
7209
|
+
* once the promise resolves.
|
|
7210
|
+
* ```ts
|
|
7211
|
+
* await plugin.storage.session.set("current", { url: "https://example.com/a.mkv" });
|
|
7212
|
+
* ```
|
|
7213
|
+
*
|
|
7214
|
+
* @since 1.5.25
|
|
5388
7215
|
*/
|
|
5389
7216
|
set(key: string, value: unknown): Promise<void>;
|
|
5390
|
-
/**
|
|
7217
|
+
/**
|
|
7218
|
+
* Remove a key; removing a missing one is fine.
|
|
7219
|
+
*
|
|
7220
|
+
* @capability None.
|
|
7221
|
+
* @throws `invalid` (rejects) A bad key.
|
|
7222
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
7223
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
7224
|
+
*
|
|
7225
|
+
* @example Forget the item once it is done
|
|
7226
|
+
* When the work finishes, the plugin removes the entry so that a later
|
|
7227
|
+
* reload does not resume it. Removing a key that is not there is fine.
|
|
7228
|
+
* ```ts
|
|
7229
|
+
* await plugin.storage.session.delete("current");
|
|
7230
|
+
* ```
|
|
7231
|
+
*
|
|
7232
|
+
* @since 1.5.25
|
|
7233
|
+
*/
|
|
5391
7234
|
delete(key: string): Promise<void>;
|
|
5392
|
-
/**
|
|
7235
|
+
/**
|
|
7236
|
+
* The keys stored, in code point order.
|
|
7237
|
+
*
|
|
7238
|
+
* @capability None.
|
|
7239
|
+
* @throws `disabled` (rejects) The plugin is stopping.
|
|
7240
|
+
* @throws `unsupported` (rejects) A player before 0.5.5.
|
|
7241
|
+
*
|
|
7242
|
+
* @example Clear everything the plugin stored for this run
|
|
7243
|
+
* The plugin starts its session state over. `keys` lists the plugin's own
|
|
7244
|
+
* keys in code point order; each `delete` is a request of its own.
|
|
7245
|
+
* ```ts
|
|
7246
|
+
* const session = plugin.storage.session;
|
|
7247
|
+
* for (const key of await session.keys()) await session.delete(key);
|
|
7248
|
+
* ```
|
|
7249
|
+
*
|
|
7250
|
+
* @since 1.5.25
|
|
7251
|
+
*/
|
|
5393
7252
|
keys(): Promise<string[]>;
|
|
5394
7253
|
}
|
|
5395
7254
|
/**
|
|
5396
7255
|
* `plugin.system`: files and links, executed by the player. No capability.
|
|
5397
7256
|
* Other systems than macOS and Windows: `unsupported`.
|
|
5398
7257
|
*
|
|
5399
|
-
* @example
|
|
7258
|
+
* @example Show an exported file and open the help page
|
|
7259
|
+
* The file manager opens with the exported file selected, then the
|
|
7260
|
+
* browser opens the plugin's help page. The player performs both on the
|
|
7261
|
+
* plugin's behalf; they reject with `unsupported` on systems other than
|
|
7262
|
+
* macOS and Windows, which the plugin turns into a message.
|
|
5400
7263
|
* ```ts
|
|
5401
|
-
*
|
|
5402
|
-
*
|
|
7264
|
+
* declare const exportedFile: string;
|
|
7265
|
+
* // ---cut---
|
|
7266
|
+
* try {
|
|
7267
|
+
* await plugin.system.reveal(exportedFile);
|
|
7268
|
+
* await plugin.system.openUrl("https://example.com/help");
|
|
7269
|
+
* } catch (error) {
|
|
7270
|
+
* if ((error as PluginError).code !== "unsupported") throw error;
|
|
7271
|
+
* await plugin.osd.show(`Saved to ${exportedFile}`); // no file manager to open
|
|
7272
|
+
* }
|
|
5403
7273
|
* ```
|
|
7274
|
+
*
|
|
7275
|
+
* @remarks
|
|
7276
|
+
* The player opens files, folders and links with the system's own
|
|
7277
|
+
* applications for the plugin, so the plugin never runs `open`,
|
|
7278
|
+
* `explorer` or a shell itself.
|
|
7279
|
+
*
|
|
7280
|
+
* @capability None.
|
|
7281
|
+
* @since 1.0.0
|
|
7282
|
+
* @category Services
|
|
5404
7283
|
*/
|
|
5405
7284
|
export interface System {
|
|
5406
7285
|
/**
|
|
@@ -5408,13 +7287,66 @@ export interface System {
|
|
|
5408
7287
|
* for all, so several files of one folder are all highlighted; Windows:
|
|
5409
7288
|
* one Explorer window per folder, selecting its first path). Paths that do
|
|
5410
7289
|
* not exist are skipped; `missing` when none exists; at most 256.
|
|
7290
|
+
*
|
|
7291
|
+
* @capability None.
|
|
7292
|
+
* @throws `missing` (rejects) None of the paths exists.
|
|
7293
|
+
* @throws `invalid` (rejects) No paths, or more than 256.
|
|
7294
|
+
* @throws `failed` (rejects) The system refused.
|
|
7295
|
+
* @throws `timeout` (rejects) macOS did not answer within 10 s.
|
|
7296
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
7297
|
+
*
|
|
7298
|
+
* @example Show the exported files in the file manager
|
|
7299
|
+
* After an export, the file manager opens with the new files selected.
|
|
7300
|
+
* Paths that no longer exist are skipped; the call rejects with `missing`
|
|
7301
|
+
* only when none of them exists.
|
|
7302
|
+
* ```ts
|
|
7303
|
+
* declare const exported: string[];
|
|
7304
|
+
* // ---cut---
|
|
7305
|
+
* await plugin.system.reveal(exported); // macOS: one Finder window for all
|
|
7306
|
+
* ```
|
|
7307
|
+
*
|
|
7308
|
+
* @since 1.0.0
|
|
5411
7309
|
*/
|
|
5412
7310
|
reveal(paths: string | string[]): Promise<void>;
|
|
5413
|
-
/**
|
|
7311
|
+
/**
|
|
7312
|
+
* Open the file or folder with its default application. `missing` if absent.
|
|
7313
|
+
*
|
|
7314
|
+
* @capability None.
|
|
7315
|
+
* @throws `missing` (rejects) The path does not exist.
|
|
7316
|
+
* @throws `invalid` (rejects) An empty path.
|
|
7317
|
+
* @throws `failed` (rejects) The system refused.
|
|
7318
|
+
* @throws `timeout` (rejects) macOS did not answer within 10 s.
|
|
7319
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
7320
|
+
*
|
|
7321
|
+
* @example Open the plugin's data folder
|
|
7322
|
+
* A "Show data folder" command opens the plugin's data directory in the
|
|
7323
|
+
* file manager, which is the default application for a folder.
|
|
7324
|
+
* ```ts
|
|
7325
|
+
* await plugin.system.openPath(plugin.dataDirectory);
|
|
7326
|
+
* ```
|
|
7327
|
+
*
|
|
7328
|
+
* @since 1.0.0
|
|
7329
|
+
*/
|
|
5414
7330
|
openPath(path: string): Promise<void>;
|
|
5415
7331
|
/**
|
|
5416
7332
|
* Open an http(s) address in the default browser. `invalid` for another
|
|
5417
7333
|
* protocol or a malformed address; `timeout` after 10 s, `failed` otherwise.
|
|
7334
|
+
*
|
|
7335
|
+
* @capability None.
|
|
7336
|
+
* @throws `invalid` (rejects) Not an `http:` or `https:` address.
|
|
7337
|
+
* @throws `timeout` (rejects) macOS did not hand it to the browser within
|
|
7338
|
+
* 10 s.
|
|
7339
|
+
* @throws `failed` (rejects) The system refused.
|
|
7340
|
+
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
7341
|
+
*
|
|
7342
|
+
* @example Open the plugin's help page
|
|
7343
|
+
* A Help command opens the plugin's documentation in the default browser.
|
|
7344
|
+
* Only `http:` and `https:` addresses are opened.
|
|
7345
|
+
* ```ts
|
|
7346
|
+
* await plugin.system.openUrl("https://example.com/plugin/help");
|
|
7347
|
+
* ```
|
|
7348
|
+
*
|
|
7349
|
+
* @since 1.0.0
|
|
5418
7350
|
*/
|
|
5419
7351
|
openUrl(url: string): Promise<void>;
|
|
5420
7352
|
}
|
|
@@ -5422,29 +7354,123 @@ export interface System {
|
|
|
5422
7354
|
* Per-plugin secrets (API keys, passwords) in the system's secret store:
|
|
5423
7355
|
* the login keychain on macOS, the Credential Manager on Windows. Needs
|
|
5424
7356
|
* `secrets`. `unsupported` on systems without a store, `unavailable` when
|
|
5425
|
-
* the store
|
|
7357
|
+
* the store cannot be reached (no keychain, a read-only store, no logged-in
|
|
7358
|
+
* session on Windows): keep a fallback (or ask again).
|
|
5426
7359
|
*
|
|
5427
7360
|
* The user denying access and other system errors reject `failed`. Never
|
|
5428
7361
|
* write secrets to settings files.
|
|
5429
7362
|
*
|
|
5430
|
-
* @example
|
|
7363
|
+
* @example Ask for an API token once and keep it in the keychain
|
|
7364
|
+
* The user types the token the first time; later runs read it from the
|
|
7365
|
+
* system's secret store. `get` resolves `null` when nothing is stored; a
|
|
7366
|
+
* store that is missing or refuses access rejects (`unsupported`,
|
|
7367
|
+
* `unavailable`, `failed`), and then the plugin keeps the token for this
|
|
7368
|
+
* run only and asks again next time.
|
|
5431
7369
|
* ```ts
|
|
5432
|
-
*
|
|
7370
|
+
* declare function askUser(): Promise<string | null>;
|
|
7371
|
+
* // ---cut---
|
|
7372
|
+
* let token: string | null = null;
|
|
7373
|
+
* try {
|
|
7374
|
+
* token = await plugin.secrets.get("api-token");
|
|
7375
|
+
* } catch {
|
|
7376
|
+
* // No usable store: fall through and ask.
|
|
7377
|
+
* }
|
|
5433
7378
|
* if (token === null) {
|
|
5434
7379
|
* token = await askUser();
|
|
5435
|
-
*
|
|
7380
|
+
* if (token)
|
|
7381
|
+
* await plugin.secrets.set("api-token", token).catch(() => {}); // best effort
|
|
5436
7382
|
* }
|
|
5437
7383
|
* ```
|
|
7384
|
+
*
|
|
7385
|
+
* @remarks
|
|
7386
|
+
* Values are stored by the player in the system's secret store under this
|
|
7387
|
+
* plugin's id: other plugins cannot read them. Names are 1–64 of
|
|
7388
|
+
* `a-z 0-9 . _ -`, values 1–1024 bytes of text, at most 256 names per
|
|
7389
|
+
* plugin (`SECRET_LIMITS`).
|
|
7390
|
+
*
|
|
7391
|
+
* @capability secrets
|
|
7392
|
+
* @since 1.0.0
|
|
7393
|
+
* @category Services
|
|
5438
7394
|
*/
|
|
5439
7395
|
export interface Secrets {
|
|
5440
|
-
/**
|
|
7396
|
+
/**
|
|
7397
|
+
* The stored value, `null` when there is none.
|
|
7398
|
+
*
|
|
7399
|
+
* @capability secrets
|
|
7400
|
+
* @throws `unsupported` (rejects) The system has no secret store.
|
|
7401
|
+
* @throws `unavailable` (rejects) The store cannot be reached (no keychain,
|
|
7402
|
+
* a read-only store, no logged-in session on Windows).
|
|
7403
|
+
* @throws `failed` (rejects) The user denied access, or another system
|
|
7404
|
+
* error.
|
|
7405
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7406
|
+
* `secrets`.
|
|
7407
|
+
* @throws `invalid` (rejects) A bad name.
|
|
7408
|
+
*
|
|
7409
|
+
* @example Read a stored token
|
|
7410
|
+
* The plugin uses the token the user saved earlier, or asks for one when
|
|
7411
|
+
* there is none. `get` resolves with the stored text or `null`.
|
|
7412
|
+
* ```ts
|
|
7413
|
+
* const token = await plugin.secrets.get("api-token");
|
|
7414
|
+
* if (token === null) await plugin.osd.show("Sign in first");
|
|
7415
|
+
* ```
|
|
7416
|
+
*
|
|
7417
|
+
* @since 1.0.0
|
|
7418
|
+
*/
|
|
5441
7419
|
get(name: string): Promise<string | null>;
|
|
5442
7420
|
/**
|
|
5443
7421
|
* Store or replace (`name`: 1–64 of `a-z 0-9 . _ -`; value 1–1024 bytes).
|
|
5444
7422
|
* At most 256 different names per plugin: a new name beyond that rejects
|
|
5445
7423
|
* `limit`. Uninstalling with `removeData` deletes them.
|
|
7424
|
+
*
|
|
7425
|
+
* @capability secrets
|
|
7426
|
+
* @throws `invalid` (rejects) A bad name, or a value that is empty, has NUL
|
|
7427
|
+
* or is over 1024 bytes.
|
|
7428
|
+
* @throws `limit` (rejects) A new name beyond 256.
|
|
7429
|
+
* @throws `unsupported` (rejects) The system has no secret store.
|
|
7430
|
+
* @throws `unavailable` (rejects) The store cannot be reached (no keychain,
|
|
7431
|
+
* a read-only store, no logged-in session on Windows).
|
|
7432
|
+
* @throws `failed` (rejects) The user denied access, or another system
|
|
7433
|
+
* error.
|
|
7434
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7435
|
+
* `secrets`.
|
|
7436
|
+
*
|
|
7437
|
+
* @example Save the token the user entered
|
|
7438
|
+
* The token the user typed is kept in the system's secret store for later
|
|
7439
|
+
* runs. `set` stores or replaces it; the store can refuse, in which case
|
|
7440
|
+
* the plugin keeps the token for this run only.
|
|
7441
|
+
* ```ts
|
|
7442
|
+
* declare const token: string;
|
|
7443
|
+
* // ---cut---
|
|
7444
|
+
* await plugin.secrets.set("api-token", token).catch((error: PluginError) => {
|
|
7445
|
+
* // No store, or access denied: the token is not kept for next time.
|
|
7446
|
+
* void plugin.osd.show("The token could not be saved ({})", [error.code]);
|
|
7447
|
+
* });
|
|
7448
|
+
* ```
|
|
7449
|
+
*
|
|
7450
|
+
* @since 1.0.0
|
|
5446
7451
|
*/
|
|
5447
7452
|
set(name: string, value: string): Promise<void>;
|
|
5448
|
-
/**
|
|
7453
|
+
/**
|
|
7454
|
+
* Remove; removing a missing secret is fine.
|
|
7455
|
+
*
|
|
7456
|
+
* @capability secrets
|
|
7457
|
+
* @throws `invalid` (rejects) A bad name.
|
|
7458
|
+
* @throws `unsupported` (rejects) The system has no secret store.
|
|
7459
|
+
* @throws `unavailable` (rejects) The store cannot be reached (no keychain,
|
|
7460
|
+
* a read-only store, no logged-in session on Windows).
|
|
7461
|
+
* @throws `failed` (rejects) The user denied access, or another system
|
|
7462
|
+
* error.
|
|
7463
|
+
* @throws `capability` (rejects) The manifest does not declare
|
|
7464
|
+
* `secrets`.
|
|
7465
|
+
*
|
|
7466
|
+
* @example Sign out by removing the token
|
|
7467
|
+
* Signing out removes the stored token, so the next start asks again.
|
|
7468
|
+
* Removing a name that is not stored is fine.
|
|
7469
|
+
* ```ts
|
|
7470
|
+
* await plugin.secrets.delete("api-token");
|
|
7471
|
+
* ```
|
|
7472
|
+
*
|
|
7473
|
+
* @since 1.0.0
|
|
7474
|
+
*/
|
|
5449
7475
|
delete(name: string): Promise<void>;
|
|
5450
7476
|
}
|