@vestra-player/sdk 1.5.30 → 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 +11 -0
- package/dist/manifest.js +1 -1
- package/dist/plugin/services.d.ts +2 -1
- package/dist/plugin/services.js +2 -1
- package/dist/plugin/testing.js +1 -1
- package/dist/plugin/types.d.ts +1458 -49
- package/dist/plugin/values.d.ts +2 -2
- package/dist/plugin/values.js +2 -2
- package/dist/processing.js +1 -1
- package/llms-full.txt +1166 -39
- package/llms.txt +22 -22
- package/package.json +1 -1
package/dist/plugin/types.d.ts
CHANGED
|
@@ -372,7 +372,13 @@ export interface Plugin extends Scope {
|
|
|
372
372
|
*/
|
|
373
373
|
readonly dataDirectory: string;
|
|
374
374
|
}
|
|
375
|
-
/**
|
|
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
|
+
*/
|
|
376
382
|
export interface CommandState {
|
|
377
383
|
/** False greys the item out; the player then never calls `run` (bound keys are swallowed). Default true. */
|
|
378
384
|
enabled: boolean;
|
|
@@ -404,6 +410,9 @@ export interface CommandState {
|
|
|
404
410
|
* run: () => plugin.player.setLoop(null),
|
|
405
411
|
* });
|
|
406
412
|
* ```
|
|
413
|
+
*
|
|
414
|
+
* @since 1.0.0
|
|
415
|
+
* @category Commands
|
|
407
416
|
*/
|
|
408
417
|
export type Bindable<T> = T | (() => T);
|
|
409
418
|
/**
|
|
@@ -411,6 +420,9 @@ export type Bindable<T> = T | (() => T);
|
|
|
411
420
|
* `@<id>` or by its English key (see {@link CommandOptions.path}): Open,
|
|
412
421
|
* Playback, Subtitles, Video, Audio, Screen, Settings. The list is
|
|
413
422
|
* `MENU_IDS` in `@vestra-player/sdk/testing`.
|
|
423
|
+
*
|
|
424
|
+
* @since 1.3.0
|
|
425
|
+
* @category Commands
|
|
414
426
|
*/
|
|
415
427
|
export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
|
|
416
428
|
/**
|
|
@@ -419,6 +431,8 @@ export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "sc
|
|
|
419
431
|
* `@vestra-player/sdk/testing`.
|
|
420
432
|
*
|
|
421
433
|
* @since 1.5.29
|
|
434
|
+
*
|
|
435
|
+
* @category Commands
|
|
422
436
|
*/
|
|
423
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";
|
|
424
438
|
/**
|
|
@@ -470,6 +484,8 @@ export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "op
|
|
|
470
484
|
* @player 0.5.10; older players show the menu entry without a button
|
|
471
485
|
* (declare `"minPlayerVersion": "0.5.10"` when the button matters).
|
|
472
486
|
* @since 1.5.29
|
|
487
|
+
*
|
|
488
|
+
* @category Commands
|
|
473
489
|
*/
|
|
474
490
|
export interface CommandButton {
|
|
475
491
|
/**
|
|
@@ -518,6 +534,10 @@ export interface CommandButton {
|
|
|
518
534
|
* {@link Commands.add} throws `invalid` synchronously for a bad path,
|
|
519
535
|
* order, group or binding. The same path registered twice, or a path that
|
|
520
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
|
|
521
541
|
*/
|
|
522
542
|
export interface CommandOptions {
|
|
523
543
|
/** Local id, unique within the plugin (the global id is `<manifest id>/<id>`). */
|
|
@@ -644,6 +664,38 @@ export interface CommandOptions {
|
|
|
644
664
|
/**
|
|
645
665
|
* A registered command, from {@link Commands.add}. Disposing it removes it
|
|
646
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
|
|
647
699
|
*/
|
|
648
700
|
export interface Command extends Disposable {
|
|
649
701
|
/** The local id given in {@link CommandOptions.id}. */
|
|
@@ -672,6 +724,9 @@ export interface Command extends Disposable {
|
|
|
672
724
|
* },
|
|
673
725
|
* });
|
|
674
726
|
* ```
|
|
727
|
+
*
|
|
728
|
+
* @capability commands
|
|
729
|
+
* @since 1.0.0
|
|
675
730
|
*/
|
|
676
731
|
set(state: Partial<{
|
|
677
732
|
[K in keyof CommandState]: Bindable<CommandState[K]>;
|
|
@@ -682,10 +737,49 @@ export interface Command extends Disposable {
|
|
|
682
737
|
* commands in this plugin, or 1024 in the whole player). You rarely need
|
|
683
738
|
* to await it: during activation a failure fails activation, later it
|
|
684
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.
|
|
685
747
|
*/
|
|
686
748
|
readonly ready: Promise<void>;
|
|
687
749
|
}
|
|
688
|
-
/**
|
|
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
|
+
*/
|
|
689
783
|
export interface Commands {
|
|
690
784
|
/**
|
|
691
785
|
* Declare a command. It is registered and kept in sync; `dispose()` removes
|
|
@@ -715,6 +809,9 @@ export interface Commands {
|
|
|
715
809
|
* run: () => plugin.player.setPaused(!plugin.player.state.paused),
|
|
716
810
|
* });
|
|
717
811
|
* ```
|
|
812
|
+
*
|
|
813
|
+
* @capability commands
|
|
814
|
+
* @since 1.0.0
|
|
718
815
|
*/
|
|
719
816
|
add(options: CommandOptions): Command;
|
|
720
817
|
}
|
|
@@ -3814,7 +3911,12 @@ export interface Dialogs {
|
|
|
3814
3911
|
fileName?: TextValue;
|
|
3815
3912
|
}): Promise<string | null>;
|
|
3816
3913
|
}
|
|
3817
|
-
/**
|
|
3914
|
+
/**
|
|
3915
|
+
* One audio track of the current media ({@link PlayerState.audioTracks}).
|
|
3916
|
+
*
|
|
3917
|
+
* @since 1.0.0
|
|
3918
|
+
* @category Playback
|
|
3919
|
+
*/
|
|
3818
3920
|
export interface AudioTrack {
|
|
3819
3921
|
/** The backend's track id; pass it to {@link Player.selectAudioTrack}. */
|
|
3820
3922
|
id: number;
|
|
@@ -3836,6 +3938,10 @@ export interface AudioTrack {
|
|
|
3836
3938
|
*
|
|
3837
3939
|
* Without `events`, {@link Player.state} only updates at start and after
|
|
3838
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
|
|
3839
3945
|
*/
|
|
3840
3946
|
export interface PlayerState {
|
|
3841
3947
|
/** Changes every time a source is opened, even the same file again. */
|
|
@@ -3913,14 +4019,24 @@ export interface PlayerState {
|
|
|
3913
4019
|
active: string | null;
|
|
3914
4020
|
};
|
|
3915
4021
|
}
|
|
3916
|
-
/**
|
|
4022
|
+
/**
|
|
4023
|
+
* An audio output device (`player.audioDevices`).
|
|
4024
|
+
*
|
|
4025
|
+
* @since 1.0.0
|
|
4026
|
+
* @category Playback
|
|
4027
|
+
*/
|
|
3917
4028
|
export interface AudioDevice {
|
|
3918
4029
|
/** Stable id to pass to `setAudioDevice` (e.g. `coreaudio/<UID>`). */
|
|
3919
4030
|
id: string;
|
|
3920
4031
|
/** What the system calls it. */
|
|
3921
4032
|
name: string;
|
|
3922
4033
|
}
|
|
3923
|
-
/**
|
|
4034
|
+
/**
|
|
4035
|
+
* One chapter of the current media.
|
|
4036
|
+
*
|
|
4037
|
+
* @since 1.0.0
|
|
4038
|
+
* @category Playback
|
|
4039
|
+
*/
|
|
3924
4040
|
export interface Chapter {
|
|
3925
4041
|
/** Chapter title, `null` without one. */
|
|
3926
4042
|
title: string | null;
|
|
@@ -3955,12 +4071,41 @@ export interface Chapter {
|
|
|
3955
4071
|
* }
|
|
3956
4072
|
* });
|
|
3957
4073
|
* ```
|
|
4074
|
+
*
|
|
4075
|
+
* @see [Playback state, controls and sources](../../docs/concepts/playback.md)
|
|
4076
|
+
* @since 1.0.0
|
|
4077
|
+
* @category Playback
|
|
3958
4078
|
*/
|
|
3959
4079
|
export interface Conditional {
|
|
3960
4080
|
/** The state the call is conditional on; default: the current `player.state`. */
|
|
3961
4081
|
ifCurrent?: PlayerState;
|
|
3962
4082
|
}
|
|
3963
|
-
/**
|
|
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
|
+
*/
|
|
3964
4109
|
export interface Media {
|
|
3965
4110
|
/** Still the player's source (a restart keeps it current). */
|
|
3966
4111
|
readonly current: boolean;
|
|
@@ -3968,13 +4113,77 @@ export interface Media {
|
|
|
3968
4113
|
* Resolves when it is playing. Rejects with code `failed` (load error),
|
|
3969
4114
|
* `replaced` (another source took over) or `ended`; with `AbortError` when
|
|
3970
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
|
|
3971
4140
|
*/
|
|
3972
4141
|
ready(options?: {
|
|
3973
4142
|
signal?: AbortSignal;
|
|
3974
4143
|
}): Promise<void>;
|
|
3975
|
-
/**
|
|
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
|
+
*/
|
|
3976
4167
|
onEnd(listener: (state: PlayerState) => void): Disposable;
|
|
3977
|
-
/**
|
|
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
|
+
*/
|
|
3978
4187
|
close(): Promise<void>;
|
|
3979
4188
|
}
|
|
3980
4189
|
/**
|
|
@@ -3997,6 +4206,18 @@ export interface Media {
|
|
|
3997
4206
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds));
|
|
3998
4207
|
* bar.on("dragEnd", () => scrub?.end());
|
|
3999
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
|
|
4000
4221
|
*/
|
|
4001
4222
|
export interface Scrub {
|
|
4002
4223
|
/**
|
|
@@ -4029,13 +4250,40 @@ export interface Scrub {
|
|
|
4029
4250
|
* const keyframes = true; // the plugin's own setting
|
|
4030
4251
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
|
|
4031
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
|
|
4032
4263
|
*/
|
|
4033
4264
|
seek(seconds: number, options?: {
|
|
4034
4265
|
keyframes?: boolean;
|
|
4035
4266
|
}): Promise<boolean>;
|
|
4036
4267
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
4037
4268
|
readonly active: boolean;
|
|
4038
|
-
/**
|
|
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
|
+
*/
|
|
4039
4287
|
end(): void;
|
|
4040
4288
|
}
|
|
4041
4289
|
/**
|
|
@@ -4049,6 +4297,9 @@ export interface Scrub {
|
|
|
4049
4297
|
*
|
|
4050
4298
|
* The SDK copies the frame once before `frame()` is called again, so you
|
|
4051
4299
|
* may reuse the same buffers for every frame.
|
|
4300
|
+
*
|
|
4301
|
+
* @since 1.0.0
|
|
4302
|
+
* @category Playback
|
|
4052
4303
|
*/
|
|
4053
4304
|
export interface StreamFrame {
|
|
4054
4305
|
/** `width × height × 4` bytes of RGBA, at most 4 MiB. */
|
|
@@ -4060,7 +4311,13 @@ export interface StreamFrame {
|
|
|
4060
4311
|
*/
|
|
4061
4312
|
audio: Float32Array;
|
|
4062
4313
|
}
|
|
4063
|
-
/**
|
|
4314
|
+
/**
|
|
4315
|
+
* Options of {@link Player.openStream}.
|
|
4316
|
+
*
|
|
4317
|
+
* @see {@link Player.openStream}
|
|
4318
|
+
* @since 1.0.0
|
|
4319
|
+
* @category Playback
|
|
4320
|
+
*/
|
|
4064
4321
|
export interface StreamOptions {
|
|
4065
4322
|
/** Frame width in pixels. */
|
|
4066
4323
|
width: number;
|
|
@@ -4075,12 +4332,48 @@ export interface StreamOptions {
|
|
|
4075
4332
|
* `openStream` start-up timeouts: code `timeout` with the step that ran out
|
|
4076
4333
|
* of time, the first buffers (`prefill`) or the host's activation.
|
|
4077
4334
|
* Both limits are 10 seconds.
|
|
4335
|
+
*
|
|
4336
|
+
* @since 1.0.0
|
|
4337
|
+
* @category Playback
|
|
4078
4338
|
*/
|
|
4079
4339
|
export type StreamStartError = PluginError & {
|
|
4080
4340
|
/** The start-up step that timed out. */
|
|
4081
4341
|
stage: "prefill" | "activation";
|
|
4082
4342
|
};
|
|
4083
|
-
/**
|
|
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
|
+
*/
|
|
4084
4377
|
export interface Stream extends Disposable {
|
|
4085
4378
|
/** The stream is producing frames. */
|
|
4086
4379
|
readonly running: boolean;
|
|
@@ -4096,6 +4389,23 @@ export interface Stream extends Disposable {
|
|
|
4096
4389
|
* (before 1.5.7 a replaced or closed stream could get `unavailable`
|
|
4097
4390
|
* "producer absent"). Without `playback.read` a failure the SDK has not
|
|
4098
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
|
|
4099
4409
|
*/
|
|
4100
4410
|
onStop(listener: (error: PluginError | null) => void): Disposable;
|
|
4101
4411
|
/**
|
|
@@ -4103,10 +4413,34 @@ export interface Stream extends Disposable {
|
|
|
4103
4413
|
* The source closes first, then the frame connection, then the producer,
|
|
4104
4414
|
* so stopping never shows as a playback failure. `dispose()` and
|
|
4105
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
|
|
4106
4434
|
*/
|
|
4107
4435
|
stop(): Promise<void>;
|
|
4108
4436
|
}
|
|
4109
|
-
/**
|
|
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
|
+
*/
|
|
4110
4444
|
export interface SequenceProvider {
|
|
4111
4445
|
/** Go to the next item (called in the provider's scope). */
|
|
4112
4446
|
next(): unknown;
|
|
@@ -4120,6 +4454,10 @@ export interface SequenceProvider {
|
|
|
4120
4454
|
/**
|
|
4121
4455
|
* What {@link Player.stats} reports. Every field is the backend's own value
|
|
4122
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
|
|
4123
4461
|
*/
|
|
4124
4462
|
export interface PlaybackStats {
|
|
4125
4463
|
/**
|
|
@@ -4234,15 +4572,90 @@ export interface PlaybackStats {
|
|
|
4234
4572
|
* void plugin.osd.show("Cannot play: {}", [(error as Error).message]);
|
|
4235
4573
|
* }
|
|
4236
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
|
|
4237
4595
|
*/
|
|
4238
4596
|
export interface Player {
|
|
4239
4597
|
/** Latest state; always newer than anything delivered before. */
|
|
4240
4598
|
readonly state: PlayerState;
|
|
4241
|
-
/**
|
|
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
|
+
*/
|
|
4242
4619
|
onChange(listener: (state: PlayerState) => void): Disposable;
|
|
4243
|
-
/**
|
|
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
|
+
*/
|
|
4244
4638
|
refresh(): Promise<PlayerState>;
|
|
4245
|
-
/**
|
|
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
|
+
*/
|
|
4246
4659
|
isCurrent(state: PlayerState): boolean;
|
|
4247
4660
|
/**
|
|
4248
4661
|
* Open a path or URL, replacing the current source (unless `ifCurrent` no
|
|
@@ -4253,6 +4666,37 @@ export interface Player {
|
|
|
4253
4666
|
* {@link Resolver} are resolved first, and so are local files of an
|
|
4254
4667
|
* extension a file resolver claims (the source is then a stream that
|
|
4255
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
|
|
4256
4700
|
*/
|
|
4257
4701
|
open(source: string, options?: {
|
|
4258
4702
|
paused?: boolean;
|
|
@@ -4290,32 +4734,219 @@ export interface Player {
|
|
|
4290
4734
|
* // `error` is null when the stream stopped normally.
|
|
4291
4735
|
* stream.onStop((error) => error && plugin.osd.show(error.message));
|
|
4292
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
|
|
4293
4749
|
*/
|
|
4294
4750
|
openStream(options: StreamOptions): Promise<Stream>;
|
|
4295
|
-
/**
|
|
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
|
+
*/
|
|
4296
4779
|
setPaused(paused: boolean, options?: Conditional): Promise<void>;
|
|
4297
|
-
/**
|
|
4780
|
+
/**
|
|
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
|
|
4808
|
+
*/
|
|
4298
4809
|
seek(seconds: number, options?: Conditional): Promise<void>;
|
|
4299
|
-
/**
|
|
4810
|
+
/**
|
|
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`.
|
|
4825
|
+
*
|
|
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.
|
|
4830
|
+
* ```ts
|
|
4831
|
+
* if (plugin.player.state.can.rate) {
|
|
4832
|
+
* await plugin.player.setRate(1.5);
|
|
4833
|
+
* await plugin.osd.show("Speed {}×", [1.5]);
|
|
4834
|
+
* }
|
|
4835
|
+
* ```
|
|
4836
|
+
*
|
|
4837
|
+
* @since 1.0.0
|
|
4838
|
+
*/
|
|
4300
4839
|
setRate(rate: number, options?: Conditional): Promise<void>;
|
|
4301
|
-
/**
|
|
4840
|
+
/**
|
|
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
|
|
4862
|
+
*/
|
|
4302
4863
|
restart(options?: Conditional): Promise<void>;
|
|
4303
|
-
/**
|
|
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
|
+
*/
|
|
4304
4891
|
close(options?: Conditional): Promise<void>;
|
|
4305
|
-
/**
|
|
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
|
+
*/
|
|
4306
4913
|
scrub(): Scrub;
|
|
4307
4914
|
/**
|
|
4308
4915
|
* Device output; last write wins and volume is applied before mute.
|
|
4309
4916
|
* Resolves `true` when this request was applied, `false` if superseded.
|
|
4310
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
|
|
4311
4938
|
*/
|
|
4312
4939
|
setOutput(output: {
|
|
4313
4940
|
volume?: number;
|
|
4314
4941
|
muted?: boolean;
|
|
4315
4942
|
}): Promise<boolean>;
|
|
4316
4943
|
/**
|
|
4317
|
-
* Become the sequence provider
|
|
4318
|
-
*
|
|
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.
|
|
4319
4950
|
*
|
|
4320
4951
|
* @example Drive the player's Next and Previous from a playlist
|
|
4321
4952
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
@@ -4339,15 +4970,82 @@ export interface Player {
|
|
|
4339
4970
|
* canPrevious: () => index > 0,
|
|
4340
4971
|
* });
|
|
4341
4972
|
* ```
|
|
4973
|
+
*
|
|
4974
|
+
* @capability playback.control
|
|
4975
|
+
* @since 1.0.0
|
|
4342
4976
|
*/
|
|
4343
4977
|
provideSequence(provider: SequenceProvider): Disposable;
|
|
4344
|
-
/** Ask the provider to go forward / back. Rejects `unavailable` when it cannot. */
|
|
4345
|
-
next(): Promise<void>;
|
|
4346
|
-
/** Ask the provider to go back. Rejects `unavailable` when it cannot. */
|
|
4347
|
-
previous(): Promise<void>;
|
|
4348
4978
|
/**
|
|
4349
|
-
*
|
|
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.
|
|
4350
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
|
|
4351
5049
|
*/
|
|
4352
5050
|
stats(): Promise<PlaybackStats>;
|
|
4353
5051
|
/**
|
|
@@ -4355,11 +5053,56 @@ export interface Player {
|
|
|
4355
5053
|
* unless `select: false`, select it. Resolves with the new track (`external`,
|
|
4356
5054
|
* not exportable). It stays until the source changes. Rejects `invalid` for a
|
|
4357
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
|
|
4358
5077
|
*/
|
|
4359
5078
|
addAudioTrack(path: string, options?: {
|
|
4360
5079
|
select?: boolean;
|
|
4361
5080
|
} & Conditional): Promise<AudioTrack>;
|
|
4362
|
-
/**
|
|
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
|
+
*/
|
|
4363
5106
|
selectAudioTrack(id: number, options?: Conditional): Promise<PlayerState>;
|
|
4364
5107
|
/**
|
|
4365
5108
|
* One frame or to the next / previous keyframe (pauses for frames). Rejects
|
|
@@ -4369,6 +5112,29 @@ export interface Player {
|
|
|
4369
5112
|
* Resolves once the new position is in effect, so `player.state.position`
|
|
4370
5113
|
* is current afterwards. One step at a time: a second call while one is
|
|
4371
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
|
|
4372
5138
|
*/
|
|
4373
5139
|
step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
|
|
4374
5140
|
/**
|
|
@@ -4391,6 +5157,23 @@ export interface Player {
|
|
|
4391
5157
|
*
|
|
4392
5158
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
4393
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
|
|
4394
5177
|
*/
|
|
4395
5178
|
setStepSeek(options: {
|
|
4396
5179
|
seconds: number;
|
|
@@ -4400,6 +5183,23 @@ export interface Player {
|
|
|
4400
5183
|
* The chapters of the current media (empty without media or chapters),
|
|
4401
5184
|
* read on demand; `sourceId` tells which source they belong to.
|
|
4402
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
|
|
4403
5203
|
*/
|
|
4404
5204
|
chapters(): Promise<{
|
|
4405
5205
|
/** The source the chapters belong to (compare with `state.sourceId`). */
|
|
@@ -4413,13 +5213,59 @@ export interface Player {
|
|
|
4413
5213
|
* stops it. After the last jump back `state.loop` becomes null.
|
|
4414
5214
|
* `invalid` out of range, `unsupported` for a source that cannot seek,
|
|
4415
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
|
|
4416
5238
|
*/
|
|
4417
5239
|
setLoop(loop: {
|
|
4418
5240
|
a: number;
|
|
4419
5241
|
b: number;
|
|
4420
5242
|
count?: number;
|
|
4421
5243
|
} | null, options?: Conditional): Promise<void>;
|
|
4422
|
-
/**
|
|
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
|
+
*/
|
|
4423
5269
|
audioDevices(): Promise<AudioDevice[]>;
|
|
4424
5270
|
/**
|
|
4425
5271
|
* Play on this device while it is present, else on the system default
|
|
@@ -4429,6 +5275,23 @@ export interface Player {
|
|
|
4429
5275
|
* Unknown ids are accepted (`state.audioOutput.active` stays `null`); a
|
|
4430
5276
|
* malformed id is `invalid`. Reverts to the default when this plugin
|
|
4431
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
|
|
4432
5295
|
*/
|
|
4433
5296
|
setAudioDevice(id: string | null): Promise<void>;
|
|
4434
5297
|
}
|
|
@@ -4453,9 +5316,45 @@ export interface Player {
|
|
|
4453
5316
|
* (`plugin.window.onChange`), which also changes when the user toggles it.
|
|
4454
5317
|
* Players before 0.5.2 reject it with `invalid`: set
|
|
4455
5318
|
* `"minPlayerVersion": "0.5.2"` when you use it.
|
|
5319
|
+
*
|
|
5320
|
+
* @see {@link Host.run}
|
|
5321
|
+
* @since 1.0.0
|
|
5322
|
+
* @category Commands
|
|
4456
5323
|
*/
|
|
4457
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";
|
|
4458
|
-
/**
|
|
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
|
+
*/
|
|
4459
5358
|
export interface Host {
|
|
4460
5359
|
/**
|
|
4461
5360
|
* Run a core action exactly like its menu item or key: same checks, same
|
|
@@ -4482,6 +5381,14 @@ export interface Host {
|
|
|
4482
5381
|
* }
|
|
4483
5382
|
* });
|
|
4484
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
|
|
4485
5392
|
*/
|
|
4486
5393
|
run(action: HostAction): Promise<void>;
|
|
4487
5394
|
/**
|
|
@@ -4511,6 +5418,12 @@ export interface Host {
|
|
|
4511
5418
|
* // ---cut---
|
|
4512
5419
|
* bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 }));
|
|
4513
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`.
|
|
4514
5427
|
*/
|
|
4515
5428
|
run(action: "seek-backward" | "seek-forward", options: {
|
|
4516
5429
|
seconds: number;
|
|
@@ -5782,16 +6695,109 @@ export interface Bindings {
|
|
|
5782
6695
|
* void plugin.osd.show("Could not copy: {}", [(error as PluginError).code]);
|
|
5783
6696
|
* }
|
|
5784
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
|
|
5785
6706
|
*/
|
|
5786
6707
|
export interface Clipboard {
|
|
5787
|
-
/**
|
|
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
|
+
*/
|
|
5788
6729
|
readText(): Promise<string | null>;
|
|
5789
|
-
/**
|
|
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
|
+
*/
|
|
5790
6751
|
writeText(text: string): Promise<void>;
|
|
5791
|
-
/**
|
|
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
|
+
*/
|
|
5792
6778
|
writeImage(path: string): Promise<void>;
|
|
5793
6779
|
}
|
|
5794
|
-
/**
|
|
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
|
+
*/
|
|
5795
6801
|
export interface Power {
|
|
5796
6802
|
/**
|
|
5797
6803
|
* `"playing"`: while a source is ready and not paused, keep the display
|
|
@@ -5802,10 +6808,32 @@ export interface Power {
|
|
|
5802
6808
|
* take the system assertion are logged by the player, not reported.
|
|
5803
6809
|
* macOS uses `caffeinate`, Windows the thread execution state; elsewhere
|
|
5804
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
|
|
5805
6827
|
*/
|
|
5806
6828
|
preventSleep(mode: "off" | "playing"): Promise<void>;
|
|
5807
6829
|
}
|
|
5808
|
-
/**
|
|
6830
|
+
/**
|
|
6831
|
+
* The output of {@link Tools.run}.
|
|
6832
|
+
*
|
|
6833
|
+
* @see {@link Tools.run}
|
|
6834
|
+
* @since 1.0.0
|
|
6835
|
+
* @category Services
|
|
6836
|
+
*/
|
|
5809
6837
|
export interface ToolResult {
|
|
5810
6838
|
/** Standard output (at most `maxOutput` bytes). */
|
|
5811
6839
|
stdout: Uint8Array;
|
|
@@ -5835,11 +6863,23 @@ export interface ToolResult {
|
|
|
5835
6863
|
* void info.format; // container, duration, bit rate…
|
|
5836
6864
|
* }
|
|
5837
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
|
|
5838
6878
|
*/
|
|
5839
6879
|
export interface Tools {
|
|
5840
6880
|
/**
|
|
5841
|
-
* Run ffmpeg or ffprobe (
|
|
5842
|
-
*
|
|
6881
|
+
* Run ffmpeg or ffprobe (`VESTRA_FFMPEG` / `VESTRA_FFPROBE` when set, else
|
|
6882
|
+
* found on `PATH`) without a shell. Rejects `unavailable` (not found), `timeout`,
|
|
5843
6883
|
* `failed` (non-zero exit; `message` is the last stderr line), `invalid`
|
|
5844
6884
|
* (output over `maxOutput`, default 16 MiB), `aborted` (`signal`). Timeout,
|
|
5845
6885
|
* `signal` and disposing the scope end the process.
|
|
@@ -5847,28 +6887,171 @@ export interface Tools {
|
|
|
5847
6887
|
* Arguments are passed as-is (no quoting needed). On Windows `PATHEXT` is
|
|
5848
6888
|
* honoured. Ending sends SIGTERM, then SIGKILL after 300 ms (immediate on
|
|
5849
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
|
|
5850
6918
|
*/
|
|
5851
6919
|
run(tool: "ffmpeg" | "ffprobe", args: string[], options: {
|
|
5852
6920
|
timeoutMs: number;
|
|
5853
6921
|
maxOutput?: number;
|
|
5854
6922
|
signal?: AbortSignal;
|
|
5855
6923
|
}): Promise<ToolResult>;
|
|
5856
|
-
/**
|
|
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
|
+
*/
|
|
5857
6948
|
available(tool: "ffmpeg" | "ffprobe"): Promise<boolean>;
|
|
5858
6949
|
}
|
|
5859
|
-
/**
|
|
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
|
+
*/
|
|
5860
6976
|
export interface JsonStore<T> extends Disposable {
|
|
5861
6977
|
/** The loaded value, or `defaults` when the file was missing or broken. */
|
|
5862
6978
|
readonly value: T;
|
|
5863
6979
|
/**
|
|
5864
6980
|
* Replace the value now and write it after `debounceMs`.
|
|
5865
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
|
|
5866
6997
|
*/
|
|
5867
6998
|
save(next: T): void;
|
|
5868
|
-
/**
|
|
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
|
+
*/
|
|
5869
7023
|
flush(): Promise<void>;
|
|
5870
7024
|
}
|
|
5871
|
-
/**
|
|
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
|
+
*/
|
|
5872
7055
|
export interface Storage {
|
|
5873
7056
|
/**
|
|
5874
7057
|
* Open `<name>.json` (`name`: 1–64 of `a-z 0-9 -`). A file that cannot be
|
|
@@ -5897,6 +7080,11 @@ export interface Storage {
|
|
|
5897
7080
|
* // Only `save` writes: changing `store.value` in place is not saved.
|
|
5898
7081
|
* store.save({ ...store.value, recent: [path, ...store.value.recent].slice(0, 10) });
|
|
5899
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
|
|
5900
7088
|
*/
|
|
5901
7089
|
json<T>(name: string, options: {
|
|
5902
7090
|
defaults: T;
|
|
@@ -5970,19 +7158,97 @@ export interface Storage {
|
|
|
5970
7158
|
* ```
|
|
5971
7159
|
*
|
|
5972
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
|
|
5973
7169
|
*/
|
|
5974
7170
|
export interface SessionStorage {
|
|
5975
|
-
/**
|
|
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
|
+
*/
|
|
5976
7192
|
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
5977
7193
|
/**
|
|
5978
7194
|
* Store or replace `value` under `key`. `invalid` for a bad key or a value
|
|
5979
7195
|
* that is not JSON, `limit` when the plugin's values would be over 1 MiB
|
|
5980
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
|
|
5981
7215
|
*/
|
|
5982
7216
|
set(key: string, value: unknown): Promise<void>;
|
|
5983
|
-
/**
|
|
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
|
+
*/
|
|
5984
7234
|
delete(key: string): Promise<void>;
|
|
5985
|
-
/**
|
|
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
|
+
*/
|
|
5986
7252
|
keys(): Promise<string[]>;
|
|
5987
7253
|
}
|
|
5988
7254
|
/**
|
|
@@ -6005,6 +7271,15 @@ export interface SessionStorage {
|
|
|
6005
7271
|
* await plugin.osd.show(`Saved to ${exportedFile}`); // no file manager to open
|
|
6006
7272
|
* }
|
|
6007
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
|
|
6008
7283
|
*/
|
|
6009
7284
|
export interface System {
|
|
6010
7285
|
/**
|
|
@@ -6012,13 +7287,66 @@ export interface System {
|
|
|
6012
7287
|
* for all, so several files of one folder are all highlighted; Windows:
|
|
6013
7288
|
* one Explorer window per folder, selecting its first path). Paths that do
|
|
6014
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
|
|
6015
7309
|
*/
|
|
6016
7310
|
reveal(paths: string | string[]): Promise<void>;
|
|
6017
|
-
/**
|
|
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
|
+
*/
|
|
6018
7330
|
openPath(path: string): Promise<void>;
|
|
6019
7331
|
/**
|
|
6020
7332
|
* Open an http(s) address in the default browser. `invalid` for another
|
|
6021
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
|
|
6022
7350
|
*/
|
|
6023
7351
|
openUrl(url: string): Promise<void>;
|
|
6024
7352
|
}
|
|
@@ -6026,7 +7354,8 @@ export interface System {
|
|
|
6026
7354
|
* Per-plugin secrets (API keys, passwords) in the system's secret store:
|
|
6027
7355
|
* the login keychain on macOS, the Credential Manager on Windows. Needs
|
|
6028
7356
|
* `secrets`. `unsupported` on systems without a store, `unavailable` when
|
|
6029
|
-
* 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).
|
|
6030
7359
|
*
|
|
6031
7360
|
* The user denying access and other system errors reject `failed`. Never
|
|
6032
7361
|
* write secrets to settings files.
|
|
@@ -6052,16 +7381,96 @@ export interface System {
|
|
|
6052
7381
|
* await plugin.secrets.set("api-token", token).catch(() => {}); // best effort
|
|
6053
7382
|
* }
|
|
6054
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
|
|
6055
7394
|
*/
|
|
6056
7395
|
export interface Secrets {
|
|
6057
|
-
/**
|
|
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
|
+
*/
|
|
6058
7419
|
get(name: string): Promise<string | null>;
|
|
6059
7420
|
/**
|
|
6060
7421
|
* Store or replace (`name`: 1–64 of `a-z 0-9 . _ -`; value 1–1024 bytes).
|
|
6061
7422
|
* At most 256 different names per plugin: a new name beyond that rejects
|
|
6062
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
|
|
6063
7451
|
*/
|
|
6064
7452
|
set(name: string, value: string): Promise<void>;
|
|
6065
|
-
/**
|
|
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
|
+
*/
|
|
6066
7475
|
delete(name: string): Promise<void>;
|
|
6067
7476
|
}
|