@vestra-player/sdk 1.6.8 → 1.6.9
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 +9 -0
- package/dist/api-requests.js +5 -1
- package/dist/docs-tree.json.gz +0 -0
- package/dist/host-capabilities.js +4 -1
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/osd.d.ts +4 -0
- package/dist/plugin/osd.js +26 -1
- package/dist/plugin/testing.d.ts +98 -0
- package/dist/plugin/testing.js +158 -3
- package/dist/plugin/timeline.d.ts +40 -3
- package/dist/plugin/timeline.js +102 -10
- package/dist/plugin/types.d.ts +187 -3
- package/dist/wire.d.ts +27 -4
- package/dist/wire.js +8 -1
- package/llms.txt +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.9
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Additions only: timeline ranges, OSD messages with action buttons, a limits constant, a wire method and `TestHost` support, and documentation of a bottom bar's `content-full-width`. `apiVersion` stays `vestra/1`, no new capability (`timeline.marks`, `playback.read` and `ui.osd` cover them). A plugin that does not use them sends exactly what it sent before; `onChange` of `plugin.timeline` still runs only when the marks change. One that uses them needs player 0.7.6 (`"minPlayerVersion": "0.7.6"`): the player builds `plugin` from its own SDK, so on older players `setRanges` and `ranges` do not exist (a `TypeError`), and `osd.show` there takes no options.
|
|
6
|
+
|
|
7
|
+
- **Timeline ranges** (player 0.7.6): `TimelineLayer.setRanges(ranges, { ifCurrent? })` puts spans of the media on the seek bar, each `{ start, end, style, title? }` with `style` `"selection"` (a span the user picked to act on, such as a clip to export; drawn in the accent colour) or `"region"` (a part of the media the plugin describes; drawn dimmer). A layer's marks and ranges are replaced on their own (`set` keeps the ranges, `setRanges` keeps the marks); disposing the layer, its scope or stopping the plugin removes both, and a new source clears every layer. At most 100 ranges per layer, `0 ≤ start < end`, `end` within the media, titles ≤ 100 characters (`TIMELINE_LIMITS`, new, with the marks' 1000); failures go to `onError` as for marks. `plugin.timeline.ranges` (new, `playback.read`) is every layer's ranges merged by start, with `TimelineRange` (`start`, `end`, `style`, `title`, `plugin`); read it in a binding to follow it. Wire: `timeline.setRanges` (needs `timeline.marks`), and `ranges` beside `marks` in `timeline.marks` and `timeline.changed`.
|
|
8
|
+
- **OSD messages with actions** (player 0.7.6): `plugin.osd.show(text, args?, { actions })` ends the message in up to two buttons (`OsdAction`: `title`, translated like the text, 1–40 characters; `command`, the local id of one of the plugin's own commands). The message stays about 4 seconds instead of 2 and does not fade while the pointer is on it and moving (it goes 2 seconds after the pointer leaves or rests, and at the latest 30 seconds after it showed); a click runs the command as if picked from a menu and removes the message. It stays one line, and nothing in it is created or removed while the pointer moves. A newer message replaces it with its buttons; the buttons go when the plugin stops, and while they show the plugin is not stopped for being idle. `osd.show` rejects with `invalid` for more than two actions, a bad title or a command the plugin has not declared. `OSD_LIMITS` gains `actions` (2) and `actionChars` (40). Older players ignore `actions`.
|
|
9
|
+
- **`TestHost`**: `timeline.rangeLayers`, `timeline.ranges` and `setTimelineRanges(pluginId, ranges)` (another plugin's ranges); `timeline.setRanges` checked as the player does; `osdActions` (each message's buttons, in step with `osd`) and `pressOsdAction(index)`, which runs the command like a click.
|
|
10
|
+
- **A bottom bar's `content-full-width`** (player 0.7.6, docs only): a bar that drops controls of its own also reports its row with every control shown in `content-full-width`, a public `length` property declared like `content-min-width`. The player then gives plugin buttons only the width beyond it, so every plugin button moves to » before the bar drops a control, and narrowing the window never brings a button back. A bar with only `content-min-width` keeps the earlier rule. [Writing a bottom bar](docs/guidelines/interface.md#writing-a-bottom-bar) and [Reference: theme](docs/reference/theme.md#bar-buttons) say so.
|
|
11
|
+
|
|
3
12
|
## 1.6.8
|
|
4
13
|
|
|
5
14
|
**Who must change: nobody; authors of a bottom bar should move to `content-min-width`.** Documentation, a read-only Slint property and a removed CLI warning: no change to the TypeScript API, the wire or `TestHost`, `apiVersion` stays `vestra/1`, no new capability. A plugin's `button` declarations stay as they are. A bottom bar that still places `BarButtons` keeps working on player 0.7.6 (it shows that strip, and the player draws no second one); it is deprecated and goes in a later patch.
|
package/dist/api-requests.js
CHANGED
|
@@ -71,8 +71,12 @@ export const API_REQUESTS = Object.freeze({
|
|
|
71
71
|
"Subtitles.remove": [{ method: "subtitles.remove" }, readsSubtitles],
|
|
72
72
|
"Subtitles.setStyle": [{ method: "subtitles.style" }, readsSubtitles],
|
|
73
73
|
"Timeline.marks": [{ method: "timeline.marks" }],
|
|
74
|
+
"Timeline.ranges": [{ method: "timeline.marks" }],
|
|
74
75
|
"Timeline.onChange": [listen("timeline.changed")],
|
|
75
|
-
"Timeline.layer": [
|
|
76
|
+
"Timeline.layer": [
|
|
77
|
+
{ method: "timeline.set" },
|
|
78
|
+
{ method: "timeline.setRanges" },
|
|
79
|
+
],
|
|
76
80
|
"MainWindow.onChange": [listen("window.state")],
|
|
77
81
|
"MainWindow.setVideoArea": [{ method: "window.setVideoArea" }],
|
|
78
82
|
"MainWindow.setAlwaysOnTop": [{ method: "window.setAlwaysOnTop" }],
|
package/dist/docs-tree.json.gz
CHANGED
|
Binary file
|
|
@@ -300,7 +300,10 @@ export const HOST_CAPABILITIES = {
|
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
label: "Show marks on the progress bar",
|
|
303
|
-
methods: [
|
|
303
|
+
methods: [
|
|
304
|
+
{ name: "timeline.set", needs: "timeline.marks" },
|
|
305
|
+
{ name: "timeline.setRanges", needs: "timeline.marks" },
|
|
306
|
+
],
|
|
304
307
|
name: "timeline.marks",
|
|
305
308
|
requires: [],
|
|
306
309
|
residency: "live",
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { HOST_CAPABILITIES } from "./host-capabilities.ts";
|
|
|
16
16
|
export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
|
|
17
17
|
export { BINDING_INPUTS } from "./plugin/bindings.ts";
|
|
18
18
|
export { OSD_LIMITS } from "./plugin/osd.ts";
|
|
19
|
+
export { TIMELINE_LIMITS } from "./plugin/timeline.ts";
|
|
19
20
|
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.ts";
|
|
20
21
|
export { SUBTITLE_LIMITS } from "./plugin/subtitles.ts";
|
|
21
22
|
export { VIDEO_PROCESSING_LIMITS } from "./processing.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -14,6 +14,7 @@ export { HOST_CAPABILITIES } from "./host-capabilities.js";
|
|
|
14
14
|
export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
|
|
15
15
|
export { BINDING_INPUTS } from "./plugin/bindings.js";
|
|
16
16
|
export { OSD_LIMITS } from "./plugin/osd.js";
|
|
17
|
+
export { TIMELINE_LIMITS } from "./plugin/timeline.js";
|
|
17
18
|
export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.js";
|
|
18
19
|
export { SUBTITLE_LIMITS } from "./plugin/subtitles.js";
|
|
19
20
|
export { VIDEO_PROCESSING_LIMITS } from "./processing.js";
|
package/dist/plugin/osd.d.ts
CHANGED
|
@@ -46,6 +46,10 @@ export declare const OSD_LIMITS: Readonly<{
|
|
|
46
46
|
readonly minColumns: 8;
|
|
47
47
|
/** The largest `OsdPanelOptions.maxColumns` (1000). */
|
|
48
48
|
readonly maxColumns: 1000;
|
|
49
|
+
/** Action buttons of one `osd.show` message (`OsdShowOptions.actions`; player 0.7.6) (2). */
|
|
50
|
+
readonly actions: 2;
|
|
51
|
+
/** Characters of an action's title, as shown (40; at least 1 that is not white space). */
|
|
52
|
+
readonly actionChars: 40;
|
|
49
53
|
}>;
|
|
50
54
|
/**
|
|
51
55
|
* Copy and check template arguments: an array or object of at most 16
|
package/dist/plugin/osd.js
CHANGED
|
@@ -47,7 +47,32 @@ export const OSD_LIMITS = Object.freeze({
|
|
|
47
47
|
minColumns: 8,
|
|
48
48
|
/** The largest `OsdPanelOptions.maxColumns` (1000). */
|
|
49
49
|
maxColumns: 1000,
|
|
50
|
+
/** Action buttons of one `osd.show` message (`OsdShowOptions.actions`; player 0.7.6) (2). */
|
|
51
|
+
actions: 2,
|
|
52
|
+
/** Characters of an action's title, as shown (40; at least 1 that is not white space). */
|
|
53
|
+
actionChars: 40,
|
|
50
54
|
});
|
|
55
|
+
/** Copy and check `OsdShowOptions.actions`: what the SDK can check before the player does. */
|
|
56
|
+
function osdActions(options) {
|
|
57
|
+
if (options === undefined || options === null)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (typeof options !== "object")
|
|
60
|
+
throw failure("invalid", "options is an object");
|
|
61
|
+
const actions = options.actions;
|
|
62
|
+
if (actions === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (!Array.isArray(actions) || actions.length > OSD_LIMITS.actions)
|
|
65
|
+
throw failure("invalid", `actions is a list of at most ${OSD_LIMITS.actions}`);
|
|
66
|
+
return actions.map((action, i) => {
|
|
67
|
+
const { title, command } = (action ?? {});
|
|
68
|
+
if (typeof command !== "string" || command === "")
|
|
69
|
+
throw failure("invalid", `actions[${i}].command is a command id`);
|
|
70
|
+
return {
|
|
71
|
+
title: shownText(title, undefined, `actions[${i}].title`),
|
|
72
|
+
command,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
51
76
|
/**
|
|
52
77
|
* Copy and check template arguments: an array or object of at most 16
|
|
53
78
|
* strings, finite numbers, texts and lists (`TEXT_LIMITS`).
|
|
@@ -134,6 +159,6 @@ class PanelHandle {
|
|
|
134
159
|
}
|
|
135
160
|
}
|
|
136
161
|
export const osd = (scope) => ({
|
|
137
|
-
show: async (text, args) => scope.runtime.host.osd.show(shownText(text, args), templateArgs(args)),
|
|
162
|
+
show: async (text, args, options) => scope.runtime.host.osd.show(shownText(text, args), templateArgs(args), osdActions(options)),
|
|
138
163
|
panel: () => new PanelHandle(scope),
|
|
139
164
|
});
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { BuiltinCommand, BuiltinSubmenu } from "../commands.ts";
|
|
|
7
7
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.ts";
|
|
8
8
|
import type { CapabilityInfo } from "../capabilities.ts";
|
|
9
9
|
import { OSD_LIMITS } from "./osd.ts";
|
|
10
|
+
import { TIMELINE_LIMITS } from "./timeline.ts";
|
|
10
11
|
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.ts";
|
|
11
12
|
import { SUBTITLE_LIMITS } from "./subtitles.ts";
|
|
12
13
|
import { BINDING_INPUTS } from "./bindings.ts";
|
|
@@ -36,6 +37,8 @@ export type { CapabilityInfo };
|
|
|
36
37
|
export { MESSAGE_LIMIT };
|
|
37
38
|
/** The player's limits on the on-screen display. */
|
|
38
39
|
export { OSD_LIMITS };
|
|
40
|
+
/** The player's limits on a timeline layer. */
|
|
41
|
+
export { TIMELINE_LIMITS };
|
|
39
42
|
/** The player's limits on stored secrets. */
|
|
40
43
|
export { SECRET_LIMITS };
|
|
41
44
|
/** The player's limits on `plugin.storage.session`. */
|
|
@@ -647,6 +650,43 @@ export declare class TestHost implements Transport {
|
|
|
647
650
|
* template and args the plugin sent are in `host.sent("osd.show")`.
|
|
648
651
|
*/
|
|
649
652
|
readonly osd: string[];
|
|
653
|
+
/**
|
|
654
|
+
* The action buttons of each `osd.show` message, in step with `osd`
|
|
655
|
+
* (index for index): each button's title as shown and its command; `[]`
|
|
656
|
+
* for a message without actions (SDK 1.6.9).
|
|
657
|
+
*/
|
|
658
|
+
readonly osdActions: {
|
|
659
|
+
/** The button title as shown, in the plugin's locale. */
|
|
660
|
+
title: string;
|
|
661
|
+
/** The local id of the plugin's command the button runs. */
|
|
662
|
+
command: string;
|
|
663
|
+
}[][];
|
|
664
|
+
/**
|
|
665
|
+
* Press button `index` of the newest OSD message, as the user does
|
|
666
|
+
* (player 0.7.6): its command runs as if picked from a menu, and settles.
|
|
667
|
+
* A disabled or busy command does nothing, as in the player.
|
|
668
|
+
*
|
|
669
|
+
* @param index - 0 or 1.
|
|
670
|
+
*
|
|
671
|
+
* @throws `Error` (rejects) The newest message has no button `index`.
|
|
672
|
+
*
|
|
673
|
+
* @example Press "Show in Folder" on the saved message
|
|
674
|
+
* The test checks the button the plugin put on its newest message, then
|
|
675
|
+
* presses it as the user would: the plugin's `reveal` command runs.
|
|
676
|
+
* ```ts
|
|
677
|
+
* import assert from "node:assert/strict";
|
|
678
|
+
* declare const host: TestHost;
|
|
679
|
+
* // ---cut---
|
|
680
|
+
* assert.deepEqual(host.osdActions.at(-1), [
|
|
681
|
+
* { title: "Show in Folder", command: "reveal" },
|
|
682
|
+
* ]);
|
|
683
|
+
* await host.pressOsdAction(0); // runs the plugin's `reveal` command
|
|
684
|
+
* ```
|
|
685
|
+
*
|
|
686
|
+
* @capability None.
|
|
687
|
+
* @since 1.6.9
|
|
688
|
+
*/
|
|
689
|
+
pressOsdAction(index: number): Promise<void>;
|
|
650
690
|
/** The text of this plugin's osd panel as shown, `null` when hidden. */
|
|
651
691
|
osdPanel: string | null;
|
|
652
692
|
/**
|
|
@@ -948,7 +988,65 @@ export declare class TestHost implements Transport {
|
|
|
948
988
|
/** The mark's title, `null` without one. */
|
|
949
989
|
title: string | null;
|
|
950
990
|
}[];
|
|
991
|
+
/**
|
|
992
|
+
* Each plugin's ranges by plugin id; layers without ranges are absent
|
|
993
|
+
* (SDK 1.6.9).
|
|
994
|
+
*/
|
|
995
|
+
rangeLayers: Map<string, {
|
|
996
|
+
/** Seconds from the start. */
|
|
997
|
+
start: number;
|
|
998
|
+
/** Seconds from the start, after `start`. */
|
|
999
|
+
end: number;
|
|
1000
|
+
/** `"selection"` or `"region"`. */
|
|
1001
|
+
style: string;
|
|
1002
|
+
/** The range's title, `null` without one. */
|
|
1003
|
+
title: string | null;
|
|
1004
|
+
}[]>;
|
|
1005
|
+
/** Every layer's ranges merged, sorted by start, with the plugin's id. */
|
|
1006
|
+
readonly ranges: {
|
|
1007
|
+
/** The manifest id of the plugin that published the range. */
|
|
1008
|
+
plugin: string;
|
|
1009
|
+
/** Seconds from the start. */
|
|
1010
|
+
start: number;
|
|
1011
|
+
/** Seconds from the start, after `start`. */
|
|
1012
|
+
end: number;
|
|
1013
|
+
/** `"selection"` or `"region"`. */
|
|
1014
|
+
style: string;
|
|
1015
|
+
/** The range's title, `null` without one. */
|
|
1016
|
+
title: string | null;
|
|
1017
|
+
}[];
|
|
951
1018
|
};
|
|
1019
|
+
/**
|
|
1020
|
+
* Simulate another plugin publishing its timeline ranges (or clearing
|
|
1021
|
+
* them with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
1022
|
+
*
|
|
1023
|
+
* @param pluginId - The other plugin's manifest id.
|
|
1024
|
+
* @param ranges - All its ranges.
|
|
1025
|
+
*
|
|
1026
|
+
* @example Let another plugin publish a clip range
|
|
1027
|
+
* The plugin under test sees a range another plugin put on the seek bar:
|
|
1028
|
+
* `setTimelineRanges` stores that plugin's ranges and pushes
|
|
1029
|
+
* `timeline.changed`, and `settle()` lets it arrive.
|
|
1030
|
+
* ```ts
|
|
1031
|
+
* import assert from "node:assert/strict";
|
|
1032
|
+
* declare const host: TestHost;
|
|
1033
|
+
* // ---cut---
|
|
1034
|
+
* host.setTimelineRanges("community.clips", [
|
|
1035
|
+
* { start: 40, end: 52.5, style: "selection", title: "Clip" },
|
|
1036
|
+
* ]);
|
|
1037
|
+
* await host.settle();
|
|
1038
|
+
* assert.equal(plugin.timeline.ranges[0]?.plugin, "community.clips");
|
|
1039
|
+
* ```
|
|
1040
|
+
*
|
|
1041
|
+
* @capability None.
|
|
1042
|
+
* @since 1.6.9
|
|
1043
|
+
*/
|
|
1044
|
+
setTimelineRanges(pluginId: string, ranges: {
|
|
1045
|
+
start: number;
|
|
1046
|
+
end: number;
|
|
1047
|
+
style: string;
|
|
1048
|
+
title?: string | null;
|
|
1049
|
+
}[]): void;
|
|
952
1050
|
/**
|
|
953
1051
|
* Simulate another plugin publishing its timeline layer (or clearing it
|
|
954
1052
|
* with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
package/dist/plugin/testing.js
CHANGED
|
@@ -76,6 +76,7 @@ import { localSocketPath } from "./local.js";
|
|
|
76
76
|
import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "../commands.js";
|
|
77
77
|
import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
|
|
78
78
|
import { OSD_LIMITS } from "./osd.js";
|
|
79
|
+
import { TIMELINE_LIMITS } from "./timeline.js";
|
|
79
80
|
import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
|
|
80
81
|
import { SUBTITLE_LIMITS } from "./subtitles.js";
|
|
81
82
|
import { BINDING_INPUTS, canonical, keyLabel } from "./bindings.js";
|
|
@@ -105,6 +106,8 @@ export { CAPABILITY_INFO };
|
|
|
105
106
|
export { MESSAGE_LIMIT };
|
|
106
107
|
/** The player's limits on the on-screen display. */
|
|
107
108
|
export { OSD_LIMITS };
|
|
109
|
+
/** The player's limits on a timeline layer. */
|
|
110
|
+
export { TIMELINE_LIMITS };
|
|
108
111
|
/** The player's limits on stored secrets. */
|
|
109
112
|
export { SECRET_LIMITS };
|
|
110
113
|
/** The player's limits on `plugin.storage.session`. */
|
|
@@ -536,6 +539,46 @@ export class TestHost {
|
|
|
536
539
|
* template and args the plugin sent are in `host.sent("osd.show")`.
|
|
537
540
|
*/
|
|
538
541
|
osd = [];
|
|
542
|
+
/**
|
|
543
|
+
* The action buttons of each `osd.show` message, in step with `osd`
|
|
544
|
+
* (index for index): each button's title as shown and its command; `[]`
|
|
545
|
+
* for a message without actions (SDK 1.6.9).
|
|
546
|
+
*/
|
|
547
|
+
osdActions = [];
|
|
548
|
+
/**
|
|
549
|
+
* Press button `index` of the newest OSD message, as the user does
|
|
550
|
+
* (player 0.7.6): its command runs as if picked from a menu, and settles.
|
|
551
|
+
* A disabled or busy command does nothing, as in the player.
|
|
552
|
+
*
|
|
553
|
+
* @param index - 0 or 1.
|
|
554
|
+
*
|
|
555
|
+
* @throws `Error` (rejects) The newest message has no button `index`.
|
|
556
|
+
*
|
|
557
|
+
* @example Press "Show in Folder" on the saved message
|
|
558
|
+
* The test checks the button the plugin put on its newest message, then
|
|
559
|
+
* presses it as the user would: the plugin's `reveal` command runs.
|
|
560
|
+
* ```ts
|
|
561
|
+
* import assert from "node:assert/strict";
|
|
562
|
+
* declare const host: TestHost;
|
|
563
|
+
* // ---cut---
|
|
564
|
+
* assert.deepEqual(host.osdActions.at(-1), [
|
|
565
|
+
* { title: "Show in Folder", command: "reveal" },
|
|
566
|
+
* ]);
|
|
567
|
+
* await host.pressOsdAction(0); // runs the plugin's `reveal` command
|
|
568
|
+
* ```
|
|
569
|
+
*
|
|
570
|
+
* @capability None.
|
|
571
|
+
* @since 1.6.9
|
|
572
|
+
*/
|
|
573
|
+
async pressOsdAction(index) {
|
|
574
|
+
const action = this.osdActions.at(-1)?.[index];
|
|
575
|
+
if (!action)
|
|
576
|
+
throw new Error(`the newest OSD message has no button ${index}`);
|
|
577
|
+
const command = this.commands.get(action.command);
|
|
578
|
+
if (!command || command.state.busy || !command.state.enabled)
|
|
579
|
+
return;
|
|
580
|
+
await command.run();
|
|
581
|
+
}
|
|
539
582
|
/** The text of this plugin's osd panel as shown, `null` when hidden. */
|
|
540
583
|
osdPanel = null;
|
|
541
584
|
#locale;
|
|
@@ -900,7 +943,59 @@ export class TestHost {
|
|
|
900
943
|
})))
|
|
901
944
|
.sort((a, b) => a.time - b.time);
|
|
902
945
|
},
|
|
946
|
+
/**
|
|
947
|
+
* Each plugin's ranges by plugin id; layers without ranges are absent
|
|
948
|
+
* (SDK 1.6.9).
|
|
949
|
+
*/
|
|
950
|
+
rangeLayers: new Map(),
|
|
951
|
+
/** Every layer's ranges merged, sorted by start, with the plugin's id. */
|
|
952
|
+
get ranges() {
|
|
953
|
+
return [...this.rangeLayers]
|
|
954
|
+
.flatMap(([plugin, ranges]) => ranges.map((range) => ({
|
|
955
|
+
...range,
|
|
956
|
+
/** The manifest id of the plugin that published the range. */
|
|
957
|
+
plugin,
|
|
958
|
+
})))
|
|
959
|
+
.sort((a, b) => a.start - b.start);
|
|
960
|
+
},
|
|
903
961
|
};
|
|
962
|
+
/**
|
|
963
|
+
* Simulate another plugin publishing its timeline ranges (or clearing
|
|
964
|
+
* them with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
965
|
+
*
|
|
966
|
+
* @param pluginId - The other plugin's manifest id.
|
|
967
|
+
* @param ranges - All its ranges.
|
|
968
|
+
*
|
|
969
|
+
* @example Let another plugin publish a clip range
|
|
970
|
+
* The plugin under test sees a range another plugin put on the seek bar:
|
|
971
|
+
* `setTimelineRanges` stores that plugin's ranges and pushes
|
|
972
|
+
* `timeline.changed`, and `settle()` lets it arrive.
|
|
973
|
+
* ```ts
|
|
974
|
+
* import assert from "node:assert/strict";
|
|
975
|
+
* declare const host: TestHost;
|
|
976
|
+
* // ---cut---
|
|
977
|
+
* host.setTimelineRanges("community.clips", [
|
|
978
|
+
* { start: 40, end: 52.5, style: "selection", title: "Clip" },
|
|
979
|
+
* ]);
|
|
980
|
+
* await host.settle();
|
|
981
|
+
* assert.equal(plugin.timeline.ranges[0]?.plugin, "community.clips");
|
|
982
|
+
* ```
|
|
983
|
+
*
|
|
984
|
+
* @capability None.
|
|
985
|
+
* @since 1.6.9
|
|
986
|
+
*/
|
|
987
|
+
setTimelineRanges(pluginId, ranges) {
|
|
988
|
+
if (ranges.length)
|
|
989
|
+
this.timeline.rangeLayers.set(pluginId, ranges.map((r) => ({
|
|
990
|
+
start: r.start,
|
|
991
|
+
end: r.end,
|
|
992
|
+
style: r.style,
|
|
993
|
+
title: r.title ?? null,
|
|
994
|
+
})));
|
|
995
|
+
else
|
|
996
|
+
this.timeline.rangeLayers.delete(pluginId);
|
|
997
|
+
this.#timelineChanged();
|
|
998
|
+
}
|
|
904
999
|
/**
|
|
905
1000
|
* Simulate another plugin publishing its timeline layer (or clearing it
|
|
906
1001
|
* with `[]`); pushes `timeline.changed` when the plugin subscribed.
|
|
@@ -941,7 +1036,10 @@ export class TestHost {
|
|
|
941
1036
|
this.#timelineChanged();
|
|
942
1037
|
}
|
|
943
1038
|
#timelineChanged() {
|
|
944
|
-
this.emit("timeline.changed", {
|
|
1039
|
+
this.emit("timeline.changed", {
|
|
1040
|
+
marks: this.timeline.marks,
|
|
1041
|
+
ranges: this.timeline.ranges,
|
|
1042
|
+
});
|
|
945
1043
|
}
|
|
946
1044
|
/**
|
|
947
1045
|
* Runtime images written to views (`plugin.ui.image`,
|
|
@@ -3663,8 +3761,9 @@ export class TestHost {
|
|
|
3663
3761
|
next.terminalReason = null;
|
|
3664
3762
|
next.capabilities = { ...readyCapabilities };
|
|
3665
3763
|
next.loop = null; // a new source clears the A-B loop
|
|
3666
|
-
if (this.timeline.layers.size) {
|
|
3764
|
+
if (this.timeline.layers.size || this.timeline.rangeLayers.size) {
|
|
3667
3765
|
this.timeline.layers.clear(); // and every timeline layer
|
|
3766
|
+
this.timeline.rangeLayers.clear();
|
|
3668
3767
|
queueMicrotask(() => this.#timelineChanged());
|
|
3669
3768
|
}
|
|
3670
3769
|
}
|
|
@@ -4452,7 +4551,22 @@ export class TestHost {
|
|
|
4452
4551
|
const text = this.#shown(p.text, p.args, "text").replace(/[\n\r]/g, " ");
|
|
4453
4552
|
if (!text.trim() || [...text].length > OSD_LIMITS.messageChars)
|
|
4454
4553
|
throw new HostError("invalid", `osd text needs 1..${OSD_LIMITS.messageChars} characters`);
|
|
4554
|
+
// Player 0.7.6: up to 2 buttons, each a command of this plugin.
|
|
4555
|
+
const sent = p.actions;
|
|
4556
|
+
if (sent !== undefined &&
|
|
4557
|
+
(!Array.isArray(sent) || sent.length > OSD_LIMITS.actions))
|
|
4558
|
+
throw new HostError("invalid", `actions is a list of at most ${OSD_LIMITS.actions}`);
|
|
4559
|
+
const actions = (sent ?? []).map((action) => {
|
|
4560
|
+
const title = this.#shown(action.title, undefined, "title").replace(/[\n\r]/g, " ");
|
|
4561
|
+
if (!title.trim() || [...title].length > OSD_LIMITS.actionChars)
|
|
4562
|
+
throw new HostError("invalid", `an action title needs 1..=${OSD_LIMITS.actionChars} characters`);
|
|
4563
|
+
const command = String(action.command);
|
|
4564
|
+
if (!this.commands.has(command))
|
|
4565
|
+
throw new HostError("invalid", `${command} is not a command of this plugin`);
|
|
4566
|
+
return { title, command };
|
|
4567
|
+
});
|
|
4455
4568
|
this.osd.push(text);
|
|
4569
|
+
this.osdActions.push(actions);
|
|
4456
4570
|
},
|
|
4457
4571
|
"osd.panel": (p) => {
|
|
4458
4572
|
if (p.text === null)
|
|
@@ -4701,7 +4815,10 @@ export class TestHost {
|
|
|
4701
4815
|
if (next !== undefined)
|
|
4702
4816
|
panel.props[property] = next;
|
|
4703
4817
|
},
|
|
4704
|
-
"timeline.marks": () => ({
|
|
4818
|
+
"timeline.marks": () => ({
|
|
4819
|
+
marks: this.timeline.marks,
|
|
4820
|
+
ranges: this.timeline.ranges,
|
|
4821
|
+
}),
|
|
4705
4822
|
"drops.provide": (p) => {
|
|
4706
4823
|
const id = String(p.id);
|
|
4707
4824
|
const index = this.dropProviders.findIndex((d) => d.id === id);
|
|
@@ -4895,6 +5012,44 @@ export class TestHost {
|
|
|
4895
5012
|
})));
|
|
4896
5013
|
return { applied: true };
|
|
4897
5014
|
},
|
|
5015
|
+
// Player 0.7.6: the layer's ranges, replaced on their own.
|
|
5016
|
+
"timeline.setRanges": (p) => {
|
|
5017
|
+
if (p.sourceId !== undefined &&
|
|
5018
|
+
p.sourceId !== this.source.stamp.generation)
|
|
5019
|
+
return { applied: false };
|
|
5020
|
+
const sent = p.ranges;
|
|
5021
|
+
const duration = this.source.duration;
|
|
5022
|
+
if (!Array.isArray(sent) || sent.length > TIMELINE_LIMITS.ranges)
|
|
5023
|
+
throw new HostError("invalid", `a layer holds at most ${TIMELINE_LIMITS.ranges} ranges`);
|
|
5024
|
+
const ranges = sent.map((range) => ({ ...range }));
|
|
5025
|
+
for (const range of ranges) {
|
|
5026
|
+
const { start, end } = range;
|
|
5027
|
+
const time = (t) => typeof t === "number" && Number.isFinite(t) && t >= 0;
|
|
5028
|
+
if (!time(start) || !time(end))
|
|
5029
|
+
throw new HostError("invalid", "start and end must be numbers ≥ 0");
|
|
5030
|
+
if (end <= start)
|
|
5031
|
+
throw new HostError("invalid", `end ${end} must be after start ${start}`);
|
|
5032
|
+
// As the player (timeline.rs): 1e-6 s of rounding past the end is allowed.
|
|
5033
|
+
if (duration !== null && end > duration + 1e-6)
|
|
5034
|
+
throw new HostError("invalid", `end ${end} is after the end of the media`);
|
|
5035
|
+
if (!["selection", "region"].includes(String(range.style)))
|
|
5036
|
+
throw new HostError("invalid", "style is selection or region");
|
|
5037
|
+
if (isTextValue(range.title))
|
|
5038
|
+
range.title = this.#declared(range.title, "title");
|
|
5039
|
+
if (range.title !== undefined &&
|
|
5040
|
+
range.title !== null &&
|
|
5041
|
+
(typeof range.title !== "string" ||
|
|
5042
|
+
[...range.title].length > TIMELINE_LIMITS.titleChars))
|
|
5043
|
+
throw new HostError("invalid", `title must be ≤ ${TIMELINE_LIMITS.titleChars} characters`);
|
|
5044
|
+
}
|
|
5045
|
+
this.setTimelineRanges(this.manifestId, ranges.map((r) => ({
|
|
5046
|
+
start: r.start,
|
|
5047
|
+
end: r.end,
|
|
5048
|
+
style: String(r.style),
|
|
5049
|
+
title: r.title ?? null,
|
|
5050
|
+
})));
|
|
5051
|
+
return { applied: true };
|
|
5052
|
+
},
|
|
4898
5053
|
"ui.interaction": (p) => void (this.mounted().locks = p),
|
|
4899
5054
|
"dialogs.confirm": (p) => {
|
|
4900
5055
|
this.confirms.push(p);
|
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
import type { Runtime } from "./runtime.ts";
|
|
2
2
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
|
-
import type {
|
|
4
|
-
import type { Timeline, TimelineMark } from "./types.ts";
|
|
3
|
+
import type { WireTimeline } from "../wire.ts";
|
|
4
|
+
import type { Timeline, TimelineMark, TimelineRange } from "./types.ts";
|
|
5
|
+
/**
|
|
6
|
+
* The player's limits on one plugin's timeline layer
|
|
7
|
+
* ({@link TimelineLayer}). A list over them fails with code `invalid`,
|
|
8
|
+
* reported to `Scope.onError` (`set` and `setRanges` never throw). The
|
|
9
|
+
* player's tests check that these agree with the player.
|
|
10
|
+
*
|
|
11
|
+
* @example Keep a long list of ranges inside the limit
|
|
12
|
+
* The plugin publishes the first 100 of its ranges; the player refuses a
|
|
13
|
+
* longer list whole.
|
|
14
|
+
* ```ts
|
|
15
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
16
|
+
* declare const plugin: Plugin;
|
|
17
|
+
* declare const segments: { start: number; end: number }[];
|
|
18
|
+
* // ---cut---
|
|
19
|
+
* import { TIMELINE_LIMITS } from "@vestra-player/sdk/testing";
|
|
20
|
+
*
|
|
21
|
+
* // A longer list would be refused whole.
|
|
22
|
+
* plugin.timeline.layer().setRanges(
|
|
23
|
+
* segments
|
|
24
|
+
* .slice(0, TIMELINE_LIMITS.ranges)
|
|
25
|
+
* .map((s) => ({ ...s, style: "region" as const })),
|
|
26
|
+
* );
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @since 1.6.9
|
|
30
|
+
*
|
|
31
|
+
* @category Testing
|
|
32
|
+
*/
|
|
33
|
+
export declare const TIMELINE_LIMITS: Readonly<{
|
|
34
|
+
/** Marks in one layer (`TimelineLayer.set`) (1000). */
|
|
35
|
+
readonly marks: 1000;
|
|
36
|
+
/** Ranges in one layer (`TimelineLayer.setRanges`; player 0.7.6) (100). */
|
|
37
|
+
readonly ranges: 100;
|
|
38
|
+
/** Characters of a mark's or a range's title, as shown (100). */
|
|
39
|
+
readonly titleChars: 100;
|
|
40
|
+
}>;
|
|
5
41
|
/**
|
|
6
42
|
* One per plugin: read before activation (with `playback.read`), then kept
|
|
7
43
|
* current from `timeline.changed` (with `events`) once the plugin uses it.
|
|
@@ -9,11 +45,12 @@ import type { Timeline, TimelineMark } from "./types.ts";
|
|
|
9
45
|
export declare class TimelineService {
|
|
10
46
|
#private;
|
|
11
47
|
marks: readonly TimelineMark[];
|
|
48
|
+
ranges: readonly TimelineRange[];
|
|
12
49
|
layer: boolean;
|
|
13
50
|
constructor(runtime: Runtime);
|
|
14
51
|
init(): Promise<void>;
|
|
15
52
|
start(): void;
|
|
16
|
-
accept(
|
|
53
|
+
accept(lists: WireTimeline): void;
|
|
17
54
|
read(): Promise<void>;
|
|
18
55
|
watch(listener: (marks: readonly TimelineMark[]) => void): {
|
|
19
56
|
dispose: () => Promise<void>;
|
package/dist/plugin/timeline.js
CHANGED
|
@@ -1,11 +1,54 @@
|
|
|
1
1
|
/** Seek-bar marks (`plugin.timeline`): one layer per plugin, merged by the player. */
|
|
2
2
|
import { failure, Latest, quiet } from "./runtime.js";
|
|
3
|
+
/**
|
|
4
|
+
* The player's limits on one plugin's timeline layer
|
|
5
|
+
* ({@link TimelineLayer}). A list over them fails with code `invalid`,
|
|
6
|
+
* reported to `Scope.onError` (`set` and `setRanges` never throw). The
|
|
7
|
+
* player's tests check that these agree with the player.
|
|
8
|
+
*
|
|
9
|
+
* @example Keep a long list of ranges inside the limit
|
|
10
|
+
* The plugin publishes the first 100 of its ranges; the player refuses a
|
|
11
|
+
* longer list whole.
|
|
12
|
+
* ```ts
|
|
13
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
14
|
+
* declare const plugin: Plugin;
|
|
15
|
+
* declare const segments: { start: number; end: number }[];
|
|
16
|
+
* // ---cut---
|
|
17
|
+
* import { TIMELINE_LIMITS } from "@vestra-player/sdk/testing";
|
|
18
|
+
*
|
|
19
|
+
* // A longer list would be refused whole.
|
|
20
|
+
* plugin.timeline.layer().setRanges(
|
|
21
|
+
* segments
|
|
22
|
+
* .slice(0, TIMELINE_LIMITS.ranges)
|
|
23
|
+
* .map((s) => ({ ...s, style: "region" as const })),
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @since 1.6.9
|
|
28
|
+
*
|
|
29
|
+
* @category Testing
|
|
30
|
+
*/
|
|
31
|
+
export const TIMELINE_LIMITS = Object.freeze({
|
|
32
|
+
/** Marks in one layer (`TimelineLayer.set`) (1000). */
|
|
33
|
+
marks: 1000,
|
|
34
|
+
/** Ranges in one layer (`TimelineLayer.setRanges`; player 0.7.6) (100). */
|
|
35
|
+
ranges: 100,
|
|
36
|
+
/** Characters of a mark's or a range's title, as shown (100). */
|
|
37
|
+
titleChars: 100,
|
|
38
|
+
});
|
|
3
39
|
const view = (marks) => Object.freeze(marks.map((mark) => Object.freeze({
|
|
4
40
|
time: mark.time,
|
|
5
41
|
kind: mark.kind,
|
|
6
42
|
title: mark.title ?? null,
|
|
7
43
|
plugin: mark.plugin,
|
|
8
44
|
})));
|
|
45
|
+
const viewRanges = (ranges) => Object.freeze(ranges.map((range) => Object.freeze({
|
|
46
|
+
start: range.start,
|
|
47
|
+
end: range.end,
|
|
48
|
+
style: range.style,
|
|
49
|
+
title: range.title ?? null,
|
|
50
|
+
plugin: range.plugin,
|
|
51
|
+
})));
|
|
9
52
|
/**
|
|
10
53
|
* One per plugin: read before activation (with `playback.read`), then kept
|
|
11
54
|
* current from `timeline.changed` (with `events`) once the plugin uses it.
|
|
@@ -13,6 +56,7 @@ const view = (marks) => Object.freeze(marks.map((mark) => Object.freeze({
|
|
|
13
56
|
export class TimelineService {
|
|
14
57
|
#runtime;
|
|
15
58
|
marks = Object.freeze([]);
|
|
59
|
+
ranges = Object.freeze([]);
|
|
16
60
|
#listeners = new Set();
|
|
17
61
|
#started = false;
|
|
18
62
|
layer = false;
|
|
@@ -29,26 +73,35 @@ export class TimelineService {
|
|
|
29
73
|
this.#started = true;
|
|
30
74
|
// A mirror: a plugin listens through `watch` and bindings.
|
|
31
75
|
this.#runtime.on("timeline.changed", (data) => {
|
|
32
|
-
const
|
|
33
|
-
if (Array.isArray(marks))
|
|
34
|
-
this.accept(
|
|
76
|
+
const lists = data;
|
|
77
|
+
if (lists && Array.isArray(lists.marks))
|
|
78
|
+
this.accept(lists);
|
|
35
79
|
else
|
|
36
80
|
void this.read();
|
|
37
81
|
}, "events", true);
|
|
38
82
|
}
|
|
39
|
-
accept(
|
|
40
|
-
const
|
|
41
|
-
|
|
83
|
+
accept(lists) {
|
|
84
|
+
const marks = view(lists.marks);
|
|
85
|
+
// Players before 0.7.6 send no ranges.
|
|
86
|
+
const ranges = viewRanges(Array.isArray(lists.ranges) ? lists.ranges : []);
|
|
87
|
+
const marksChanged = JSON.stringify(marks) !== JSON.stringify(this.marks);
|
|
88
|
+
const rangesChanged = JSON.stringify(ranges) !== JSON.stringify(this.ranges);
|
|
89
|
+
if (!marksChanged && !rangesChanged)
|
|
42
90
|
return;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
91
|
+
if (marksChanged)
|
|
92
|
+
this.marks = marks;
|
|
93
|
+
if (rangesChanged)
|
|
94
|
+
this.ranges = ranges;
|
|
95
|
+
// `onChange` follows the marks only, as before ranges existed.
|
|
96
|
+
if (marksChanged)
|
|
97
|
+
for (const listener of [...this.#listeners])
|
|
98
|
+
listener(marks);
|
|
46
99
|
this.#runtime.invalidate();
|
|
47
100
|
}
|
|
48
101
|
async read() {
|
|
49
102
|
const result = await this.#runtime.host.timeline.marks().catch(() => null);
|
|
50
103
|
if (result)
|
|
51
|
-
this.accept(result
|
|
104
|
+
this.accept(result);
|
|
52
105
|
}
|
|
53
106
|
watch(listener) {
|
|
54
107
|
this.#listeners.add(listener);
|
|
@@ -70,6 +123,11 @@ export function timeline(scope) {
|
|
|
70
123
|
scope.runtime.reading("timeline.changed");
|
|
71
124
|
return service().marks;
|
|
72
125
|
},
|
|
126
|
+
get ranges() {
|
|
127
|
+
service().start();
|
|
128
|
+
scope.runtime.reading("timeline.changed");
|
|
129
|
+
return service().ranges;
|
|
130
|
+
},
|
|
73
131
|
onChange(listener) {
|
|
74
132
|
service().start();
|
|
75
133
|
const watch = service().watch((marks) => scope.run(listener, marks));
|
|
@@ -93,12 +151,18 @@ export function timeline(scope) {
|
|
|
93
151
|
class Layer {
|
|
94
152
|
#scope;
|
|
95
153
|
#latest;
|
|
154
|
+
#latestRanges;
|
|
155
|
+
/** Ranges were sent: disposing clears them too. */
|
|
156
|
+
#ranged = false;
|
|
96
157
|
#disposed = false;
|
|
97
158
|
constructor(scope) {
|
|
98
159
|
this.#scope = scope;
|
|
99
160
|
this.#latest = new Latest(async (write) => {
|
|
100
161
|
await scope.runtime.host.timeline.set(write.marks, write.sourceId);
|
|
101
162
|
});
|
|
163
|
+
this.#latestRanges = new Latest(async (write) => {
|
|
164
|
+
await scope.runtime.host.timeline.setRanges(write.ranges, write.sourceId);
|
|
165
|
+
});
|
|
102
166
|
scope.add(this);
|
|
103
167
|
}
|
|
104
168
|
set = (marks, options = {}) => {
|
|
@@ -121,13 +185,41 @@ class Layer {
|
|
|
121
185
|
write.sourceId = options.ifCurrent.sourceId;
|
|
122
186
|
quiet(this.#latest.push(write).catch((error) => this.#scope.report(error)));
|
|
123
187
|
};
|
|
188
|
+
setRanges = (ranges, options = {}) => {
|
|
189
|
+
if (this.#disposed)
|
|
190
|
+
return;
|
|
191
|
+
if (!Array.isArray(ranges)) {
|
|
192
|
+
this.#scope.report(failure("invalid", "ranges must be a list"));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const write = {
|
|
196
|
+
ranges: ranges.map((range) => ({
|
|
197
|
+
start: range?.start,
|
|
198
|
+
end: range?.end,
|
|
199
|
+
style: range?.style,
|
|
200
|
+
...(range?.title === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { title: range.title }),
|
|
203
|
+
})),
|
|
204
|
+
};
|
|
205
|
+
if (options.ifCurrent)
|
|
206
|
+
write.sourceId = options.ifCurrent.sourceId;
|
|
207
|
+
this.#ranged = true;
|
|
208
|
+
quiet(this.#latestRanges
|
|
209
|
+
.push(write)
|
|
210
|
+
.catch((error) => this.#scope.report(error)));
|
|
211
|
+
};
|
|
124
212
|
async dispose() {
|
|
125
213
|
if (this.#disposed)
|
|
126
214
|
return;
|
|
127
215
|
this.#disposed = true;
|
|
128
216
|
this.#scope.forget(this);
|
|
129
217
|
this.#latest.stop();
|
|
218
|
+
this.#latestRanges.stop();
|
|
130
219
|
timelineOf(this.#scope.runtime).layer = false;
|
|
131
220
|
await this.#scope.runtime.host.timeline.set([]).catch(() => { });
|
|
221
|
+
// Only a layer that sent ranges: an older player refuses the method.
|
|
222
|
+
if (this.#ranged)
|
|
223
|
+
await this.#scope.runtime.host.timeline.setRanges([]).catch(() => { });
|
|
132
224
|
}
|
|
133
225
|
}
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -4216,6 +4216,36 @@ export interface TimelineMark {
|
|
|
4216
4216
|
/** The manifest id of the plugin that published it. */
|
|
4217
4217
|
readonly plugin: string;
|
|
4218
4218
|
}
|
|
4219
|
+
/**
|
|
4220
|
+
* How a timeline range is drawn; readers choose colours by it.
|
|
4221
|
+
*
|
|
4222
|
+
* - `"selection"`: a span the user picked to act on, such as a clip to
|
|
4223
|
+
* export. The player's seek bar draws it in the accent colour.
|
|
4224
|
+
* - `"region"`: a part of the media the plugin describes, such as an intro
|
|
4225
|
+
* or a chapter's span. Drawn dimmer, under selections.
|
|
4226
|
+
*
|
|
4227
|
+
* @since 1.6.9
|
|
4228
|
+
* @category Playback
|
|
4229
|
+
*/
|
|
4230
|
+
export type RangeStyle = "selection" | "region";
|
|
4231
|
+
/**
|
|
4232
|
+
* One range on the seek bar (player 0.7.6).
|
|
4233
|
+
*
|
|
4234
|
+
* @since 1.6.9
|
|
4235
|
+
* @category Playback
|
|
4236
|
+
*/
|
|
4237
|
+
export interface TimelineRange {
|
|
4238
|
+
/** Seconds from the start. */
|
|
4239
|
+
readonly start: number;
|
|
4240
|
+
/** Seconds from the start, after `start`. */
|
|
4241
|
+
readonly end: number;
|
|
4242
|
+
/** How it is drawn. */
|
|
4243
|
+
readonly style: RangeStyle;
|
|
4244
|
+
/** Label, `null` without one. */
|
|
4245
|
+
readonly title: string | null;
|
|
4246
|
+
/** The manifest id of the plugin that published it. */
|
|
4247
|
+
readonly plugin: string;
|
|
4248
|
+
}
|
|
4219
4249
|
/**
|
|
4220
4250
|
* This plugin's own marks; disposing it removes them.
|
|
4221
4251
|
*
|
|
@@ -4273,6 +4303,54 @@ export interface TimelineLayer extends Disposable {
|
|
|
4273
4303
|
}[], options?: {
|
|
4274
4304
|
ifCurrent?: PlayerState;
|
|
4275
4305
|
}): void;
|
|
4306
|
+
/**
|
|
4307
|
+
* Replace the layer's ranges (last write wins; `[]` clears them), spans of
|
|
4308
|
+
* the media drawn on the seek bar (player 0.7.6). The marks and the ranges
|
|
4309
|
+
* of a layer are replaced on their own: `set` keeps the ranges and
|
|
4310
|
+
* `setRanges` keeps the marks. Disposing the layer, its scope or stopping
|
|
4311
|
+
* the plugin removes both, and a new source clears every layer.
|
|
4312
|
+
*
|
|
4313
|
+
* At most 100 ranges (`TIMELINE_LIMITS.ranges`), `0 ≤ start < end`, `end`
|
|
4314
|
+
* within the media while its duration is known, titles ≤ 100 characters.
|
|
4315
|
+
* Ranges may overlap. Failures go to `onError`; with `ifCurrent`, a
|
|
4316
|
+
* write for another source is dropped silently. A title is data or a
|
|
4317
|
+
* {@link Text} the player translates, as in `set`.
|
|
4318
|
+
*
|
|
4319
|
+
* Players before 0.7.6 build `plugin` from an SDK without `setRanges`, so
|
|
4320
|
+
* the call throws a `TypeError` there: declare
|
|
4321
|
+
* `"minPlayerVersion": "0.7.6"`.
|
|
4322
|
+
*
|
|
4323
|
+
* @example Show the clip range on the seek bar while a panel is open
|
|
4324
|
+
* The span the user picked shows on the seek bar in the accent colour
|
|
4325
|
+
* while the plugin's tab is showing, and goes when it is not. Two point
|
|
4326
|
+
* marks would show the ends but not the span between them.
|
|
4327
|
+
* ```ts
|
|
4328
|
+
* declare const panel: Panel;
|
|
4329
|
+
* declare const clip: { start: number; end: number };
|
|
4330
|
+
* // ---cut---
|
|
4331
|
+
* const layer = plugin.timeline.layer();
|
|
4332
|
+
* // Only while the tab is on screen; [] takes the range away.
|
|
4333
|
+
* const show = () =>
|
|
4334
|
+
* layer.setRanges(
|
|
4335
|
+
* panel.state?.effective
|
|
4336
|
+
* ? [{ start: clip.start, end: clip.end, style: "selection", title: "Clip" }]
|
|
4337
|
+
* : [],
|
|
4338
|
+
* { ifCurrent: plugin.player.state },
|
|
4339
|
+
* );
|
|
4340
|
+
* panel.onChange(show);
|
|
4341
|
+
* ```
|
|
4342
|
+
*
|
|
4343
|
+
* @capability timeline.marks
|
|
4344
|
+
* @since 1.6.9
|
|
4345
|
+
*/
|
|
4346
|
+
setRanges(ranges: {
|
|
4347
|
+
start: number;
|
|
4348
|
+
end: number;
|
|
4349
|
+
style: RangeStyle;
|
|
4350
|
+
title?: string | Text;
|
|
4351
|
+
}[], options?: {
|
|
4352
|
+
ifCurrent?: PlayerState;
|
|
4353
|
+
}): void;
|
|
4276
4354
|
}
|
|
4277
4355
|
/**
|
|
4278
4356
|
* `plugin.timeline`: marks on the seek bar. Every plugin publishes one
|
|
@@ -4303,6 +4381,38 @@ export interface TimelineLayer extends Disposable {
|
|
|
4303
4381
|
export interface Timeline {
|
|
4304
4382
|
/** Every layer merged, by time (`playback.read`). Empty on a new source. */
|
|
4305
4383
|
readonly marks: readonly TimelineMark[];
|
|
4384
|
+
/**
|
|
4385
|
+
* Every layer's ranges merged, by start (`playback.read`; player 0.7.6:
|
|
4386
|
+
* on older players `ranges` does not exist and reads `undefined`). Empty
|
|
4387
|
+
* on a new source. Kept current like `marks` (with `events`): read it in
|
|
4388
|
+
* a {@link View.bind} function and the view follows it. `onChange` runs
|
|
4389
|
+
* only when the marks change.
|
|
4390
|
+
*
|
|
4391
|
+
* @example Draw the published ranges in a seek bar of your own
|
|
4392
|
+
* A bottom bar draws a band for every range on the seek bar, whoever
|
|
4393
|
+
* published it, as fractions of the duration.
|
|
4394
|
+
* ```ts
|
|
4395
|
+
* declare const bar: View;
|
|
4396
|
+
* // ---cut---
|
|
4397
|
+
* bar.bind(() => {
|
|
4398
|
+
* // Reading `ranges` here runs the binding again when they change.
|
|
4399
|
+
* const duration = plugin.player.state.duration ?? 0;
|
|
4400
|
+
* return {
|
|
4401
|
+
* ranges: duration
|
|
4402
|
+
* ? plugin.timeline.ranges.map((range) => ({
|
|
4403
|
+
* from: range.start / duration,
|
|
4404
|
+
* to: range.end / duration,
|
|
4405
|
+
* selection: range.style === "selection",
|
|
4406
|
+
* }))
|
|
4407
|
+
* : [],
|
|
4408
|
+
* };
|
|
4409
|
+
* });
|
|
4410
|
+
* ```
|
|
4411
|
+
*
|
|
4412
|
+
* @capability playback.read
|
|
4413
|
+
* @since 1.6.9
|
|
4414
|
+
*/
|
|
4415
|
+
readonly ranges: readonly TimelineRange[];
|
|
4306
4416
|
/**
|
|
4307
4417
|
* The merged list changed (needs `events`; at most every 100 ms).
|
|
4308
4418
|
*
|
|
@@ -6700,12 +6810,19 @@ export interface Osd {
|
|
|
6700
6810
|
*
|
|
6701
6811
|
* @param text - The template, 1–200 characters once translated and
|
|
6702
6812
|
* formatted; line breaks become spaces.
|
|
6813
|
+
* With `options.actions` (player 0.7.6) the message ends in up to two
|
|
6814
|
+
* buttons, each running one of the plugin's own commands
|
|
6815
|
+
* ({@link OsdShowOptions}). Older players show the message without them.
|
|
6816
|
+
*
|
|
6703
6817
|
* @param args - Values for its placeholders (player 0.4.3; older players
|
|
6704
|
-
* show the template as it is).
|
|
6818
|
+
* show the template as it is). Pass `null` with a `Text` and options.
|
|
6819
|
+
* @param options - Action buttons after the text (SDK 1.6.9).
|
|
6705
6820
|
* @returns Resolves once the message is on screen.
|
|
6706
6821
|
*
|
|
6707
6822
|
* @throws `invalid` (rejects) The shown text is empty, only white space,
|
|
6708
|
-
* or longer than 200 characters, or `args` is not a {@link TemplateArgs}
|
|
6823
|
+
* or longer than 200 characters, or `args` is not a {@link TemplateArgs};
|
|
6824
|
+
* or there are more than 2 actions, a title is empty or longer than 40
|
|
6825
|
+
* characters once translated, or a command is not one of the plugin's.
|
|
6709
6826
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
6710
6827
|
* @throws `capability` (rejects) The manifest does not declare `ui.osd`.
|
|
6711
6828
|
*
|
|
@@ -6719,9 +6836,28 @@ export interface Osd {
|
|
|
6719
6836
|
* await plugin.osd.show("Speed {}×", [plugin.player.state.rate]);
|
|
6720
6837
|
* ```
|
|
6721
6838
|
*
|
|
6839
|
+
* @example Offer "Show in Folder" after saving a file
|
|
6840
|
+
* The OSD reads "Saved clip.mp4 [Show in Folder]" for about 4 seconds,
|
|
6841
|
+
* longer while the pointer is on it. A click runs the plugin's own
|
|
6842
|
+
* `reveal` command, which knows the last saved file; the message goes.
|
|
6843
|
+
* ```ts
|
|
6844
|
+
* declare const path: string;
|
|
6845
|
+
* // ---cut---
|
|
6846
|
+
* let saved = path; // the command acts on the last saved file
|
|
6847
|
+
* plugin.commands.add({
|
|
6848
|
+
* id: "reveal",
|
|
6849
|
+
* title: "Show Last Saved File in Folder",
|
|
6850
|
+
* enabled: () => saved !== "",
|
|
6851
|
+
* run: () => plugin.system.reveal(saved),
|
|
6852
|
+
* });
|
|
6853
|
+
* await plugin.osd.show("Saved {}", [saved.split(/[\\/]/).pop() ?? saved], {
|
|
6854
|
+
* actions: [{ title: "Show in Folder", command: "reveal" }], // one button
|
|
6855
|
+
* });
|
|
6856
|
+
* ```
|
|
6857
|
+
*
|
|
6722
6858
|
* @since 1.0.0
|
|
6723
6859
|
*/
|
|
6724
|
-
show(text: TextValue, args?: TemplateArgs): Promise<void>;
|
|
6860
|
+
show(text: TextValue, args?: TemplateArgs | null, options?: OsdShowOptions): Promise<void>;
|
|
6725
6861
|
/**
|
|
6726
6862
|
* Create this plugin's information block.
|
|
6727
6863
|
*
|
|
@@ -6748,6 +6884,54 @@ export interface Osd {
|
|
|
6748
6884
|
*/
|
|
6749
6885
|
panel(): OsdPanel;
|
|
6750
6886
|
}
|
|
6887
|
+
/**
|
|
6888
|
+
* One button at the end of an OSD message ({@link Osd.show}, player 0.7.6).
|
|
6889
|
+
*
|
|
6890
|
+
* @since 1.6.9
|
|
6891
|
+
* @category UI
|
|
6892
|
+
*/
|
|
6893
|
+
export interface OsdAction {
|
|
6894
|
+
/**
|
|
6895
|
+
* The button's label: English source text the player translates with the
|
|
6896
|
+
* plugin's catalog, or a {@link Text}; 1–40 characters once translated
|
|
6897
|
+
* (`OSD_LIMITS.actionChars`). A verb, as on a menu item: "Show in
|
|
6898
|
+
* Folder", "Copy", "Undo".
|
|
6899
|
+
*/
|
|
6900
|
+
title: TextValue;
|
|
6901
|
+
/**
|
|
6902
|
+
* The local id of one of this plugin's own commands
|
|
6903
|
+
* ({@link CommandOptions.id}), declared when the message is shown. A
|
|
6904
|
+
* click runs it as if the user picked it from a menu: a disabled command
|
|
6905
|
+
* does nothing. The command takes no arguments, so it acts on what the
|
|
6906
|
+
* plugin remembers (the file it just saved).
|
|
6907
|
+
*/
|
|
6908
|
+
command: string;
|
|
6909
|
+
}
|
|
6910
|
+
/**
|
|
6911
|
+
* Options of {@link Osd.show} (SDK 1.6.9).
|
|
6912
|
+
*
|
|
6913
|
+
* @remarks
|
|
6914
|
+
* A message with actions stays about 4 seconds instead of 2, and does not
|
|
6915
|
+
* fade while the pointer is on it and moving: it goes 2 seconds after the
|
|
6916
|
+
* pointer leaves or rests (the player's idle time), and at the latest 30
|
|
6917
|
+
* seconds after it showed, hovered or not. A click on a button runs its
|
|
6918
|
+
* command and removes the message.
|
|
6919
|
+
* It is still one line: the buttons follow the text. Buttons take no
|
|
6920
|
+
* keyboard focus; screen readers can press them. A newer message, from any
|
|
6921
|
+
* plugin or the player, replaces the message with its buttons, and the
|
|
6922
|
+
* buttons go when the plugin stops. While its buttons show, the plugin is
|
|
6923
|
+
* not stopped for being idle.
|
|
6924
|
+
*
|
|
6925
|
+
* Players before 0.7.6 ignore `actions` and show the text alone for 2
|
|
6926
|
+
* seconds, so do not make an action the only way to reach something.
|
|
6927
|
+
*
|
|
6928
|
+
* @since 1.6.9
|
|
6929
|
+
* @category UI
|
|
6930
|
+
*/
|
|
6931
|
+
export interface OsdShowOptions {
|
|
6932
|
+
/** At most 2 buttons (`OSD_LIMITS.actions`), in order after the text. */
|
|
6933
|
+
actions?: readonly OsdAction[];
|
|
6934
|
+
}
|
|
6751
6935
|
/**
|
|
6752
6936
|
* The state of a filter, display layer or render options handle.
|
|
6753
6937
|
* After `failed` nothing retries automatically until the next `set()`,
|
package/dist/wire.d.ts
CHANGED
|
@@ -262,6 +262,19 @@ export interface WireMark {
|
|
|
262
262
|
title: string | null;
|
|
263
263
|
plugin: string;
|
|
264
264
|
}
|
|
265
|
+
/** One timeline range as the player merges them (player 0.7.6). */
|
|
266
|
+
export interface WireRange {
|
|
267
|
+
start: number;
|
|
268
|
+
end: number;
|
|
269
|
+
style: "selection" | "region";
|
|
270
|
+
title: string | null;
|
|
271
|
+
plugin: string;
|
|
272
|
+
}
|
|
273
|
+
/** `timeline.marks` and `timeline.changed`; `ranges` from player 0.7.6. */
|
|
274
|
+
export interface WireTimeline {
|
|
275
|
+
marks: WireMark[];
|
|
276
|
+
ranges?: WireRange[];
|
|
277
|
+
}
|
|
265
278
|
/** The backdrop as `layout.state` reports it; `null` without one. */
|
|
266
279
|
export interface LayoutBackdrop {
|
|
267
280
|
owner: number;
|
|
@@ -568,9 +581,7 @@ export declare function createWire(transport: Transport): {
|
|
|
568
581
|
};
|
|
569
582
|
/** Mark layers on the seek bar (`plugin.timeline`). */
|
|
570
583
|
timeline: {
|
|
571
|
-
marks: () => Promise<
|
|
572
|
-
marks: WireMark[];
|
|
573
|
-
}>;
|
|
584
|
+
marks: () => Promise<WireTimeline>;
|
|
574
585
|
set: (marks: {
|
|
575
586
|
time: number;
|
|
576
587
|
kind: string;
|
|
@@ -578,6 +589,15 @@ export declare function createWire(transport: Transport): {
|
|
|
578
589
|
}[], sourceId?: number) => Promise<{
|
|
579
590
|
applied: boolean;
|
|
580
591
|
}>;
|
|
592
|
+
/** Player 0.7.6. */
|
|
593
|
+
setRanges: (ranges: {
|
|
594
|
+
start: number;
|
|
595
|
+
end: number;
|
|
596
|
+
style: string;
|
|
597
|
+
title?: string | TextJson;
|
|
598
|
+
}[], sourceId?: number) => Promise<{
|
|
599
|
+
applied: boolean;
|
|
600
|
+
}>;
|
|
581
601
|
};
|
|
582
602
|
/** Chapters and the A-B loop of the current media. */
|
|
583
603
|
marks: {
|
|
@@ -666,7 +686,10 @@ export declare function createWire(transport: Transport): {
|
|
|
666
686
|
};
|
|
667
687
|
/** `text` is a template in the plugin's catalog, formatted with `args` (player 0.4.3). */
|
|
668
688
|
osd: {
|
|
669
|
-
show: (text: TextValueJson, args?: TemplateArgsJson
|
|
689
|
+
show: (text: TextValueJson, args?: TemplateArgsJson, actions?: {
|
|
690
|
+
title: TextValueJson;
|
|
691
|
+
command: string;
|
|
692
|
+
}[]) => Promise<void>;
|
|
670
693
|
panel: (text: TextValueJson | null, args?: TemplateArgsJson, maxColumns?: number) => Promise<void>;
|
|
671
694
|
};
|
|
672
695
|
layout: {
|
package/dist/wire.js
CHANGED
|
@@ -156,6 +156,11 @@ export function createWire(transport) {
|
|
|
156
156
|
marks,
|
|
157
157
|
...(sourceId === undefined ? {} : { sourceId }),
|
|
158
158
|
}),
|
|
159
|
+
/** Player 0.7.6. */
|
|
160
|
+
setRanges: (ranges, sourceId) => transport.request("timeline.setRanges", {
|
|
161
|
+
ranges,
|
|
162
|
+
...(sourceId === undefined ? {} : { sourceId }),
|
|
163
|
+
}),
|
|
159
164
|
},
|
|
160
165
|
/** Chapters and the A-B loop of the current media. */
|
|
161
166
|
marks: {
|
|
@@ -245,9 +250,11 @@ export function createWire(transport) {
|
|
|
245
250
|
},
|
|
246
251
|
/** `text` is a template in the plugin's catalog, formatted with `args` (player 0.4.3). */
|
|
247
252
|
osd: {
|
|
248
|
-
show: (text, args) => transport.request("osd.show", {
|
|
253
|
+
show: (text, args, actions) => transport.request("osd.show", {
|
|
249
254
|
text,
|
|
250
255
|
...(args === undefined ? {} : { args }),
|
|
256
|
+
// Player 0.7.6; older players ignore it.
|
|
257
|
+
...(actions === undefined ? {} : { actions }),
|
|
251
258
|
}),
|
|
252
259
|
panel: (text, args, maxColumns) => transport.request("osd.panel", {
|
|
253
260
|
text,
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.6.
|
|
3
|
+
> @vestra-player/sdk 1.6.9: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
|
|
4
4
|
|
|
5
5
|
## Read the documentation
|
|
6
6
|
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
- `/API/Commands`: 15 entries: CommandState, Bindable, Condition, When, MenuId, BarButtonIcon, CommandButton, CommandOptions, Command, Commands, HostAction, Host, BindingRef, BindingInfo, Bindings
|
|
32
32
|
- `/API/Media`: 6 entries: CaptureOptions, Snapshot, ExportOptions, JobState, ExportJob, MediaCapture
|
|
33
33
|
- `/API/Packages`: 10 entries: PackageManifest, PackageSource, InstallOptions, PackageCandidate, PackageChangeStatus, PackageChange, PluginInfo, PluginDisplay, PluginWarning, Packages
|
|
34
|
-
- `/API/Playback`:
|
|
34
|
+
- `/API/Playback`: 20 entries: MarkKind, TimelineMark, RangeStyle, TimelineRange, TimelineLayer, Timeline, AudioTrack, PlayerState, AudioDevice, Chapter, Conditional, Media, Scrub, StreamFrame, StreamOptions,…
|
|
35
35
|
- `/API/Plugin`: 13 entries: Disposable, PluginError, Scope, Plugin, StopReason, DisposeInfo, WindowState, MainWindow, AppInfo, App, SystemInfo, DisplayInfo, Json
|
|
36
36
|
- `/API/Processing`: 18 entries: FilterStatus, VideoFilter, AudioFilter, ViewSettings, VideoView, RenderSettings, RenderOptions, VideoShaders, UpscalingReasonCode, UpscalingReason, UpscalingDecodingCause,…
|
|
37
37
|
- `/API/Services`: 15 entries: Clipboard, Power, ToolResult, Tools, JsonStore, Storage, SessionStorage, System, Secrets, RenderingMode, RenderingPreference, RenderingReasonCode, RenderingReason, RenderingState,…
|
|
38
38
|
- `/API/Sources`: 7 entries: ResolverOptions, WebResolverOptions, FileResolverOptions, ResolvedStream, Resolver, Resolvers, ResolvedMedia
|
|
39
39
|
- `/API/Subtitles`: 5 entries: SubtitleTrack, SubtitlesState, SubtitleConditional, Subtitles, SubtitleStyle
|
|
40
|
-
- `/API/UI`:
|
|
41
|
-
- `/API/Testing`:
|
|
40
|
+
- `/API/UI`: 44 entries: ImageRef, ImageOptions, MarkdownRef, MarkdownOptions, PropValue, PropData, Props, Ui, View, Window, Windows, SlotState, SlotPanel, PanelState, PanelSlot, SidebarSlot, Panel,…
|
|
41
|
+
- `/API/Testing`: 33 entries: CapabilityInfo, CAPABILITY_INFO, MESSAGE_LIMIT, COMMAND_LIMITS, MENU_IDS, BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BuiltinSubmenu, BUILTIN_SUBMENUS, BuiltinCommand, BUILTIN_COMMANDS, LOCALES,…
|
|
42
42
|
- `/API/Docs`: 4 entries: DOCS_PAGE_BYTES, DocsOptions, docs, searchDocs
|