@vestra-player/sdk 1.6.9 → 1.6.11
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 +24 -1
- package/dist/commands.d.ts +4 -2
- package/dist/commands.js +5 -1
- package/dist/docs-tree.json.gz +0 -0
- package/dist/i18n.js +1 -0
- package/dist/media.d.ts +43 -0
- package/dist/plugin/media.js +46 -1
- package/dist/plugin/testing.d.ts +28 -4
- package/dist/plugin/testing.js +127 -16
- package/dist/plugin/types.d.ts +254 -13
- package/dist/plugin/ui.js +10 -2
- package/dist/wire.d.ts +15 -3
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/slint/vestra/bar-buttons.slint +8 -4
- package/slint/vestra/icons.slint +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.11
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Additions only: a mount slot, a panel position, a button icon and `TestHost` support; `apiVersion` stays `vestra/1`, no new capability. A plugin that mounts `"adjust"` sets `minPlayerVersion` to `0.7.6`: older players refuse the slot (`invalid`), which fails the activation like any refused mount.
|
|
6
|
+
|
|
7
|
+
- **The adjust slot** (player 0.7.6, menu redesign step 3b): `plugin.panels.mount("adjust", file, { title })` puts a panel in the adjust column, for controls that change what is playing (picture, sound, subtitles, speed). Any number of plugins mount there, one tab each. The slot is a contract about purpose; the player decides how it looks and may change that. The rule, in [Slots](docs/explanation/ui.md#slots-what-each-one-is-for) and on `Panels.mount`: if the user still looks at the panel after closing it, it is `"sidebar"`; otherwise it is `"adjust"`. Today the player draws a second column between the video and the sidebar, with text tabs and the opening key under each title (the key of the command the panel's commands name as their `home`), no close or detach button, floating over the video in a narrow window and sliding out from the right edge in fullscreen; it starts closed and is compiled the first time it opens.
|
|
8
|
+
- **`plugin.panels.adjust`**: the position, the same shape as `panels.sidebar` without `setDetached` (`PanelSlot`: `state`, `panels`, `onChange`, `setVisible`, `toggle`, `activate`). `Panel.show` and `Panel.toggle` work on an adjust panel as on a sidebar tab. Players before 0.7.6 build the `plugin` object with their own SDK, which has no `adjust`: there `plugin.panels.adjust` is `undefined` and reading its `state` throws a `TypeError` (as for `commands.keys`), so declare `minPlayerVersion` 0.7.6 or test it first.
|
|
9
|
+
- **Wire**: `ui.mount` and `layout.set` take `slot: "adjust"`; `layout.state` reports `adjust` (a `LayoutSlot`, never `detached`); `LayoutSlot` names the adjust column.
|
|
10
|
+
- **`BAR_BUTTON_ICONS`** gains `"sliders"` (the player's Adjust button), and `BUILTIN_COMMANDS` the player's `host/adjust`. `@vestra/bar-buttons.slint` (synced): `BarButtonsHost.adjust`, counted in `fixed`; `@vestra/icons.slint`: `sliders`.
|
|
11
|
+
- **`TestHost`**: `layout.adjust` (starts hidden), `mountOther("adjust", …)`, `selectTab` for adjust tabs, `layout.set` refuses detaching the adjust column, `keepsRunning` names an open adjust column's current tab; an unknown mount slot is refused with `invalid`, as by the player.
|
|
12
|
+
- **Example** [adjust-panel](docs/examples/adjust-panel.md): a speed slider in the adjust column, opened and closed by its own key.
|
|
13
|
+
|
|
14
|
+
## 1.6.10
|
|
15
|
+
|
|
16
|
+
**Who must change: nobody.** Additions only: a clip format, optional export fields, two optional `JobState` fields, an error code and a `TestHost` field. `apiVersion` stays `vestra/1`, no new capability (`media.control` covers them). A plugin that does not use them sends exactly what it sent before; one that uses them needs player 0.7.6: declare `"minPlayerVersion": "0.7.6"`, or compare `plugin.app.version` with 0.7.6 to offer them only where they exist (do not probe by trying: on an older player `plugin` comes from that player's own, older SDK, which rejects `format: "copy"` with `invalid` but builds a re-encode request without the `encoding` it does not know, so the clip is encoded with the fixed settings and no error), and leave `progress`, `start` and `end` out.
|
|
17
|
+
|
|
18
|
+
- **Stream-copied clips** (`Snapshot.export({ format: "copy", destination, end, container? })`, player 0.7.6): the video and the snapshot's audio track are copied as they are in the file, without re-encoding: lossless and about as fast as the disk. A copy can only begin on a keyframe: it starts at the video's last keyframe at or before the snapshot time (the first keyframe when the video itself starts later; keyframes an MP4 edit list discards do not count) and is cut at `end`, a few frames past it at most. `container` is `auto` (default: MP4 for H.264, HEVC, AV1 or MPEG-4 video with AAC, MP3, AC-3, E-AC-3 or ALAC audio, MKV for anything else), `mp4` or `mkv`; the player gives `destination` the container's extension and never overwrites a file. A copy needs a raw snapshot (`invalid` from a processed one), leaves subtitle tracks and chapters out, and keeps the snapshot lease, the job limits and cancellation of every export. One the player cannot make (no video stream, a cover image, an external audio track, codecs `mp4` cannot hold) ends `failed` with the new code **`unsupported`**. See `Snapshot.export`.
|
|
19
|
+
- **The range written** (player 0.7.6): a finished clip's `outcome` has `start` and `end`, the source times of its first frame and of the end of its last frame: a copy's earliest written frame (its keyframe, or an open GOP's leading frames) and its actual end, a re-encode's start and start plus the encoded length.
|
|
20
|
+
- **`JobState.progress`** (player 0.7.6): `{ done, total }`, seconds of the clip written and to write, from FFmpeg's progress output, at most 4 updates a second; `null` while unknown (before writing starts, and for frames). The player still checks the file after `done` reaches `total`.
|
|
21
|
+
- **`ExportEncoding`** (`encoding` of `format: "mp4"`, player 0.7.6): `crf` (14–30), `speed` (`faster`, `balanced`, `smaller`), `maxHeight` (1080, 720, 480; a ceiling), `fps` (60, 30; a ceiling), `audioBitrate` (128, 192, 256 kb/s) `threads` (`single`, or `auto`: half the logical processors, at most 8) and `videoBitrate: "source"` (aim at the source video's average bitrate instead of a CRF, peaks up to 1.5 times it: a "Same as source" preset; not with `crf`; `unsupported` when the player cannot tell the source's bitrate). A closed set, never encoder arguments; the SDK and the player refuse other values with `invalid`. Without it, clips are encoded as before.
|
|
22
|
+
- **Error codes**: `export-failed` (an export job's failure) and `unsupported` (a copy the player cannot make, a source bitrate it cannot tell) are listed in [Reference: errors](docs/reference/errors.md).
|
|
23
|
+
- **`TestHost`**: `jobs.start` takes `rawCopy`, `encoding` and `container` and refuses what the player refuses, now also a job kind that does not match the snapshot (`rawPng` from a processed snapshot, say: `invalid`, as the player answers; plugins that go through `Snapshot.export` never send one); a running clip reports half its `progress`, and a finished one `start` and `end` (a copy 1 s before the snapshot time, as if snapped to a keyframe, its destination named for the container). **`TestHost.exportFailure`** makes running exports fail with a given code, for testing a plugin's `unsupported` path.
|
|
24
|
+
- **Exports run at below-normal priority** (player 0.7.6): the player starts the FFmpeg that writes a frame or a clip below normal priority (Windows `BELOW_NORMAL_PRIORITY_CLASS`, `nice 10` elsewhere), so playback keeps its rate while an export uses the cores; probes stay at normal priority.
|
|
25
|
+
|
|
3
26
|
## 1.6.9
|
|
4
27
|
|
|
5
28
|
**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.
|
|
@@ -29,7 +52,7 @@
|
|
|
29
52
|
- **`plugin.commands.keys(id)`** (player 0.7.6): the keys of one of the plugin's **own** commands, as the player's menus and command palette write them (`["W"]`, `["Ctrl+B"]`, `["⌘B"]` on macOS, arrows as `←`), always the current binding: the user's keys after a change, `[]` when the user cleared them or turned on Ignore Default Shortcuts, `[]` for a key another command holds; mouse bindings and (outside macOS) `Cmd` bindings left out. Read-only and synchronous; read in a `bind` function, the view follows every change. Another plugin's commands, or an unknown id, read `[]`; it needs `commands` only, not `bindings.manage`. The first read asks the player (`commands.keys`) and subscribes to the `commands.keys` event, which the player sends only to the plugin whose own labels changed, at most every 200 ms; until the first answer every command reads `[]`. See [Key hints beside controls](docs/explanation/commands.md#key-hints-beside-controls).
|
|
30
53
|
- **`plugin.commands.onKeysChange(listener)`** (player 0.7.6): runs after any of the plugin's keys changed. `commands.keys` is a wake event, like `bindings.changed`: a listener (or a binding that reads `keys`) does not keep a plugin running while idle, and a plugin in standby is started for a change of its keys.
|
|
31
54
|
- **`KeyHint`** in `@vestra/widgets.slint`: a key cap (monospace `font-xs`, `text-dim`, a 1px border, both palettes); an empty `text` draws nothing and takes no space.
|
|
32
|
-
- **`ToolTipArea.keys`** (a list of the new struct `ToolTipKey`, `{ key, text }`) and **`ToolTipArea.detail`**: the tip reads "Brightness
|
|
55
|
+
- **`ToolTipArea.keys`** (a list of the new struct `ToolTipKey`, `{ key, text }`) and **`ToolTipArea.detail`**: the tip reads "Brightness [W] − [E] + · double-click: Default" with key caps. Without them the tip is the plain text tip as before.
|
|
33
56
|
- **`TestHost.rebind(command, inputs | null)`** changes or clears a key of the plugin's command as the user does; `TestHost` answers `commands.keys` and pushes the event like the player, writing labels for `host.systemInfo.os` (macOS by default: `⇧W`, `⌘B`). `bindings.set` and `bindings.reset` push it too.
|
|
34
57
|
- The [theme-widgets example](examples/theme-widgets) shows a slider's two commands (F7, F8) as key caps and in its tooltip.
|
|
35
58
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -136,7 +136,9 @@ export declare const MENU_IDS: readonly ["open", "playback", "subtitles", "video
|
|
|
136
136
|
* The icons a bottom-bar button may name (`CommandOptions.button.icon`):
|
|
137
137
|
* every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
|
|
138
138
|
* in your own Slint with `Icon { name: "store"; }`). The player's tests check
|
|
139
|
-
* that this list is its icon set.
|
|
139
|
+
* that this list is its icon set. `"sliders"` (three sliders, the player's
|
|
140
|
+
* Adjust button) is new in player 0.7.6 (SDK 1.6.11); older players refuse
|
|
141
|
+
* it as a button icon with `invalid`.
|
|
140
142
|
*
|
|
141
143
|
* @example Check in a test that the button's icon exists
|
|
142
144
|
* The test fails before release if the plugin's bar button names an icon
|
|
@@ -154,7 +156,7 @@ export declare const MENU_IDS: readonly ["open", "playback", "subtitles", "video
|
|
|
154
156
|
*
|
|
155
157
|
* @category Testing
|
|
156
158
|
*/
|
|
157
|
-
export declare const BAR_BUTTON_ICONS: readonly ["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"];
|
|
159
|
+
export declare const BAR_BUTTON_ICONS: readonly ["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", "sliders"];
|
|
158
160
|
/**
|
|
159
161
|
* The player's limits on bottom-bar buttons (`CommandOptions.button`), the
|
|
160
162
|
* same values as the player's (its tests check that they agree). A button
|
package/dist/commands.js
CHANGED
|
@@ -141,7 +141,9 @@ export const MENU_IDS = [
|
|
|
141
141
|
* The icons a bottom-bar button may name (`CommandOptions.button.icon`):
|
|
142
142
|
* every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
|
|
143
143
|
* in your own Slint with `Icon { name: "store"; }`). The player's tests check
|
|
144
|
-
* that this list is its icon set.
|
|
144
|
+
* that this list is its icon set. `"sliders"` (three sliders, the player's
|
|
145
|
+
* Adjust button) is new in player 0.7.6 (SDK 1.6.11); older players refuse
|
|
146
|
+
* it as a button icon with `invalid`.
|
|
145
147
|
*
|
|
146
148
|
* @example Check in a test that the button's icon exists
|
|
147
149
|
* The test fails before release if the plugin's bar button names an icon
|
|
@@ -198,6 +200,7 @@ export const BAR_BUTTON_ICONS = [
|
|
|
198
200
|
"exit-fullscreen",
|
|
199
201
|
"store",
|
|
200
202
|
"ai",
|
|
203
|
+
"sliders",
|
|
201
204
|
];
|
|
202
205
|
/**
|
|
203
206
|
* The player's limits on bottom-bar buttons (`CommandOptions.button`), the
|
|
@@ -601,6 +604,7 @@ host | enter-fullscreen | (Enter Fullscreen)
|
|
|
601
604
|
host | exit-fullscreen | (Leave Fullscreen)
|
|
602
605
|
host | sidebar | (Toggle Sidebar)
|
|
603
606
|
host | palette | (Command Palette) | Ctrl+Shift+P
|
|
607
|
+
host | adjust | (Adjust)
|
|
604
608
|
vestra.info | info.brief | (Brief Info) | Shift+Tab
|
|
605
609
|
vestra.playlist | playlist.play | (Play) | Enter, P | panel
|
|
606
610
|
vestra.playlist | playlist.list-add | (Add Files…) | Ctrl+I | panel
|
package/dist/docs-tree.json.gz
CHANGED
|
Binary file
|
package/dist/i18n.js
CHANGED
package/dist/media.d.ts
CHANGED
|
@@ -160,9 +160,27 @@ export type MediaJob = {
|
|
|
160
160
|
duration: string;
|
|
161
161
|
};
|
|
162
162
|
};
|
|
163
|
+
/** A stream copy (player 0.7.6): what was copied and from where. */
|
|
164
|
+
copy?: {
|
|
165
|
+
container: "mp4" | "mkv";
|
|
166
|
+
videoCodec: string;
|
|
167
|
+
audioCodec: string | null;
|
|
168
|
+
/** The keyframe the copy starts on, source seconds. */
|
|
169
|
+
keyframe: number;
|
|
170
|
+
/** FFmpeg's input seek, the keyframe rounded down to a microsecond. */
|
|
171
|
+
seek: number;
|
|
172
|
+
written: {
|
|
173
|
+
start: number;
|
|
174
|
+
end: number;
|
|
175
|
+
};
|
|
176
|
+
};
|
|
163
177
|
};
|
|
164
178
|
outcome: null | {
|
|
165
179
|
destination: string;
|
|
180
|
+
/** Clips (player 0.7.6): first frame, source seconds. */
|
|
181
|
+
start?: number;
|
|
182
|
+
/** Clips (player 0.7.6): end of the last frame, source seconds. */
|
|
183
|
+
end?: number;
|
|
166
184
|
} | {
|
|
167
185
|
error: {
|
|
168
186
|
code: string;
|
|
@@ -171,6 +189,24 @@ export type MediaJob = {
|
|
|
171
189
|
} | {
|
|
172
190
|
cancelled: true;
|
|
173
191
|
};
|
|
192
|
+
/**
|
|
193
|
+
* Player 0.7.6: seconds of a clip written and to write while FFmpeg runs;
|
|
194
|
+
* `null` (or absent from older players) while unknown.
|
|
195
|
+
*/
|
|
196
|
+
progress?: {
|
|
197
|
+
done: number;
|
|
198
|
+
total: number;
|
|
199
|
+
} | null;
|
|
200
|
+
};
|
|
201
|
+
/** The wire form of `ExportEncoding` (`jobs.start`). */
|
|
202
|
+
export type WireEncoding = {
|
|
203
|
+
crf?: number;
|
|
204
|
+
speed?: "faster" | "balanced" | "smaller";
|
|
205
|
+
maxHeight?: 1080 | 720 | 480;
|
|
206
|
+
fps?: 60 | 30;
|
|
207
|
+
audioBitrate?: 128 | 192 | 256;
|
|
208
|
+
threads?: "single" | "auto";
|
|
209
|
+
videoBitrate?: "source";
|
|
174
210
|
};
|
|
175
211
|
export type MediaOperation = {
|
|
176
212
|
status: "pending" | "unknown" | "unknownExpired";
|
|
@@ -210,6 +246,13 @@ export declare function mediaClients(transport: Transport): {
|
|
|
210
246
|
} | {
|
|
211
247
|
kind: "rawMp4" | "processedMp4";
|
|
212
248
|
end: number;
|
|
249
|
+
/** Player 0.7.6. */
|
|
250
|
+
encoding?: WireEncoding;
|
|
251
|
+
} | {
|
|
252
|
+
/** Stream copy, player 0.7.6. */
|
|
253
|
+
kind: "rawCopy";
|
|
254
|
+
end: number;
|
|
255
|
+
container?: "auto" | "mp4" | "mkv";
|
|
213
256
|
})) => Promise<{
|
|
214
257
|
jobId: number;
|
|
215
258
|
status: "queued";
|
package/dist/plugin/media.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
import { failure, sleep, uncertain } from "./runtime.js";
|
|
2
2
|
import { playerOf } from "./player.js";
|
|
3
3
|
const TERMINAL = ["completed", "failed", "cancelled"];
|
|
4
|
+
/** The player's sets for each {@link ExportEncoding} field. */
|
|
5
|
+
const ENCODING = {
|
|
6
|
+
crf: Array.from({ length: 17 }, (_, i) => i + 14),
|
|
7
|
+
speed: ["faster", "balanced", "smaller"],
|
|
8
|
+
maxHeight: [1080, 720, 480],
|
|
9
|
+
fps: [60, 30],
|
|
10
|
+
audioBitrate: [128, 192, 256],
|
|
11
|
+
threads: ["single", "auto"],
|
|
12
|
+
videoBitrate: ["source"],
|
|
13
|
+
};
|
|
14
|
+
/** Refuse what the player would refuse, with the field named. */
|
|
15
|
+
function checkEncoding(encoding) {
|
|
16
|
+
if (typeof encoding !== "object" || encoding === null)
|
|
17
|
+
throw failure("invalid", "encoding must be an object");
|
|
18
|
+
for (const [key, value] of Object.entries(encoding)) {
|
|
19
|
+
const allowed = ENCODING[key];
|
|
20
|
+
if (!allowed)
|
|
21
|
+
throw failure("invalid", `unknown encoding option ${key}`);
|
|
22
|
+
if (value !== undefined && !allowed.includes(value))
|
|
23
|
+
throw failure("invalid", `encoding.${key} must be one of ${allowed.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
if (encoding.crf !== undefined && encoding.videoBitrate !== undefined)
|
|
26
|
+
throw failure("invalid", "encoding takes crf or videoBitrate, not both");
|
|
27
|
+
return Object.fromEntries(Object.entries(encoding).filter(([, value]) => value !== undefined));
|
|
28
|
+
}
|
|
4
29
|
/** One counter for snapshot and job operations, as the host's book expects. */
|
|
5
30
|
class MediaService {
|
|
6
31
|
runtime;
|
|
@@ -159,8 +184,28 @@ class SnapshotHandle {
|
|
|
159
184
|
let job;
|
|
160
185
|
switch (options.format) {
|
|
161
186
|
case "mp4":
|
|
162
|
-
job = {
|
|
187
|
+
job = {
|
|
188
|
+
kind: `${mode}Mp4`,
|
|
189
|
+
end: options.end,
|
|
190
|
+
...(options.encoding === undefined
|
|
191
|
+
? {}
|
|
192
|
+
: { encoding: checkEncoding(options.encoding) }),
|
|
193
|
+
};
|
|
163
194
|
break;
|
|
195
|
+
case "copy": {
|
|
196
|
+
if (processed)
|
|
197
|
+
throw failure("invalid", "a stream copy needs a raw snapshot: effects need a re-encode");
|
|
198
|
+
const container = options.container;
|
|
199
|
+
if (container !== undefined &&
|
|
200
|
+
!["auto", "mp4", "mkv"].includes(container))
|
|
201
|
+
throw failure("invalid", "container must be auto, mp4 or mkv");
|
|
202
|
+
job = {
|
|
203
|
+
kind: "rawCopy",
|
|
204
|
+
end: options.end,
|
|
205
|
+
...(container === undefined ? {} : { container }),
|
|
206
|
+
};
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
164
209
|
case "jpeg": {
|
|
165
210
|
const quality = options.quality;
|
|
166
211
|
if (quality !== undefined &&
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -601,7 +601,7 @@ export declare class TestHost implements Transport {
|
|
|
601
601
|
* See {@link TestHost.mounted}.
|
|
602
602
|
*/
|
|
603
603
|
panel: (Omit<FakeWindow, "callbacks" | "userClose"> & {
|
|
604
|
-
/** `"bottom"`, `"sidebar"` or `"backdrop"`. */
|
|
604
|
+
/** `"bottom"`, `"sidebar"`, `"adjust"` or `"backdrop"`. */
|
|
605
605
|
slot: string;
|
|
606
606
|
/** The exported component the SDK found in the file. */
|
|
607
607
|
component: string;
|
|
@@ -643,6 +643,12 @@ export declare class TestHost implements Transport {
|
|
|
643
643
|
* the current tab's (the plugin under test is owner 1).
|
|
644
644
|
*/
|
|
645
645
|
sidebar: LayoutSlot | null;
|
|
646
|
+
/**
|
|
647
|
+
* The adjust column (SDK 1.6.11, player 0.7.6), like the sidebar but
|
|
648
|
+
* never `detached`. It starts hidden (`preferred: false`, `reason:
|
|
649
|
+
* "userHidden"`), as the player's does until the user opens it.
|
|
650
|
+
*/
|
|
651
|
+
adjust: LayoutSlot | null;
|
|
646
652
|
};
|
|
647
653
|
/**
|
|
648
654
|
* Every `osd.show` text as the player shows it (translated with the
|
|
@@ -1331,7 +1337,7 @@ export declare class TestHost implements Transport {
|
|
|
1331
1337
|
*
|
|
1332
1338
|
* @capability None.
|
|
1333
1339
|
*/
|
|
1334
|
-
mountOther(slot: "bottom" | "sidebar" | "backdrop", plugin: string, options?: {
|
|
1340
|
+
mountOther(slot: "bottom" | "sidebar" | "adjust" | "backdrop", plugin: string, options?: {
|
|
1335
1341
|
title?: string;
|
|
1336
1342
|
}): void;
|
|
1337
1343
|
/**
|
|
@@ -1654,9 +1660,27 @@ export declare class TestHost implements Transport {
|
|
|
1654
1660
|
/**
|
|
1655
1661
|
* Export jobs by job id. Each state query advances a job one step
|
|
1656
1662
|
* (`queued` → `running` → `completed`; `cancelling` → `cancelled`).
|
|
1657
|
-
*
|
|
1663
|
+
* A running clip reports half its `progress`; a finished clip's
|
|
1664
|
+
* `outcome` has `start` and `end` (a stream copy starts 1 s before the
|
|
1665
|
+
* snapshot time, as if snapped to a keyframe, and its destination gets
|
|
1666
|
+
* the extension of its container: `.mkv` for `container: "mkv"`, else
|
|
1667
|
+
* `.mp4`). Wire-level.
|
|
1658
1668
|
*/
|
|
1659
1669
|
readonly jobs: Map<number, MediaJob>;
|
|
1670
|
+
/**
|
|
1671
|
+
* Set it to make every running export end `failed` with this error on
|
|
1672
|
+
* its next state query, as the player reports one (`unsupported` for a
|
|
1673
|
+
* stream copy it cannot make, `export-failed` otherwise); `null` (the
|
|
1674
|
+
* default) completes them.
|
|
1675
|
+
*
|
|
1676
|
+
* @since 1.6.10
|
|
1677
|
+
*/
|
|
1678
|
+
exportFailure: {
|
|
1679
|
+
/** The `outcome.error.code`. */
|
|
1680
|
+
code: string;
|
|
1681
|
+
/** The `outcome.error.message`. */
|
|
1682
|
+
message: string;
|
|
1683
|
+
} | null;
|
|
1660
1684
|
/**
|
|
1661
1685
|
* `openStream`'s transport: a local stream (a Unix socket on macOS and
|
|
1662
1686
|
* Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the
|
|
@@ -2811,7 +2835,7 @@ export declare class TestHost implements Transport {
|
|
|
2811
2835
|
* @since 1.0.0
|
|
2812
2836
|
*/
|
|
2813
2837
|
mounted(): Omit<FakeWindow, "callbacks" | "userClose"> & {
|
|
2814
|
-
/** `"bottom"`, `"sidebar"` or `"backdrop"`. */
|
|
2838
|
+
/** `"bottom"`, `"sidebar"`, `"adjust"` or `"backdrop"`. */
|
|
2815
2839
|
slot: string;
|
|
2816
2840
|
/** The exported component the SDK found in the file. */
|
|
2817
2841
|
component: string;
|
package/dist/plugin/testing.js
CHANGED
|
@@ -167,6 +167,24 @@ export { LIST_SEPARATORS };
|
|
|
167
167
|
* ```
|
|
168
168
|
*/
|
|
169
169
|
export { LOCALES };
|
|
170
|
+
/** `jobs.start`'s `encoding`, checked against the player's sets. */
|
|
171
|
+
function validEncoding(value) {
|
|
172
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
173
|
+
return false;
|
|
174
|
+
const sets = {
|
|
175
|
+
crf: Array.from({ length: 17 }, (_, i) => i + 14),
|
|
176
|
+
speed: ["faster", "balanced", "smaller"],
|
|
177
|
+
maxHeight: [1080, 720, 480],
|
|
178
|
+
fps: [60, 30],
|
|
179
|
+
audioBitrate: [128, 192, 256],
|
|
180
|
+
threads: ["single", "auto"],
|
|
181
|
+
videoBitrate: ["source"],
|
|
182
|
+
};
|
|
183
|
+
const fields = value;
|
|
184
|
+
if (fields.crf !== undefined && fields.videoBitrate !== undefined)
|
|
185
|
+
return false;
|
|
186
|
+
return Object.entries(value).every(([key, v]) => sets[key]?.includes(v) ?? false);
|
|
187
|
+
}
|
|
170
188
|
/** A `plugin.storage.session` key, checked as the player does. */
|
|
171
189
|
function sessionKey(key) {
|
|
172
190
|
if (typeof key !== "string" ||
|
|
@@ -310,6 +328,7 @@ const SLOT_LABELS = {
|
|
|
310
328
|
bottom: "bottom bar",
|
|
311
329
|
sidebar: "sidebar",
|
|
312
330
|
backdrop: "video backdrop",
|
|
331
|
+
adjust: "adjust column",
|
|
313
332
|
};
|
|
314
333
|
/** Texts only the test host uses, with their Chinese of SDK 1.5.8. */
|
|
315
334
|
const TEST_ZH_HANS = {
|
|
@@ -532,6 +551,7 @@ export class TestHost {
|
|
|
532
551
|
layout = {
|
|
533
552
|
bottom: null,
|
|
534
553
|
sidebar: null,
|
|
554
|
+
adjust: null,
|
|
535
555
|
};
|
|
536
556
|
/**
|
|
537
557
|
* Every `osd.show` text as the player shows it (translated with the
|
|
@@ -1510,10 +1530,10 @@ export class TestHost {
|
|
|
1510
1530
|
* ```
|
|
1511
1531
|
*/
|
|
1512
1532
|
selectTab(plugin) {
|
|
1513
|
-
const slot = this.layout.sidebar;
|
|
1533
|
+
const slot = [this.layout.sidebar, this.layout.adjust].find((each) => each?.panels?.some((entry) => entry.plugin === plugin));
|
|
1514
1534
|
const panel = slot?.panels?.find((entry) => entry.plugin === plugin);
|
|
1515
1535
|
if (!slot || !panel)
|
|
1516
|
-
throw new Error(`${plugin} has no
|
|
1536
|
+
throw new Error(`${plugin} has no tab`);
|
|
1517
1537
|
slot.owner = panel.owner;
|
|
1518
1538
|
this.emit("window.state", this.#windowFacts());
|
|
1519
1539
|
}
|
|
@@ -1521,15 +1541,18 @@ export class TestHost {
|
|
|
1521
1541
|
const state = this.layout[slot];
|
|
1522
1542
|
if (state)
|
|
1523
1543
|
state.panels = [...(state.panels ?? []), panel];
|
|
1524
|
-
else
|
|
1544
|
+
else {
|
|
1545
|
+
// The adjust column starts hidden, as the player's.
|
|
1546
|
+
const shown = slot !== "adjust";
|
|
1525
1547
|
this.layout[slot] = {
|
|
1526
1548
|
owner: panel.owner,
|
|
1527
|
-
preferred:
|
|
1528
|
-
effective:
|
|
1529
|
-
reason: null,
|
|
1549
|
+
preferred: shown,
|
|
1550
|
+
effective: shown,
|
|
1551
|
+
reason: shown ? null : "userHidden",
|
|
1530
1552
|
detached: false,
|
|
1531
1553
|
panels: [panel],
|
|
1532
1554
|
};
|
|
1555
|
+
}
|
|
1533
1556
|
}
|
|
1534
1557
|
#removePanel(slot, owner) {
|
|
1535
1558
|
const state = this.layout[slot];
|
|
@@ -1809,9 +1832,24 @@ export class TestHost {
|
|
|
1809
1832
|
/**
|
|
1810
1833
|
* Export jobs by job id. Each state query advances a job one step
|
|
1811
1834
|
* (`queued` → `running` → `completed`; `cancelling` → `cancelled`).
|
|
1812
|
-
*
|
|
1835
|
+
* A running clip reports half its `progress`; a finished clip's
|
|
1836
|
+
* `outcome` has `start` and `end` (a stream copy starts 1 s before the
|
|
1837
|
+
* snapshot time, as if snapped to a keyframe, and its destination gets
|
|
1838
|
+
* the extension of its container: `.mkv` for `container: "mkv"`, else
|
|
1839
|
+
* `.mp4`). Wire-level.
|
|
1813
1840
|
*/
|
|
1814
1841
|
jobs = new Map();
|
|
1842
|
+
/**
|
|
1843
|
+
* Set it to make every running export end `failed` with this error on
|
|
1844
|
+
* its next state query, as the player reports one (`unsupported` for a
|
|
1845
|
+
* stream copy it cannot make, `export-failed` otherwise); `null` (the
|
|
1846
|
+
* default) completes them.
|
|
1847
|
+
*
|
|
1848
|
+
* @since 1.6.10
|
|
1849
|
+
*/
|
|
1850
|
+
exportFailure = null;
|
|
1851
|
+
/** What each job was asked to write, for its outcome. */
|
|
1852
|
+
#exportJobs = new Map();
|
|
1815
1853
|
/**
|
|
1816
1854
|
* `openStream`'s transport: a local stream (a Unix socket on macOS and
|
|
1817
1855
|
* Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the
|
|
@@ -4214,11 +4252,16 @@ export class TestHost {
|
|
|
4214
4252
|
for (const why of this.listeningWhy.get(event) ?? ["a listener"])
|
|
4215
4253
|
reasons.push(`listens to ${event}: ${why}`);
|
|
4216
4254
|
// A sidebar panel is never live (player 0.7.3): the current tab keeps
|
|
4217
|
-
// the plugin running as busy work, a background tab not at all.
|
|
4255
|
+
// the plugin running as busy work, a background tab not at all. The
|
|
4256
|
+
// adjust column's current tab only while the column is open (0.7.6).
|
|
4218
4257
|
if (this.panel?.slot === "sidebar") {
|
|
4219
4258
|
if (this.layout.sidebar?.owner === 1)
|
|
4220
4259
|
reasons.push("its sidebar tab is the current tab");
|
|
4221
4260
|
}
|
|
4261
|
+
else if (this.panel?.slot === "adjust") {
|
|
4262
|
+
if (this.layout.adjust?.owner === 1 && this.layout.adjust.preferred)
|
|
4263
|
+
reasons.push("its adjust tab is the current tab");
|
|
4264
|
+
}
|
|
4222
4265
|
else if (this.panel)
|
|
4223
4266
|
reasons.push("a mounted panel");
|
|
4224
4267
|
for (const [id, window] of this.windows)
|
|
@@ -4454,8 +4497,11 @@ export class TestHost {
|
|
|
4454
4497
|
"ui.mount": (p) => {
|
|
4455
4498
|
if (this.panel)
|
|
4456
4499
|
throw new HostError("conflict", "owner already mounted");
|
|
4500
|
+
if (!(String(p.slot) in SLOT_LABELS))
|
|
4501
|
+
throw new HostError("invalid", "unknown mount slot");
|
|
4457
4502
|
// Like the player: one panel in the bottom bar and the backdrop, any
|
|
4458
|
-
// number of tabs in the sidebar (player 0.7.3; 0.4.x to 0.7.2 took 8)
|
|
4503
|
+
// number of tabs in the sidebar (player 0.7.3; 0.4.x to 0.7.2 took 8)
|
|
4504
|
+
// and in the adjust column (player 0.7.6).
|
|
4459
4505
|
const holder = p.slot === "backdrop"
|
|
4460
4506
|
? this.backdrop?.owner
|
|
4461
4507
|
: p.slot === "bottom"
|
|
@@ -4509,7 +4555,10 @@ export class TestHost {
|
|
|
4509
4555
|
"ui.menu": (p) => {
|
|
4510
4556
|
if (!this.panel)
|
|
4511
4557
|
throw new HostError("unavailable", "panel not shown");
|
|
4512
|
-
|
|
4558
|
+
const tabs = this.panel.slot === "sidebar" || this.panel.slot === "adjust"
|
|
4559
|
+
? this.layout[this.panel.slot]
|
|
4560
|
+
: null;
|
|
4561
|
+
if (tabs && tabs.owner !== 1)
|
|
4513
4562
|
throw new HostError("unavailable", "the panel's tab is not the current one");
|
|
4514
4563
|
const items = p.items;
|
|
4515
4564
|
for (const id of items)
|
|
@@ -4526,11 +4575,15 @@ export class TestHost {
|
|
|
4526
4575
|
}),
|
|
4527
4576
|
"layout.set": (p) => {
|
|
4528
4577
|
const slot = String(p.slot);
|
|
4578
|
+
if (!["bottom", "sidebar", "adjust"].includes(slot))
|
|
4579
|
+
throw new HostError("invalid", "unknown slot");
|
|
4529
4580
|
const state = this.layout[slot];
|
|
4530
4581
|
if (!state)
|
|
4531
4582
|
throw new HostError("missing", "no panel at this position");
|
|
4532
4583
|
if (p.detached !== undefined && slot === "bottom")
|
|
4533
4584
|
throw new HostError("invalid", "the bottom bar cannot be detached");
|
|
4585
|
+
if (p.detached === true && slot === "adjust")
|
|
4586
|
+
throw new HostError("invalid", "the adjust column cannot be detached");
|
|
4534
4587
|
if (p.active !== undefined && p.active !== null) {
|
|
4535
4588
|
const panel = state.panels?.find((entry) => entry.plugin === p.active);
|
|
4536
4589
|
if (!panel)
|
|
@@ -5637,8 +5690,18 @@ export class TestHost {
|
|
|
5637
5690
|
"jobs.start": (p) => {
|
|
5638
5691
|
if (!this.snapshots.has(Number(p.snapshotId)))
|
|
5639
5692
|
throw new HostError("missing", "snapshot released");
|
|
5640
|
-
|
|
5641
|
-
|
|
5693
|
+
const kind = String(p.kind);
|
|
5694
|
+
const snapshot = this.snapshots.get(Number(p.snapshotId));
|
|
5695
|
+
const clip = /^(rawMp4|processedMp4|rawCopy)$/.test(kind);
|
|
5696
|
+
if (!(/^(raw|processed)(Png|Jpeg|Bmp)$/.test(kind) || clip) ||
|
|
5697
|
+
(p.quality !== undefined && !kind.endsWith("Jpeg")) ||
|
|
5698
|
+
(p.encoding !== undefined &&
|
|
5699
|
+
(!kind.endsWith("Mp4") || !validEncoding(p.encoding))) ||
|
|
5700
|
+
(p.container !== undefined &&
|
|
5701
|
+
(kind !== "rawCopy" ||
|
|
5702
|
+
!["auto", "mp4", "mkv"].includes(String(p.container)))) ||
|
|
5703
|
+
(clip && typeof p.end !== "number") ||
|
|
5704
|
+
!kind.startsWith(snapshot.kind))
|
|
5642
5705
|
throw new HostError("invalid", `unknown job kind ${p.kind}`);
|
|
5643
5706
|
this.exports.push(structuredClone(p));
|
|
5644
5707
|
const jobId = ++this.#next.job;
|
|
@@ -5648,23 +5711,71 @@ export class TestHost {
|
|
|
5648
5711
|
publicationStarted: false,
|
|
5649
5712
|
facts: null,
|
|
5650
5713
|
outcome: null,
|
|
5714
|
+
progress: null,
|
|
5715
|
+
});
|
|
5716
|
+
this.#exportJobs.set(jobId, {
|
|
5717
|
+
destination: String(p.destination),
|
|
5718
|
+
// A copy starts 1 s early (on a "keyframe"), the player's own way of
|
|
5719
|
+
// saying the start moved; a re-encode starts on the frame.
|
|
5720
|
+
clip: clip
|
|
5721
|
+
? {
|
|
5722
|
+
start: kind === "rawCopy"
|
|
5723
|
+
? Math.max(0, snapshot.time - 1)
|
|
5724
|
+
: snapshot.time,
|
|
5725
|
+
end: Number(p.end),
|
|
5726
|
+
container: kind !== "rawCopy"
|
|
5727
|
+
? null
|
|
5728
|
+
: p.container === "mkv"
|
|
5729
|
+
? "mkv"
|
|
5730
|
+
: "mp4",
|
|
5731
|
+
}
|
|
5732
|
+
: null,
|
|
5651
5733
|
});
|
|
5652
5734
|
return { jobId, status: "queued", snapshotId: p.snapshotId };
|
|
5653
5735
|
},
|
|
5654
|
-
// Each read advances a job one step: queued → running → completed.
|
|
5736
|
+
// Each read advances a job one step: queued → running → completed. A
|
|
5737
|
+
// clip reports half its progress while running.
|
|
5655
5738
|
"jobs.state": (p) => {
|
|
5656
5739
|
const job = this.jobs.get(Number(p.jobId));
|
|
5657
5740
|
if (!job)
|
|
5658
5741
|
throw new HostError("missing", "job");
|
|
5659
5742
|
const current = structuredClone(job);
|
|
5660
|
-
|
|
5743
|
+
const made = this.#exportJobs.get(job.jobId);
|
|
5744
|
+
const clip = made?.clip;
|
|
5745
|
+
if (job.status === "queued") {
|
|
5661
5746
|
job.status = "running";
|
|
5662
|
-
|
|
5747
|
+
if (clip) {
|
|
5748
|
+
const total = clip.end - clip.start;
|
|
5749
|
+
job.progress = { done: total / 2, total };
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
else if (job.status === "running" && this.exportFailure)
|
|
5753
|
+
Object.assign(job, {
|
|
5754
|
+
status: "failed",
|
|
5755
|
+
outcome: { error: { ...this.exportFailure } },
|
|
5756
|
+
});
|
|
5757
|
+
else if (job.status === "running") {
|
|
5758
|
+
const destination = clip?.container === "mkv"
|
|
5759
|
+
? made.destination.replace(/(\.(mp4|m4v|mov|mkv|webm))?$/i, ".mkv")
|
|
5760
|
+
: clip?.container === "mp4"
|
|
5761
|
+
? made.destination.replace(/(\.(mp4|m4v|mov|mkv|webm))?$/i, ".mp4")
|
|
5762
|
+
: "/tmp/output";
|
|
5663
5763
|
Object.assign(job, {
|
|
5664
5764
|
status: "completed",
|
|
5665
5765
|
publicationStarted: true,
|
|
5666
|
-
outcome:
|
|
5766
|
+
outcome: clip
|
|
5767
|
+
? { destination, start: clip.start, end: clip.end }
|
|
5768
|
+
: { destination },
|
|
5769
|
+
...(job.progress
|
|
5770
|
+
? {
|
|
5771
|
+
progress: {
|
|
5772
|
+
done: job.progress.total,
|
|
5773
|
+
total: job.progress.total,
|
|
5774
|
+
},
|
|
5775
|
+
}
|
|
5776
|
+
: {}),
|
|
5667
5777
|
});
|
|
5778
|
+
}
|
|
5668
5779
|
else if (job.status === "cancelling")
|
|
5669
5780
|
Object.assign(job, {
|
|
5670
5781
|
status: "cancelled",
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export interface Disposable {
|
|
|
93
93
|
* | `busy` | The player is still doing a previous call of the same kind (a dialog is open, a step is in flight). |
|
|
94
94
|
* | `conflict` | Another plugin owns the resource (the sequence provider, the display layer, the backdrop, a menu path), or this plugin already uses the id (a window or command id). |
|
|
95
95
|
* | `unavailable` | Not possible right now (no ready media, no user interface, fullscreen, a disabled core action). |
|
|
96
|
-
* | `unsupported` | Not possible on this system, build or source (a platform gap, software rendering, a source that cannot seek, a filter on a generated stream). |
|
|
96
|
+
* | `unsupported` | Not possible on this system, build or source (a platform gap, software rendering, a source that cannot seek, a filter on a generated stream, an export the player cannot make as asked: a stream copy, a source bitrate it cannot tell). |
|
|
97
97
|
* | `backend` | Loading or resolving failed; `message` is the backend's reason. |
|
|
98
98
|
* | `failed` | The operation ran and failed (`media.ready()` load error, a non-zero `tools.run` exit, the system refused). |
|
|
99
99
|
* | `replaced` | Accepted, but another source replaced it before it finished. |
|
|
@@ -114,6 +114,7 @@ export interface Disposable {
|
|
|
114
114
|
* | `unknown`, `rejected`, `expired` | After a transport problem the SDK could not confirm the outcome (does not happen in normal operation). |
|
|
115
115
|
* | `activation` | The plugin opened a tool window while its activation was still pending and the player could not track it. |
|
|
116
116
|
* | `command` | The player could not run or update a command it no longer knows for this plugin (removed meanwhile, or another plugin's). |
|
|
117
|
+
* | `export-failed` | An export job ({@link ExportJob}) failed: the code of its `outcome.error`, not a rejection; `message` has the worker's or FFmpeg's reason. |
|
|
117
118
|
* | `input-overflow` | `openStream` input: the player's queue of physical key transitions was full; the input was revoked and its keys released. |
|
|
118
119
|
* | `internal` | An internal step of the player failed (keeping an operation's result, a subtitle or media job); `message` has the reason. |
|
|
119
120
|
* | `javascript`, `slint` | {@link Packages.validate}: the package's entry module does not pass `node --check` / one of its Slint files does not compile; `message` has the diagnostics. |
|
|
@@ -2577,12 +2578,13 @@ export interface PanelState {
|
|
|
2577
2578
|
* else the position's reason ({@link SlotState.reason}).
|
|
2578
2579
|
*/
|
|
2579
2580
|
reason: SlotState["reason"] | "otherTab";
|
|
2580
|
-
/** The sidebar floats in its own window (with all its tabs). */
|
|
2581
|
+
/** The sidebar floats in its own window (with all its tabs); always `false` in the adjust column. */
|
|
2581
2582
|
detached: boolean;
|
|
2582
2583
|
}
|
|
2583
2584
|
/**
|
|
2584
|
-
* The bottom bar or the
|
|
2585
|
-
* `plugin.panels.sidebar`),
|
|
2585
|
+
* The bottom bar, the sidebar or the adjust column position
|
|
2586
|
+
* (`plugin.panels.bottom`, `plugin.panels.sidebar`, `plugin.panels.adjust`),
|
|
2587
|
+
* independent of who mounted the panel there.
|
|
2586
2588
|
*
|
|
2587
2589
|
* @remarks
|
|
2588
2590
|
* The position does not belong to the plugin that mounted the panel: any
|
|
@@ -4139,6 +4141,38 @@ export interface Panels {
|
|
|
4139
4141
|
* takes no pointer or keyboard input. While it is mounted, the player does
|
|
4140
4142
|
* not show the cover art of audio files itself (from the next file
|
|
4141
4143
|
* opened on), so the plugin can draw it.
|
|
4144
|
+
* - `"adjust"` (player 0.7.6, SDK 1.6.11): **controls that change what is
|
|
4145
|
+
* playing**: the picture, the sound, the subtitles, the speed (the
|
|
4146
|
+
* player's own Picture, Audio, Subtitles and Playback panels, a colour
|
|
4147
|
+
* grading tool). Several plugins may mount here, one tab each, labelled
|
|
4148
|
+
* `title` (else the plugin's name). Declare `minPlayerVersion: "0.7.6"`:
|
|
4149
|
+
* older players refuse the slot (`invalid`, "unknown mount slot"), and a
|
|
4150
|
+
* mount refused during `activate` fails the activation.
|
|
4151
|
+
*
|
|
4152
|
+
* **Choosing between `"sidebar"` and `"adjust"`.** The slot says what the
|
|
4153
|
+
* panel is for; the player decides how it looks and may change that at any
|
|
4154
|
+
* time. Ask: after the user closes the panel, do they still look at what it
|
|
4155
|
+
* showed? If yes (a list, chapters, search results, another video), it is
|
|
4156
|
+
* content: `"sidebar"`. If it only changes what is playing, it is
|
|
4157
|
+
* `"adjust"`.
|
|
4158
|
+
*
|
|
4159
|
+
* **What the player shows today (may change; do not depend on it).** The
|
|
4160
|
+
* adjust column is a second column between the video and the sidebar
|
|
4161
|
+
* (`[video][adjust][sidebar]`; both can be open, the sidebar always
|
|
4162
|
+
* rightmost), 317 wide by default, 280 to half the window, with text tabs
|
|
4163
|
+
* and the key that opens each tab under its title (the key of the command
|
|
4164
|
+
* your other commands name as their {@link CommandOptions.home | home}). It
|
|
4165
|
+
* has no close or detach button: the bottom bar's Adjust button, your
|
|
4166
|
+
* panel's own command ({@link Panel.toggle}) and the menus open and close
|
|
4167
|
+
* it. It starts closed. In a window too narrow to dock it beside a video at
|
|
4168
|
+
* least 480 wide, it floats over the right of the video, 300 wide; in
|
|
4169
|
+
* fullscreen it slides out from the right edge, left of the sidebar. Its
|
|
4170
|
+
* width, open state, current tab and tab order are remembered across
|
|
4171
|
+
* launches. Its strip is always drawn, so panel coordinates (drops) start
|
|
4172
|
+
* below it, and the bridge global's `tabbed` property, if declared, is
|
|
4173
|
+
* `true`. As in the sidebar, only the current tab's plugin is kept
|
|
4174
|
+
* running while the column is open, and the player compiles a panel the
|
|
4175
|
+
* first time it is shown, not at start-up.
|
|
4142
4176
|
*
|
|
4143
4177
|
* @capability ui.mount
|
|
4144
4178
|
*
|
|
@@ -4175,12 +4209,12 @@ export interface Panels {
|
|
|
4175
4209
|
*
|
|
4176
4210
|
* @since 1.0.0
|
|
4177
4211
|
*/
|
|
4178
|
-
mount(slot: "bottom" | "sidebar" | "backdrop", file: string, options?: {
|
|
4212
|
+
mount(slot: "bottom" | "sidebar" | "adjust" | "backdrop", file: string, options?: {
|
|
4179
4213
|
/** The exported component to mount, if the file exports several. */
|
|
4180
4214
|
component?: string;
|
|
4181
4215
|
/** The exported global to use as the bridge, if the file exports several. */
|
|
4182
4216
|
global?: string;
|
|
4183
|
-
/** The floating sidebar window's title (at most 128 bytes). */
|
|
4217
|
+
/** The tab label and the floating sidebar window's title (at most 128 bytes). */
|
|
4184
4218
|
title?: string;
|
|
4185
4219
|
}): Panel;
|
|
4186
4220
|
/**
|
|
@@ -4192,6 +4226,51 @@ export interface Panels {
|
|
|
4192
4226
|
readonly bottom: PanelSlot;
|
|
4193
4227
|
/** The sidebar position, including its floating state; any plugin may read and toggle it. */
|
|
4194
4228
|
readonly sidebar: SidebarSlot;
|
|
4229
|
+
/**
|
|
4230
|
+
* The adjust column (player 0.7.6, SDK 1.6.11), whoever mounted there;
|
|
4231
|
+
* any plugin may read and toggle it. The same shape as
|
|
4232
|
+
* {@link Panels.sidebar} without floating: the adjust column never
|
|
4233
|
+
* detaches.
|
|
4234
|
+
*
|
|
4235
|
+
* @remarks
|
|
4236
|
+
* `state` is `null` while no plugin has a panel there. Players before
|
|
4237
|
+
* 0.7.6 build the `plugin` object with their own SDK, which has no
|
|
4238
|
+
* `adjust`: there `plugin.panels.adjust` is `undefined` and reading its
|
|
4239
|
+
* `state` throws a `TypeError`, so declare `"minPlayerVersion": "0.7.6"`
|
|
4240
|
+
* (or test `plugin.panels.adjust` first). The column starts hidden (`preferred: false`, `reason:
|
|
4241
|
+
* "userHidden"`) until the user opens it. To open the column at your own
|
|
4242
|
+
* panel, call {@link Panel.show} (or {@link Panel.toggle}, which closes it
|
|
4243
|
+
* again when your tab is already the current one, the behaviour of the
|
|
4244
|
+
* player's own `Alt+1…4`).
|
|
4245
|
+
*
|
|
4246
|
+
* @example Open the adjust column at this plugin's tab with a key
|
|
4247
|
+
* One command opens the column at the plugin's tab, and pressing it again
|
|
4248
|
+
* while that tab is shown closes the column. The panel's other commands
|
|
4249
|
+
* name it as their `home`, so the player shows its key under the tab and
|
|
4250
|
+
* the command palette knows where they live.
|
|
4251
|
+
* ```ts
|
|
4252
|
+
* const panel = plugin.panels.mount("adjust", "ui/wheels.slint", { title: "Wheels" });
|
|
4253
|
+
* plugin.commands.add({
|
|
4254
|
+
* id: "wheels",
|
|
4255
|
+
* title: "Colour Wheels",
|
|
4256
|
+
* bindings: ["Alt+5"],
|
|
4257
|
+
* // Opens the column at this tab; closes it when this tab is shown.
|
|
4258
|
+
* run: () => panel.toggle(),
|
|
4259
|
+
* });
|
|
4260
|
+
* plugin.commands.add({
|
|
4261
|
+
* id: "wheels-reset",
|
|
4262
|
+
* title: "Reset Colour Wheels",
|
|
4263
|
+
* // Lives in the panel `wheels` opens: its key shows under the tab.
|
|
4264
|
+
* home: "wheels",
|
|
4265
|
+
* run: () => panel.set({ lift: 0 }),
|
|
4266
|
+
* });
|
|
4267
|
+
* ```
|
|
4268
|
+
*
|
|
4269
|
+
* @player 0.7.6 (declare `"minPlayerVersion": "0.7.6"`; older players
|
|
4270
|
+
* have no `adjust`: reading its `state` throws a `TypeError`).
|
|
4271
|
+
* @since 1.6.11
|
|
4272
|
+
*/
|
|
4273
|
+
readonly adjust: PanelSlot;
|
|
4195
4274
|
}
|
|
4196
4275
|
/**
|
|
4197
4276
|
* What a timeline mark stands for; readers choose colours by it.
|
|
@@ -8294,11 +8373,30 @@ export interface Snapshot extends Disposable {
|
|
|
8294
8373
|
* Start exporting; the job runs in the host's offline worker.
|
|
8295
8374
|
* One snapshot can be exported several times.
|
|
8296
8375
|
*
|
|
8376
|
+
* A **stream copy** (`format: "copy"`, player 0.7.6) writes the video
|
|
8377
|
+
* stream and the snapshot's audio track as they are in the file, without
|
|
8378
|
+
* decoding: lossless and about as fast as the disk, but a copy can only
|
|
8379
|
+
* begin on a keyframe. It starts at the video's last keyframe at or before
|
|
8380
|
+
* the snapshot time (its first keyframe when the video itself starts
|
|
8381
|
+
* later; keyframes an MP4 edit list discards do not count) and is cut at
|
|
8382
|
+
* `end`, where it may run a few frames past `end`. An open GOP can write
|
|
8383
|
+
* a frame or two that present before the keyframe; `start` includes them.
|
|
8384
|
+
* The finished job's `outcome` gives the `start` and `end` actually
|
|
8385
|
+
* written; a plugin that previews the cut (with `plugin.tools.run`
|
|
8386
|
+
* and ffprobe's packet flags, say) shows those once the job is done.
|
|
8387
|
+
* Copies need a raw snapshot (effects and burned-in subtitles need a
|
|
8388
|
+
* re-encode) and leave subtitle tracks and chapters out. `auto` writes
|
|
8389
|
+
* MP4 for H.264, HEVC, AV1 or MPEG-4 video with AAC, MP3, AC-3, E-AC-3 or
|
|
8390
|
+
* ALAC audio (or none), and MKV for anything else. A copy the player
|
|
8391
|
+
* cannot make (no video stream, a cover image, an external audio track,
|
|
8392
|
+
* codecs `mp4` cannot hold) ends `failed` with the code `unsupported`.
|
|
8393
|
+
*
|
|
8297
8394
|
* @capability media.read and media.control
|
|
8298
8395
|
* @throws `released` (rejects) The snapshot was already released.
|
|
8299
8396
|
* @throws `invalid` (rejects) Bad options (`quality` outside 1–100, a
|
|
8300
8397
|
* relative destination, a clip `end` not after the snapshot time or
|
|
8301
|
-
* past the duration
|
|
8398
|
+
* past the duration, an {@link ExportEncoding} value outside its set,
|
|
8399
|
+
* `copy` from a processed snapshot).
|
|
8302
8400
|
* @throws `busy` (rejects) This plugin runs 2 exports (or the player 8).
|
|
8303
8401
|
* @throws `unavailable` (rejects) No FFmpeg, or the export worker could not
|
|
8304
8402
|
* start.
|
|
@@ -8318,18 +8416,48 @@ export interface Snapshot extends Disposable {
|
|
|
8318
8416
|
* if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
|
|
8319
8417
|
* ```
|
|
8320
8418
|
*
|
|
8419
|
+
* @example Cut without re-encoding and report where it starts
|
|
8420
|
+
* The clip is copied as it is in the file. A copy starts on a keyframe,
|
|
8421
|
+
* so the OSD says where it actually starts; a file the player cannot
|
|
8422
|
+
* copy ends with `unsupported`, and the plugin offers a re-encode instead.
|
|
8423
|
+
* ```ts
|
|
8424
|
+
* declare const snapshot: Snapshot;
|
|
8425
|
+
* // ---cut---
|
|
8426
|
+
* const job = await snapshot.export({ format: "copy", destination: "/tmp/clip.mp4", end: snapshot.time + 20 });
|
|
8427
|
+
* const state = await job.done;
|
|
8428
|
+
* const outcome = state.outcome;
|
|
8429
|
+
* // The copy starts on a keyframe: outcome.start says where.
|
|
8430
|
+
* if (outcome && "destination" in outcome)
|
|
8431
|
+
* await plugin.osd.show("Saved {} from {} s", [outcome.destination, (outcome.start ?? snapshot.time).toFixed(1)]);
|
|
8432
|
+
* // Codecs the container cannot hold, a cover image, …
|
|
8433
|
+
* else if (outcome && "error" in outcome && outcome.error.code === "unsupported")
|
|
8434
|
+
* await plugin.osd.show("Fast cut is not possible for this file");
|
|
8435
|
+
* ```
|
|
8436
|
+
*
|
|
8321
8437
|
* @since 1.0.0
|
|
8322
8438
|
*/
|
|
8323
8439
|
export(options: ExportOptions): Promise<ExportJob>;
|
|
8324
8440
|
}
|
|
8325
8441
|
/**
|
|
8326
8442
|
* One frame as PNG, JPEG (`quality` 1–100, default 90) or BMP (24-bit), or
|
|
8327
|
-
* a clip as MP4
|
|
8443
|
+
* a clip: re-encoded as MP4 (`mp4`, frame-accurate, optional
|
|
8444
|
+
* {@link ExportEncoding}) or copied without re-encoding (`copy`, player
|
|
8445
|
+
* 0.7.6, starting on a keyframe).
|
|
8328
8446
|
*
|
|
8329
8447
|
* `destination` is never overwritten if it exists. The extension does not
|
|
8330
|
-
* choose the format
|
|
8331
|
-
*
|
|
8332
|
-
*
|
|
8448
|
+
* choose the format (a copy's extension is set to its container).
|
|
8449
|
+
* `sampleTime` picks the frame (default: the snapshot time); `end` is the
|
|
8450
|
+
* clip's end in source seconds. On Windows the export worker needs FFmpeg
|
|
8451
|
+
* 6.1 or newer.
|
|
8452
|
+
*
|
|
8453
|
+
* **Which players have `copy`, `encoding` and the progress and range of
|
|
8454
|
+
* a job**: player 0.7.6 and later. Declare `"minPlayerVersion": "0.7.6"`,
|
|
8455
|
+
* or, to keep older players and offer these only where they exist, compare
|
|
8456
|
+
* {@link AppInfo.version | plugin.app.version} with `0.7.6`, as for any
|
|
8457
|
+
* newer API. Do not probe by trying: on an older player the `plugin`
|
|
8458
|
+
* object comes from that player's own, older SDK, which rejects `copy` with
|
|
8459
|
+
* `invalid` but builds a re-encode request without the `encoding` it does
|
|
8460
|
+
* not know, so the clip is encoded with the fixed settings and no error.
|
|
8333
8461
|
*
|
|
8334
8462
|
* @since 1.0.0
|
|
8335
8463
|
* @category Media
|
|
@@ -8351,19 +8479,132 @@ export type ExportOptions = {
|
|
|
8351
8479
|
/** Integer 1–100, default 90. */
|
|
8352
8480
|
quality?: number;
|
|
8353
8481
|
} | {
|
|
8354
|
-
/** A clip from the snapshot time. */
|
|
8482
|
+
/** A clip from the snapshot time, re-encoded as H.264 + AAC. */
|
|
8355
8483
|
format: "mp4";
|
|
8356
8484
|
/** Absolute path of the new file. */
|
|
8357
8485
|
destination: string;
|
|
8358
8486
|
/** Clip end, source seconds. */
|
|
8359
8487
|
end: number;
|
|
8488
|
+
/**
|
|
8489
|
+
* Encoder settings (player 0.7.6); without it, the settings before
|
|
8490
|
+
* they existed.
|
|
8491
|
+
*
|
|
8492
|
+
* @since 1.6.10
|
|
8493
|
+
*/
|
|
8494
|
+
encoding?: ExportEncoding;
|
|
8495
|
+
} | {
|
|
8496
|
+
/**
|
|
8497
|
+
* A clip copied stream by stream, without re-encoding (player 0.7.6):
|
|
8498
|
+
* lossless and about as fast as the disk. It starts on the video
|
|
8499
|
+
* keyframe at or before the snapshot time; see {@link Snapshot.export}.
|
|
8500
|
+
*
|
|
8501
|
+
* @since 1.6.10
|
|
8502
|
+
*/
|
|
8503
|
+
format: "copy";
|
|
8504
|
+
/**
|
|
8505
|
+
* Absolute path of the new file. The player gives it the extension of
|
|
8506
|
+
* the container it writes (`.mp4` or `.mkv`); the finished job's
|
|
8507
|
+
* `outcome.destination` is the actual path.
|
|
8508
|
+
*/
|
|
8509
|
+
destination: string;
|
|
8510
|
+
/** Clip end, source seconds. */
|
|
8511
|
+
end: number;
|
|
8512
|
+
/**
|
|
8513
|
+
* `auto` (default): MP4 when the codecs fit it, otherwise MKV. `mp4`
|
|
8514
|
+
* fails with `unsupported` when they do not; `mkv` always writes MKV.
|
|
8515
|
+
*/
|
|
8516
|
+
container?: "auto" | "mp4" | "mkv";
|
|
8360
8517
|
};
|
|
8518
|
+
/**
|
|
8519
|
+
* The encoder settings of a re-encoded clip (`format: "mp4"`, player
|
|
8520
|
+
* 0.7.6): a closed set of values, never encoder arguments. Every field is
|
|
8521
|
+
* optional; the defaults are the settings clips had before: CRF 18, the
|
|
8522
|
+
* fastest speed, the source's size and frame rate, AAC 192 kb/s, one thread.
|
|
8523
|
+
* A value outside its set rejects the export with `invalid`. `videoBitrate:
|
|
8524
|
+
* "source"` replaces the CRF with the source's bitrate, for a "Same as
|
|
8525
|
+
* source" preset.
|
|
8526
|
+
*
|
|
8527
|
+
* @example A small file
|
|
8528
|
+
* A 720p, 30 fps clip at a lower quality, encoded with the cores the player
|
|
8529
|
+
* can spare.
|
|
8530
|
+
* ```ts
|
|
8531
|
+
* declare const snapshot: Snapshot;
|
|
8532
|
+
* // ---cut---
|
|
8533
|
+
* // Each field is a ceiling or a choice from a closed set.
|
|
8534
|
+
* const job = await snapshot.export({
|
|
8535
|
+
* format: "mp4",
|
|
8536
|
+
* destination: "/tmp/small.mp4",
|
|
8537
|
+
* end: snapshot.time + 30,
|
|
8538
|
+
* encoding: { crf: 26, maxHeight: 720, fps: 30, audioBitrate: 128, threads: "auto" },
|
|
8539
|
+
* });
|
|
8540
|
+
* ```
|
|
8541
|
+
*
|
|
8542
|
+
* @since 1.6.10
|
|
8543
|
+
* @category Media
|
|
8544
|
+
*/
|
|
8545
|
+
export interface ExportEncoding {
|
|
8546
|
+
/**
|
|
8547
|
+
* H.264 quality, an integer 14–30 (x264's CRF): lower is a better picture
|
|
8548
|
+
* and a bigger file. Default 18.
|
|
8549
|
+
*/
|
|
8550
|
+
crf?: number;
|
|
8551
|
+
/**
|
|
8552
|
+
* Encoding effort: `faster` (default), `balanced` or `smaller` (slower,
|
|
8553
|
+
* a smaller file at the same quality); x264's `veryfast`, `medium` and
|
|
8554
|
+
* `slow`.
|
|
8555
|
+
*/
|
|
8556
|
+
speed?: "faster" | "balanced" | "smaller";
|
|
8557
|
+
/**
|
|
8558
|
+
* The tallest picture: a taller one is scaled down to this height,
|
|
8559
|
+
* keeping its shape; a shorter one is left as it is. Default: the
|
|
8560
|
+
* source's size.
|
|
8561
|
+
*/
|
|
8562
|
+
maxHeight?: 1080 | 720 | 480;
|
|
8563
|
+
/**
|
|
8564
|
+
* The highest frame rate: a faster source drops frames to it; a slower
|
|
8565
|
+
* one is left as it is. Default: the source's rate.
|
|
8566
|
+
*/
|
|
8567
|
+
fps?: 60 | 30;
|
|
8568
|
+
/** AAC audio bitrate in kb/s. Default 192. */
|
|
8569
|
+
audioBitrate?: 128 | 192 | 256;
|
|
8570
|
+
/**
|
|
8571
|
+
* `single` (default) encodes on one thread, leaving the other cores to
|
|
8572
|
+
* playback. `auto` lets the player use more: half the logical
|
|
8573
|
+
* processors, at most 8, so playback and the interface keep their rate.
|
|
8574
|
+
* From player 0.7.6 every export runs at below-normal priority (as
|
|
8575
|
+
* HandBrake and OBS run their encoders), with either setting.
|
|
8576
|
+
*/
|
|
8577
|
+
threads?: "single" | "auto";
|
|
8578
|
+
/**
|
|
8579
|
+
* `source`: aim at the source video's average bitrate instead of a
|
|
8580
|
+
* quality level (peaks up to 1.5 times it), so the file comes out about
|
|
8581
|
+
* the size of that stretch of the source: a "Same as source" preset. Not
|
|
8582
|
+
* together with `crf` (`invalid`). A source whose bitrate the player
|
|
8583
|
+
* cannot tell ends the job `failed` with `unsupported`.
|
|
8584
|
+
*/
|
|
8585
|
+
videoBitrate?: "source";
|
|
8586
|
+
}
|
|
8361
8587
|
/**
|
|
8362
8588
|
* An export job's state. `status` is `queued`, `running`, `cancelling`,
|
|
8363
8589
|
* `completed`, `failed` or `cancelled`; once terminal, `outcome` is
|
|
8364
8590
|
* `{ destination }`, `{ error: { code, message } }` or `{ cancelled: true }`.
|
|
8365
8591
|
* `facts` describes what the worker produced (versions, range, streams).
|
|
8366
8592
|
*
|
|
8593
|
+
* From player 0.7.6:
|
|
8594
|
+
* - a finished clip's `outcome` also has `start` and `end`, the source
|
|
8595
|
+
* times of its first frame and of the end of its last frame as written: a
|
|
8596
|
+
* copy's earliest written frame (its keyframe, or the leading frames of
|
|
8597
|
+
* an open GOP) and actual end; a re-encode's start and start
|
|
8598
|
+
* plus the encoded length.
|
|
8599
|
+
* - `progress` is `{ done, total }` in seconds of the clip while FFmpeg
|
|
8600
|
+
* writes it (at most 4 updates a second; the SDK reads it about every
|
|
8601
|
+
* 350 ms), `null` while unknown: before writing starts, and always for a
|
|
8602
|
+
* frame. Show an indeterminate bar then. After `done` reaches `total` the
|
|
8603
|
+
* player still checks the file before the job completes.
|
|
8604
|
+
* - `error.code` is `export-failed`, or `unsupported` for an export the
|
|
8605
|
+
* player cannot make as asked (a stream copy it cannot make, a
|
|
8606
|
+
* `videoBitrate: "source"` whose source bitrate it cannot tell).
|
|
8607
|
+
*
|
|
8367
8608
|
* @since 1.0.0
|
|
8368
8609
|
* @category Media
|
|
8369
8610
|
*/
|
|
@@ -9254,7 +9495,7 @@ export interface PluginWarning {
|
|
|
9254
9495
|
* 0.4.1); a newer player may add codes.
|
|
9255
9496
|
*/
|
|
9256
9497
|
code: string;
|
|
9257
|
-
/** The panel position (`"bottom"`, `"
|
|
9498
|
+
/** The panel position (`"bottom"`, `"backdrop"`; the sidebar and the adjust column hold any number of tabs) for `slot-occupied`. */
|
|
9258
9499
|
slot?: string;
|
|
9259
9500
|
/**
|
|
9260
9501
|
* What was taken, for `resolver-taken`: 「文件类型 .nes」 or the
|
package/dist/plugin/ui.js
CHANGED
|
@@ -991,7 +991,8 @@ class LayoutService {
|
|
|
991
991
|
state = {
|
|
992
992
|
bottom: null,
|
|
993
993
|
sidebar: null,
|
|
994
|
-
|
|
994
|
+
adjust: null,
|
|
995
|
+
tabs: { bottom: null, sidebar: null, adjust: null },
|
|
995
996
|
backdrop: null,
|
|
996
997
|
};
|
|
997
998
|
#listeners = new Set();
|
|
@@ -1051,10 +1052,16 @@ class LayoutService {
|
|
|
1051
1052
|
active: panel.owner === slot.owner,
|
|
1052
1053
|
})))
|
|
1053
1054
|
: null;
|
|
1055
|
+
const adjust = raw.adjust ?? null;
|
|
1054
1056
|
const next = {
|
|
1055
1057
|
bottom: view(raw.bottom),
|
|
1056
1058
|
sidebar: view(raw.sidebar),
|
|
1057
|
-
|
|
1059
|
+
adjust: view(adjust),
|
|
1060
|
+
tabs: {
|
|
1061
|
+
bottom: tabs(raw.bottom),
|
|
1062
|
+
sidebar: tabs(raw.sidebar),
|
|
1063
|
+
adjust: tabs(adjust),
|
|
1064
|
+
},
|
|
1058
1065
|
backdrop: raw.backdrop
|
|
1059
1066
|
? Object.freeze({ effective: raw.backdrop.effective === true })
|
|
1060
1067
|
: null,
|
|
@@ -1145,6 +1152,7 @@ export const panels = (scope) => ({
|
|
|
1145
1152
|
mount: (position, file, options = {}) => new PanelHandle(scope, position, file, options),
|
|
1146
1153
|
bottom: slot(scope, "bottom"),
|
|
1147
1154
|
sidebar: slot(scope, "sidebar"),
|
|
1155
|
+
adjust: slot(scope, "adjust"),
|
|
1148
1156
|
backdrop: backdrop(scope),
|
|
1149
1157
|
});
|
|
1150
1158
|
export const dialogs = (scope) => {
|
package/dist/wire.d.ts
CHANGED
|
@@ -288,7 +288,10 @@ export interface LayoutPanel {
|
|
|
288
288
|
/** The tab label: the mount's `title`, else the plugin's name. */
|
|
289
289
|
title: string;
|
|
290
290
|
}
|
|
291
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* One position (the bottom bar, the sidebar or, from player 0.7.6, the
|
|
293
|
+
* adjust column) as `layout.state` reports it.
|
|
294
|
+
*/
|
|
292
295
|
export interface LayoutSlot {
|
|
293
296
|
/** The current tab's panel (the only panel of the bottom bar). */
|
|
294
297
|
owner: number;
|
|
@@ -696,10 +699,13 @@ export declare function createWire(transport: Transport): {
|
|
|
696
699
|
state: () => Promise<{
|
|
697
700
|
bottom: LayoutSlot | null;
|
|
698
701
|
sidebar: LayoutSlot | null;
|
|
702
|
+
/** Player 0.7.6 (absent from older players). */
|
|
703
|
+
adjust?: LayoutSlot | null;
|
|
699
704
|
backdrop?: LayoutBackdrop | null;
|
|
700
705
|
}>;
|
|
701
706
|
set: (options: {
|
|
702
|
-
|
|
707
|
+
/** `"adjust"`: player 0.7.6 (older players refuse it with `invalid`). */
|
|
708
|
+
slot: "bottom" | "sidebar" | "adjust";
|
|
703
709
|
visible?: boolean;
|
|
704
710
|
detached?: boolean;
|
|
705
711
|
/** The plugin id of the panel whose tab becomes the current one. */
|
|
@@ -727,7 +733,8 @@ export declare function createWire(transport: Transport): {
|
|
|
727
733
|
images: string[];
|
|
728
734
|
}>;
|
|
729
735
|
mount: (options: {
|
|
730
|
-
|
|
736
|
+
/** `"adjust"`: player 0.7.6 (older players refuse it with `invalid`). */
|
|
737
|
+
slot: "bottom" | "sidebar" | "backdrop" | "adjust";
|
|
731
738
|
file: string;
|
|
732
739
|
component: string;
|
|
733
740
|
global: string;
|
|
@@ -798,6 +805,11 @@ export declare function createWire(transport: Transport): {
|
|
|
798
805
|
} | {
|
|
799
806
|
kind: "rawMp4" | "processedMp4";
|
|
800
807
|
end: number;
|
|
808
|
+
encoding?: import("./media.ts").WireEncoding;
|
|
809
|
+
} | {
|
|
810
|
+
kind: "rawCopy";
|
|
811
|
+
end: number;
|
|
812
|
+
container?: "auto" | "mp4" | "mkv";
|
|
801
813
|
})) => Promise<{
|
|
802
814
|
jobId: number;
|
|
803
815
|
status: "queued";
|
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.11: 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
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
## API
|
|
30
30
|
|
|
31
31
|
- `/API/Commands`: 15 entries: CommandState, Bindable, Condition, When, MenuId, BarButtonIcon, CommandButton, CommandOptions, Command, Commands, HostAction, Host, BindingRef, BindingInfo, Bindings
|
|
32
|
-
- `/API/Media`:
|
|
32
|
+
- `/API/Media`: 7 entries: CaptureOptions, Snapshot, ExportOptions, ExportEncoding, JobState, ExportJob, MediaCapture
|
|
33
33
|
- `/API/Packages`: 10 entries: PackageManifest, PackageSource, InstallOptions, PackageCandidate, PackageChangeStatus, PackageChange, PluginInfo, PluginDisplay, PluginWarning, Packages
|
|
34
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
|
package/package.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// player's own. Since SDK 1.6.8 (menu redesign step 3a) the player draws
|
|
4
4
|
// the strip itself, at the right end of the bottom slot, its 40px cells on
|
|
5
5
|
// the slot's bottom edge (a bar's 40px button row), and also when no plugin
|
|
6
|
-
// mounts a bottom bar: `[plugin buttons…][»][Command Palette][
|
|
7
|
-
// Sidebar]`. A bottom bar places nothing: it lays its controls out left of
|
|
6
|
+
// mounts a bottom bar: `[plugin buttons…][»][Command Palette][Adjust]
|
|
7
|
+
// [Toggle Sidebar]`. A bottom bar places nothing: it lays its controls out left of
|
|
8
8
|
// the strip and says how wide they must stay:
|
|
9
9
|
// import { BarButtonsHost } from "@vestra/bar-buttons.slint";
|
|
10
10
|
// export component MyBar inherits Rectangle {
|
|
@@ -65,12 +65,16 @@ export global BarButtonsHost {
|
|
|
65
65
|
in property <BarButtonItem> toggle;
|
|
66
66
|
// The command palette (`host/palette`; `token` "" while there is none).
|
|
67
67
|
in property <BarButtonItem> palette;
|
|
68
|
+
// The adjust column's toggle (`host/adjust`, player 0.7.6; `token` ""
|
|
69
|
+
// while there is none).
|
|
70
|
+
in property <BarButtonItem> adjust;
|
|
68
71
|
// Set when a (deprecated) `BarButtons` exists in this window.
|
|
69
72
|
in-out property <bool> placed;
|
|
70
73
|
// The width the player gives its strip (the player sets it).
|
|
71
74
|
in-out property <length> room;
|
|
72
|
-
// The player's own cells, always shown
|
|
73
|
-
|
|
75
|
+
// The player's own cells, always shown: Command Palette, Adjust,
|
|
76
|
+
// Toggle Sidebar.
|
|
77
|
+
private property <int> own: (root.palette.token != "" ? 1 : 0) + (root.adjust.token != "" ? 1 : 0) + (root.toggle.token != "" ? 1 : 0);
|
|
74
78
|
// The cells a bar must always leave free, 41px each: the player's own,
|
|
75
79
|
// and the `»` cell whenever any plugin button exists (drawn only on
|
|
76
80
|
// overflow, but always reserved, so `»` never covers the bar). Never
|
package/slint/vestra/icons.slint
CHANGED
|
@@ -52,6 +52,9 @@ export global Icons {
|
|
|
52
52
|
// small cross (AI), the default 14 × 12 box; bar buttons use them.
|
|
53
53
|
if name == "store" { return "M 2.5 4 H 11.5 L 10.9 9.6 Q 10.8 10.5 9.9 10.5 H 4.1 Q 3.2 10.5 3.1 9.6 Z M 5.2 4 V 3.3 A 1.8 1.8 0 0 1 8.8 3.3 V 4"; }
|
|
54
54
|
if name == "ai" { return "M 6 1.5 Q 6.6 5.4 10.5 6 Q 6.6 6.6 6 10.5 Q 5.4 6.6 1.5 6 Q 5.4 5.4 6 1.5 Z M 11.5 1.2 V 3.8 M 10.2 2.5 H 12.8"; }
|
|
55
|
+
// Three sliders with their knobs (the adjust column, `host/adjust`),
|
|
56
|
+
// the default 14 × 12 box.
|
|
57
|
+
if name == "sliders" { return "M 1.5 2.5 H 7.6 M 10.4 2.5 H 12.5 M 10.4 2.5 A 1.4 1.4 0 1 1 7.6 2.5 A 1.4 1.4 0 1 1 10.4 2.5 Z M 1.5 6 H 3.1 M 5.9 6 H 12.5 M 5.9 6 A 1.4 1.4 0 1 1 3.1 6 A 1.4 1.4 0 1 1 5.9 6 Z M 1.5 9.5 H 6.6 M 9.4 9.5 H 12.5 M 9.4 9.5 A 1.4 1.4 0 1 1 6.6 9.5 A 1.4 1.4 0 1 1 9.4 9.5 Z"; }
|
|
55
58
|
return "";
|
|
56
59
|
}
|
|
57
60
|
// Line width.
|