@vestra-player/sdk 1.6.9 → 1.6.10
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 +13 -1
- package/dist/docs-tree.json.gz +0 -0
- package/dist/media.d.ts +43 -0
- package/dist/plugin/media.js +46 -1
- package/dist/plugin/testing.d.ts +19 -1
- package/dist/plugin/testing.js +98 -7
- package/dist/plugin/types.d.ts +170 -7
- package/dist/wire.d.ts +5 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.10
|
|
4
|
+
|
|
5
|
+
**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.
|
|
6
|
+
|
|
7
|
+
- **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`.
|
|
8
|
+
- **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.
|
|
9
|
+
- **`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`.
|
|
10
|
+
- **`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.
|
|
11
|
+
- **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).
|
|
12
|
+
- **`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.
|
|
13
|
+
- **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.
|
|
14
|
+
|
|
3
15
|
## 1.6.9
|
|
4
16
|
|
|
5
17
|
**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 +41,7 @@
|
|
|
29
41
|
- **`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
42
|
- **`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
43
|
- **`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
|
|
44
|
+
- **`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
45
|
- **`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
46
|
- The [theme-widgets example](examples/theme-widgets) shows a slider's two commands (F7, F8) as key caps and in its tooltip.
|
|
35
47
|
|
package/dist/docs-tree.json.gz
CHANGED
|
Binary file
|
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
|
@@ -1654,9 +1654,27 @@ export declare class TestHost implements Transport {
|
|
|
1654
1654
|
/**
|
|
1655
1655
|
* Export jobs by job id. Each state query advances a job one step
|
|
1656
1656
|
* (`queued` → `running` → `completed`; `cancelling` → `cancelled`).
|
|
1657
|
-
*
|
|
1657
|
+
* A running clip reports half its `progress`; a finished clip's
|
|
1658
|
+
* `outcome` has `start` and `end` (a stream copy starts 1 s before the
|
|
1659
|
+
* snapshot time, as if snapped to a keyframe, and its destination gets
|
|
1660
|
+
* the extension of its container: `.mkv` for `container: "mkv"`, else
|
|
1661
|
+
* `.mp4`). Wire-level.
|
|
1658
1662
|
*/
|
|
1659
1663
|
readonly jobs: Map<number, MediaJob>;
|
|
1664
|
+
/**
|
|
1665
|
+
* Set it to make every running export end `failed` with this error on
|
|
1666
|
+
* its next state query, as the player reports one (`unsupported` for a
|
|
1667
|
+
* stream copy it cannot make, `export-failed` otherwise); `null` (the
|
|
1668
|
+
* default) completes them.
|
|
1669
|
+
*
|
|
1670
|
+
* @since 1.6.10
|
|
1671
|
+
*/
|
|
1672
|
+
exportFailure: {
|
|
1673
|
+
/** The `outcome.error.code`. */
|
|
1674
|
+
code: string;
|
|
1675
|
+
/** The `outcome.error.message`. */
|
|
1676
|
+
message: string;
|
|
1677
|
+
} | null;
|
|
1660
1678
|
/**
|
|
1661
1679
|
* `openStream`'s transport: a local stream (a Unix socket on macOS and
|
|
1662
1680
|
* Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the
|
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" ||
|
|
@@ -1809,9 +1827,24 @@ export class TestHost {
|
|
|
1809
1827
|
/**
|
|
1810
1828
|
* Export jobs by job id. Each state query advances a job one step
|
|
1811
1829
|
* (`queued` → `running` → `completed`; `cancelling` → `cancelled`).
|
|
1812
|
-
*
|
|
1830
|
+
* A running clip reports half its `progress`; a finished clip's
|
|
1831
|
+
* `outcome` has `start` and `end` (a stream copy starts 1 s before the
|
|
1832
|
+
* snapshot time, as if snapped to a keyframe, and its destination gets
|
|
1833
|
+
* the extension of its container: `.mkv` for `container: "mkv"`, else
|
|
1834
|
+
* `.mp4`). Wire-level.
|
|
1813
1835
|
*/
|
|
1814
1836
|
jobs = new Map();
|
|
1837
|
+
/**
|
|
1838
|
+
* Set it to make every running export end `failed` with this error on
|
|
1839
|
+
* its next state query, as the player reports one (`unsupported` for a
|
|
1840
|
+
* stream copy it cannot make, `export-failed` otherwise); `null` (the
|
|
1841
|
+
* default) completes them.
|
|
1842
|
+
*
|
|
1843
|
+
* @since 1.6.10
|
|
1844
|
+
*/
|
|
1845
|
+
exportFailure = null;
|
|
1846
|
+
/** What each job was asked to write, for its outcome. */
|
|
1847
|
+
#exportJobs = new Map();
|
|
1815
1848
|
/**
|
|
1816
1849
|
* `openStream`'s transport: a local stream (a Unix socket on macOS and
|
|
1817
1850
|
* Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the
|
|
@@ -5637,8 +5670,18 @@ export class TestHost {
|
|
|
5637
5670
|
"jobs.start": (p) => {
|
|
5638
5671
|
if (!this.snapshots.has(Number(p.snapshotId)))
|
|
5639
5672
|
throw new HostError("missing", "snapshot released");
|
|
5640
|
-
|
|
5641
|
-
|
|
5673
|
+
const kind = String(p.kind);
|
|
5674
|
+
const snapshot = this.snapshots.get(Number(p.snapshotId));
|
|
5675
|
+
const clip = /^(rawMp4|processedMp4|rawCopy)$/.test(kind);
|
|
5676
|
+
if (!(/^(raw|processed)(Png|Jpeg|Bmp)$/.test(kind) || clip) ||
|
|
5677
|
+
(p.quality !== undefined && !kind.endsWith("Jpeg")) ||
|
|
5678
|
+
(p.encoding !== undefined &&
|
|
5679
|
+
(!kind.endsWith("Mp4") || !validEncoding(p.encoding))) ||
|
|
5680
|
+
(p.container !== undefined &&
|
|
5681
|
+
(kind !== "rawCopy" ||
|
|
5682
|
+
!["auto", "mp4", "mkv"].includes(String(p.container)))) ||
|
|
5683
|
+
(clip && typeof p.end !== "number") ||
|
|
5684
|
+
!kind.startsWith(snapshot.kind))
|
|
5642
5685
|
throw new HostError("invalid", `unknown job kind ${p.kind}`);
|
|
5643
5686
|
this.exports.push(structuredClone(p));
|
|
5644
5687
|
const jobId = ++this.#next.job;
|
|
@@ -5648,23 +5691,71 @@ export class TestHost {
|
|
|
5648
5691
|
publicationStarted: false,
|
|
5649
5692
|
facts: null,
|
|
5650
5693
|
outcome: null,
|
|
5694
|
+
progress: null,
|
|
5695
|
+
});
|
|
5696
|
+
this.#exportJobs.set(jobId, {
|
|
5697
|
+
destination: String(p.destination),
|
|
5698
|
+
// A copy starts 1 s early (on a "keyframe"), the player's own way of
|
|
5699
|
+
// saying the start moved; a re-encode starts on the frame.
|
|
5700
|
+
clip: clip
|
|
5701
|
+
? {
|
|
5702
|
+
start: kind === "rawCopy"
|
|
5703
|
+
? Math.max(0, snapshot.time - 1)
|
|
5704
|
+
: snapshot.time,
|
|
5705
|
+
end: Number(p.end),
|
|
5706
|
+
container: kind !== "rawCopy"
|
|
5707
|
+
? null
|
|
5708
|
+
: p.container === "mkv"
|
|
5709
|
+
? "mkv"
|
|
5710
|
+
: "mp4",
|
|
5711
|
+
}
|
|
5712
|
+
: null,
|
|
5651
5713
|
});
|
|
5652
5714
|
return { jobId, status: "queued", snapshotId: p.snapshotId };
|
|
5653
5715
|
},
|
|
5654
|
-
// Each read advances a job one step: queued → running → completed.
|
|
5716
|
+
// Each read advances a job one step: queued → running → completed. A
|
|
5717
|
+
// clip reports half its progress while running.
|
|
5655
5718
|
"jobs.state": (p) => {
|
|
5656
5719
|
const job = this.jobs.get(Number(p.jobId));
|
|
5657
5720
|
if (!job)
|
|
5658
5721
|
throw new HostError("missing", "job");
|
|
5659
5722
|
const current = structuredClone(job);
|
|
5660
|
-
|
|
5723
|
+
const made = this.#exportJobs.get(job.jobId);
|
|
5724
|
+
const clip = made?.clip;
|
|
5725
|
+
if (job.status === "queued") {
|
|
5661
5726
|
job.status = "running";
|
|
5662
|
-
|
|
5727
|
+
if (clip) {
|
|
5728
|
+
const total = clip.end - clip.start;
|
|
5729
|
+
job.progress = { done: total / 2, total };
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
else if (job.status === "running" && this.exportFailure)
|
|
5733
|
+
Object.assign(job, {
|
|
5734
|
+
status: "failed",
|
|
5735
|
+
outcome: { error: { ...this.exportFailure } },
|
|
5736
|
+
});
|
|
5737
|
+
else if (job.status === "running") {
|
|
5738
|
+
const destination = clip?.container === "mkv"
|
|
5739
|
+
? made.destination.replace(/(\.(mp4|m4v|mov|mkv|webm))?$/i, ".mkv")
|
|
5740
|
+
: clip?.container === "mp4"
|
|
5741
|
+
? made.destination.replace(/(\.(mp4|m4v|mov|mkv|webm))?$/i, ".mp4")
|
|
5742
|
+
: "/tmp/output";
|
|
5663
5743
|
Object.assign(job, {
|
|
5664
5744
|
status: "completed",
|
|
5665
5745
|
publicationStarted: true,
|
|
5666
|
-
outcome:
|
|
5746
|
+
outcome: clip
|
|
5747
|
+
? { destination, start: clip.start, end: clip.end }
|
|
5748
|
+
: { destination },
|
|
5749
|
+
...(job.progress
|
|
5750
|
+
? {
|
|
5751
|
+
progress: {
|
|
5752
|
+
done: job.progress.total,
|
|
5753
|
+
total: job.progress.total,
|
|
5754
|
+
},
|
|
5755
|
+
}
|
|
5756
|
+
: {}),
|
|
5667
5757
|
});
|
|
5758
|
+
}
|
|
5668
5759
|
else if (job.status === "cancelling")
|
|
5669
5760
|
Object.assign(job, {
|
|
5670
5761
|
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. |
|
|
@@ -8294,11 +8295,30 @@ export interface Snapshot extends Disposable {
|
|
|
8294
8295
|
* Start exporting; the job runs in the host's offline worker.
|
|
8295
8296
|
* One snapshot can be exported several times.
|
|
8296
8297
|
*
|
|
8298
|
+
* A **stream copy** (`format: "copy"`, player 0.7.6) writes the video
|
|
8299
|
+
* stream and the snapshot's audio track as they are in the file, without
|
|
8300
|
+
* decoding: lossless and about as fast as the disk, but a copy can only
|
|
8301
|
+
* begin on a keyframe. It starts at the video's last keyframe at or before
|
|
8302
|
+
* the snapshot time (its first keyframe when the video itself starts
|
|
8303
|
+
* later; keyframes an MP4 edit list discards do not count) and is cut at
|
|
8304
|
+
* `end`, where it may run a few frames past `end`. An open GOP can write
|
|
8305
|
+
* a frame or two that present before the keyframe; `start` includes them.
|
|
8306
|
+
* The finished job's `outcome` gives the `start` and `end` actually
|
|
8307
|
+
* written; a plugin that previews the cut (with `plugin.tools.run`
|
|
8308
|
+
* and ffprobe's packet flags, say) shows those once the job is done.
|
|
8309
|
+
* Copies need a raw snapshot (effects and burned-in subtitles need a
|
|
8310
|
+
* re-encode) and leave subtitle tracks and chapters out. `auto` writes
|
|
8311
|
+
* MP4 for H.264, HEVC, AV1 or MPEG-4 video with AAC, MP3, AC-3, E-AC-3 or
|
|
8312
|
+
* ALAC audio (or none), and MKV for anything else. A copy the player
|
|
8313
|
+
* cannot make (no video stream, a cover image, an external audio track,
|
|
8314
|
+
* codecs `mp4` cannot hold) ends `failed` with the code `unsupported`.
|
|
8315
|
+
*
|
|
8297
8316
|
* @capability media.read and media.control
|
|
8298
8317
|
* @throws `released` (rejects) The snapshot was already released.
|
|
8299
8318
|
* @throws `invalid` (rejects) Bad options (`quality` outside 1–100, a
|
|
8300
8319
|
* relative destination, a clip `end` not after the snapshot time or
|
|
8301
|
-
* past the duration
|
|
8320
|
+
* past the duration, an {@link ExportEncoding} value outside its set,
|
|
8321
|
+
* `copy` from a processed snapshot).
|
|
8302
8322
|
* @throws `busy` (rejects) This plugin runs 2 exports (or the player 8).
|
|
8303
8323
|
* @throws `unavailable` (rejects) No FFmpeg, or the export worker could not
|
|
8304
8324
|
* start.
|
|
@@ -8318,18 +8338,48 @@ export interface Snapshot extends Disposable {
|
|
|
8318
8338
|
* if (end.status !== "completed") await plugin.osd.show("Export {}", [end.status]);
|
|
8319
8339
|
* ```
|
|
8320
8340
|
*
|
|
8341
|
+
* @example Cut without re-encoding and report where it starts
|
|
8342
|
+
* The clip is copied as it is in the file. A copy starts on a keyframe,
|
|
8343
|
+
* so the OSD says where it actually starts; a file the player cannot
|
|
8344
|
+
* copy ends with `unsupported`, and the plugin offers a re-encode instead.
|
|
8345
|
+
* ```ts
|
|
8346
|
+
* declare const snapshot: Snapshot;
|
|
8347
|
+
* // ---cut---
|
|
8348
|
+
* const job = await snapshot.export({ format: "copy", destination: "/tmp/clip.mp4", end: snapshot.time + 20 });
|
|
8349
|
+
* const state = await job.done;
|
|
8350
|
+
* const outcome = state.outcome;
|
|
8351
|
+
* // The copy starts on a keyframe: outcome.start says where.
|
|
8352
|
+
* if (outcome && "destination" in outcome)
|
|
8353
|
+
* await plugin.osd.show("Saved {} from {} s", [outcome.destination, (outcome.start ?? snapshot.time).toFixed(1)]);
|
|
8354
|
+
* // Codecs the container cannot hold, a cover image, …
|
|
8355
|
+
* else if (outcome && "error" in outcome && outcome.error.code === "unsupported")
|
|
8356
|
+
* await plugin.osd.show("Fast cut is not possible for this file");
|
|
8357
|
+
* ```
|
|
8358
|
+
*
|
|
8321
8359
|
* @since 1.0.0
|
|
8322
8360
|
*/
|
|
8323
8361
|
export(options: ExportOptions): Promise<ExportJob>;
|
|
8324
8362
|
}
|
|
8325
8363
|
/**
|
|
8326
8364
|
* One frame as PNG, JPEG (`quality` 1–100, default 90) or BMP (24-bit), or
|
|
8327
|
-
* a clip as MP4
|
|
8365
|
+
* a clip: re-encoded as MP4 (`mp4`, frame-accurate, optional
|
|
8366
|
+
* {@link ExportEncoding}) or copied without re-encoding (`copy`, player
|
|
8367
|
+
* 0.7.6, starting on a keyframe).
|
|
8328
8368
|
*
|
|
8329
8369
|
* `destination` is never overwritten if it exists. The extension does not
|
|
8330
|
-
* choose the format
|
|
8331
|
-
*
|
|
8332
|
-
*
|
|
8370
|
+
* choose the format (a copy's extension is set to its container).
|
|
8371
|
+
* `sampleTime` picks the frame (default: the snapshot time); `end` is the
|
|
8372
|
+
* clip's end in source seconds. On Windows the export worker needs FFmpeg
|
|
8373
|
+
* 6.1 or newer.
|
|
8374
|
+
*
|
|
8375
|
+
* **Which players have `copy`, `encoding` and the progress and range of
|
|
8376
|
+
* a job**: player 0.7.6 and later. Declare `"minPlayerVersion": "0.7.6"`,
|
|
8377
|
+
* or, to keep older players and offer these only where they exist, compare
|
|
8378
|
+
* {@link AppInfo.version | plugin.app.version} with `0.7.6`, as for any
|
|
8379
|
+
* newer API. Do not probe by trying: on an older player the `plugin`
|
|
8380
|
+
* object comes from that player's own, older SDK, which rejects `copy` with
|
|
8381
|
+
* `invalid` but builds a re-encode request without the `encoding` it does
|
|
8382
|
+
* not know, so the clip is encoded with the fixed settings and no error.
|
|
8333
8383
|
*
|
|
8334
8384
|
* @since 1.0.0
|
|
8335
8385
|
* @category Media
|
|
@@ -8351,19 +8401,132 @@ export type ExportOptions = {
|
|
|
8351
8401
|
/** Integer 1–100, default 90. */
|
|
8352
8402
|
quality?: number;
|
|
8353
8403
|
} | {
|
|
8354
|
-
/** A clip from the snapshot time. */
|
|
8404
|
+
/** A clip from the snapshot time, re-encoded as H.264 + AAC. */
|
|
8355
8405
|
format: "mp4";
|
|
8356
8406
|
/** Absolute path of the new file. */
|
|
8357
8407
|
destination: string;
|
|
8358
8408
|
/** Clip end, source seconds. */
|
|
8359
8409
|
end: number;
|
|
8410
|
+
/**
|
|
8411
|
+
* Encoder settings (player 0.7.6); without it, the settings before
|
|
8412
|
+
* they existed.
|
|
8413
|
+
*
|
|
8414
|
+
* @since 1.6.10
|
|
8415
|
+
*/
|
|
8416
|
+
encoding?: ExportEncoding;
|
|
8417
|
+
} | {
|
|
8418
|
+
/**
|
|
8419
|
+
* A clip copied stream by stream, without re-encoding (player 0.7.6):
|
|
8420
|
+
* lossless and about as fast as the disk. It starts on the video
|
|
8421
|
+
* keyframe at or before the snapshot time; see {@link Snapshot.export}.
|
|
8422
|
+
*
|
|
8423
|
+
* @since 1.6.10
|
|
8424
|
+
*/
|
|
8425
|
+
format: "copy";
|
|
8426
|
+
/**
|
|
8427
|
+
* Absolute path of the new file. The player gives it the extension of
|
|
8428
|
+
* the container it writes (`.mp4` or `.mkv`); the finished job's
|
|
8429
|
+
* `outcome.destination` is the actual path.
|
|
8430
|
+
*/
|
|
8431
|
+
destination: string;
|
|
8432
|
+
/** Clip end, source seconds. */
|
|
8433
|
+
end: number;
|
|
8434
|
+
/**
|
|
8435
|
+
* `auto` (default): MP4 when the codecs fit it, otherwise MKV. `mp4`
|
|
8436
|
+
* fails with `unsupported` when they do not; `mkv` always writes MKV.
|
|
8437
|
+
*/
|
|
8438
|
+
container?: "auto" | "mp4" | "mkv";
|
|
8360
8439
|
};
|
|
8440
|
+
/**
|
|
8441
|
+
* The encoder settings of a re-encoded clip (`format: "mp4"`, player
|
|
8442
|
+
* 0.7.6): a closed set of values, never encoder arguments. Every field is
|
|
8443
|
+
* optional; the defaults are the settings clips had before: CRF 18, the
|
|
8444
|
+
* fastest speed, the source's size and frame rate, AAC 192 kb/s, one thread.
|
|
8445
|
+
* A value outside its set rejects the export with `invalid`. `videoBitrate:
|
|
8446
|
+
* "source"` replaces the CRF with the source's bitrate, for a "Same as
|
|
8447
|
+
* source" preset.
|
|
8448
|
+
*
|
|
8449
|
+
* @example A small file
|
|
8450
|
+
* A 720p, 30 fps clip at a lower quality, encoded with the cores the player
|
|
8451
|
+
* can spare.
|
|
8452
|
+
* ```ts
|
|
8453
|
+
* declare const snapshot: Snapshot;
|
|
8454
|
+
* // ---cut---
|
|
8455
|
+
* // Each field is a ceiling or a choice from a closed set.
|
|
8456
|
+
* const job = await snapshot.export({
|
|
8457
|
+
* format: "mp4",
|
|
8458
|
+
* destination: "/tmp/small.mp4",
|
|
8459
|
+
* end: snapshot.time + 30,
|
|
8460
|
+
* encoding: { crf: 26, maxHeight: 720, fps: 30, audioBitrate: 128, threads: "auto" },
|
|
8461
|
+
* });
|
|
8462
|
+
* ```
|
|
8463
|
+
*
|
|
8464
|
+
* @since 1.6.10
|
|
8465
|
+
* @category Media
|
|
8466
|
+
*/
|
|
8467
|
+
export interface ExportEncoding {
|
|
8468
|
+
/**
|
|
8469
|
+
* H.264 quality, an integer 14–30 (x264's CRF): lower is a better picture
|
|
8470
|
+
* and a bigger file. Default 18.
|
|
8471
|
+
*/
|
|
8472
|
+
crf?: number;
|
|
8473
|
+
/**
|
|
8474
|
+
* Encoding effort: `faster` (default), `balanced` or `smaller` (slower,
|
|
8475
|
+
* a smaller file at the same quality); x264's `veryfast`, `medium` and
|
|
8476
|
+
* `slow`.
|
|
8477
|
+
*/
|
|
8478
|
+
speed?: "faster" | "balanced" | "smaller";
|
|
8479
|
+
/**
|
|
8480
|
+
* The tallest picture: a taller one is scaled down to this height,
|
|
8481
|
+
* keeping its shape; a shorter one is left as it is. Default: the
|
|
8482
|
+
* source's size.
|
|
8483
|
+
*/
|
|
8484
|
+
maxHeight?: 1080 | 720 | 480;
|
|
8485
|
+
/**
|
|
8486
|
+
* The highest frame rate: a faster source drops frames to it; a slower
|
|
8487
|
+
* one is left as it is. Default: the source's rate.
|
|
8488
|
+
*/
|
|
8489
|
+
fps?: 60 | 30;
|
|
8490
|
+
/** AAC audio bitrate in kb/s. Default 192. */
|
|
8491
|
+
audioBitrate?: 128 | 192 | 256;
|
|
8492
|
+
/**
|
|
8493
|
+
* `single` (default) encodes on one thread, leaving the other cores to
|
|
8494
|
+
* playback. `auto` lets the player use more: half the logical
|
|
8495
|
+
* processors, at most 8, so playback and the interface keep their rate.
|
|
8496
|
+
* From player 0.7.6 every export runs at below-normal priority (as
|
|
8497
|
+
* HandBrake and OBS run their encoders), with either setting.
|
|
8498
|
+
*/
|
|
8499
|
+
threads?: "single" | "auto";
|
|
8500
|
+
/**
|
|
8501
|
+
* `source`: aim at the source video's average bitrate instead of a
|
|
8502
|
+
* quality level (peaks up to 1.5 times it), so the file comes out about
|
|
8503
|
+
* the size of that stretch of the source: a "Same as source" preset. Not
|
|
8504
|
+
* together with `crf` (`invalid`). A source whose bitrate the player
|
|
8505
|
+
* cannot tell ends the job `failed` with `unsupported`.
|
|
8506
|
+
*/
|
|
8507
|
+
videoBitrate?: "source";
|
|
8508
|
+
}
|
|
8361
8509
|
/**
|
|
8362
8510
|
* An export job's state. `status` is `queued`, `running`, `cancelling`,
|
|
8363
8511
|
* `completed`, `failed` or `cancelled`; once terminal, `outcome` is
|
|
8364
8512
|
* `{ destination }`, `{ error: { code, message } }` or `{ cancelled: true }`.
|
|
8365
8513
|
* `facts` describes what the worker produced (versions, range, streams).
|
|
8366
8514
|
*
|
|
8515
|
+
* From player 0.7.6:
|
|
8516
|
+
* - a finished clip's `outcome` also has `start` and `end`, the source
|
|
8517
|
+
* times of its first frame and of the end of its last frame as written: a
|
|
8518
|
+
* copy's earliest written frame (its keyframe, or the leading frames of
|
|
8519
|
+
* an open GOP) and actual end; a re-encode's start and start
|
|
8520
|
+
* plus the encoded length.
|
|
8521
|
+
* - `progress` is `{ done, total }` in seconds of the clip while FFmpeg
|
|
8522
|
+
* writes it (at most 4 updates a second; the SDK reads it about every
|
|
8523
|
+
* 350 ms), `null` while unknown: before writing starts, and always for a
|
|
8524
|
+
* frame. Show an indeterminate bar then. After `done` reaches `total` the
|
|
8525
|
+
* player still checks the file before the job completes.
|
|
8526
|
+
* - `error.code` is `export-failed`, or `unsupported` for an export the
|
|
8527
|
+
* player cannot make as asked (a stream copy it cannot make, a
|
|
8528
|
+
* `videoBitrate: "source"` whose source bitrate it cannot tell).
|
|
8529
|
+
*
|
|
8367
8530
|
* @since 1.0.0
|
|
8368
8531
|
* @category Media
|
|
8369
8532
|
*/
|
package/dist/wire.d.ts
CHANGED
|
@@ -798,6 +798,11 @@ export declare function createWire(transport: Transport): {
|
|
|
798
798
|
} | {
|
|
799
799
|
kind: "rawMp4" | "processedMp4";
|
|
800
800
|
end: number;
|
|
801
|
+
encoding?: import("./media.ts").WireEncoding;
|
|
802
|
+
} | {
|
|
803
|
+
kind: "rawCopy";
|
|
804
|
+
end: number;
|
|
805
|
+
container?: "auto" | "mp4" | "mkv";
|
|
801
806
|
})) => Promise<{
|
|
802
807
|
jobId: number;
|
|
803
808
|
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.10: 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
|