@bitmagic/asset-core 0.2.24 → 0.2.25-dev.1
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/dist/trailer/analyze.d.ts +24 -0
- package/dist/trailer/analyze.d.ts.map +1 -0
- package/dist/trailer/analyze.js +186 -0
- package/dist/trailer/analyze.js.map +1 -0
- package/dist/trailer/auto-edl.d.ts +51 -0
- package/dist/trailer/auto-edl.d.ts.map +1 -0
- package/dist/trailer/auto-edl.js +266 -0
- package/dist/trailer/auto-edl.js.map +1 -0
- package/dist/trailer/beat-cut.d.ts +58 -0
- package/dist/trailer/beat-cut.d.ts.map +1 -0
- package/dist/trailer/beat-cut.js +232 -0
- package/dist/trailer/beat-cut.js.map +1 -0
- package/dist/trailer/capture.d.ts +102 -0
- package/dist/trailer/capture.d.ts.map +1 -0
- package/dist/trailer/capture.js +268 -0
- package/dist/trailer/capture.js.map +1 -0
- package/dist/trailer/edl-time.d.ts +62 -0
- package/dist/trailer/edl-time.d.ts.map +1 -0
- package/dist/trailer/edl-time.js +105 -0
- package/dist/trailer/edl-time.js.map +1 -0
- package/dist/trailer/errors.d.ts +15 -0
- package/dist/trailer/errors.d.ts.map +1 -0
- package/dist/trailer/errors.js +22 -0
- package/dist/trailer/errors.js.map +1 -0
- package/dist/trailer/ffmpeg.d.ts +19 -0
- package/dist/trailer/ffmpeg.d.ts.map +1 -0
- package/dist/trailer/ffmpeg.js +100 -0
- package/dist/trailer/ffmpeg.js.map +1 -0
- package/dist/trailer/hud-state.d.ts +182 -0
- package/dist/trailer/hud-state.d.ts.map +1 -0
- package/dist/trailer/hud-state.js +523 -0
- package/dist/trailer/hud-state.js.map +1 -0
- package/dist/trailer/index.d.ts +30 -0
- package/dist/trailer/index.d.ts.map +1 -0
- package/dist/trailer/index.js +30 -0
- package/dist/trailer/index.js.map +1 -0
- package/dist/trailer/io.d.ts +23 -0
- package/dist/trailer/io.d.ts.map +1 -0
- package/dist/trailer/io.js +20 -0
- package/dist/trailer/io.js.map +1 -0
- package/dist/trailer/link.d.ts +2 -0
- package/dist/trailer/link.d.ts.map +1 -0
- package/dist/trailer/link.js +15 -0
- package/dist/trailer/link.js.map +1 -0
- package/dist/trailer/make.d.ts +60 -0
- package/dist/trailer/make.d.ts.map +1 -0
- package/dist/trailer/make.js +196 -0
- package/dist/trailer/make.js.map +1 -0
- package/dist/trailer/merge.d.ts +8 -0
- package/dist/trailer/merge.d.ts.map +1 -0
- package/dist/trailer/merge.js +165 -0
- package/dist/trailer/merge.js.map +1 -0
- package/dist/trailer/music-beats.d.ts +124 -0
- package/dist/trailer/music-beats.d.ts.map +1 -0
- package/dist/trailer/music-beats.js +672 -0
- package/dist/trailer/music-beats.js.map +1 -0
- package/dist/trailer/paths.d.ts +29 -0
- package/dist/trailer/paths.d.ts.map +1 -0
- package/dist/trailer/paths.js +101 -0
- package/dist/trailer/paths.js.map +1 -0
- package/dist/trailer/png.d.ts +10 -0
- package/dist/trailer/png.d.ts.map +1 -0
- package/dist/trailer/png.js +37 -0
- package/dist/trailer/png.js.map +1 -0
- package/dist/trailer/render.d.ts +93 -0
- package/dist/trailer/render.d.ts.map +1 -0
- package/dist/trailer/render.js +527 -0
- package/dist/trailer/render.js.map +1 -0
- package/dist/trailer/revision.d.ts +9 -0
- package/dist/trailer/revision.d.ts.map +1 -0
- package/dist/trailer/revision.js +31 -0
- package/dist/trailer/revision.js.map +1 -0
- package/dist/trailer/synth.d.ts +10 -0
- package/dist/trailer/synth.d.ts.map +1 -0
- package/dist/trailer/synth.js +120 -0
- package/dist/trailer/synth.js.map +1 -0
- package/dist/trailer/timeline-types.d.ts +196 -0
- package/dist/trailer/timeline-types.d.ts.map +1 -0
- package/dist/trailer/timeline-types.js +419 -0
- package/dist/trailer/timeline-types.js.map +1 -0
- package/dist/trailer/titles.d.ts +38 -0
- package/dist/trailer/titles.d.ts.map +1 -0
- package/dist/trailer/titles.js +110 -0
- package/dist/trailer/titles.js.map +1 -0
- package/dist/trailer/video-chain.d.ts +130 -0
- package/dist/trailer/video-chain.d.ts.map +1 -0
- package/dist/trailer/video-chain.js +261 -0
- package/dist/trailer/video-chain.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure EDL time mapping: source frames -> output-timeline seconds.
|
|
3
|
+
*
|
|
4
|
+
* Crossfades OVERLAP the previous clip, so every clip after a crossfade
|
|
5
|
+
* starts (and ends) earlier on the output timeline than a cut-only chain
|
|
6
|
+
* would place it. All render math (xfade offsets, sound delays, total
|
|
7
|
+
* duration) derives from this module so it can be unit-tested without ffmpeg.
|
|
8
|
+
*/
|
|
9
|
+
import type { EdlClip, TimelineSound } from './timeline-types.js';
|
|
10
|
+
export declare const DEFAULT_CROSSFADE_SEC = 0.25;
|
|
11
|
+
export declare const DEFAULT_TAIL_PAD_FRAMES = 30;
|
|
12
|
+
export declare const MIN_SPEED = 0.25;
|
|
13
|
+
export declare const MAX_SPEED = 2;
|
|
14
|
+
export interface ClipTiming {
|
|
15
|
+
index: number;
|
|
16
|
+
in: number;
|
|
17
|
+
out: number;
|
|
18
|
+
speed: number;
|
|
19
|
+
/** Resolved transition INTO this clip; first clip is always 'cut'. */
|
|
20
|
+
transition: 'cut' | 'crossfade';
|
|
21
|
+
/** Seconds; 0 for cuts. Also the xfade offset shortfall vs a plain concat. */
|
|
22
|
+
transitionDuration: number;
|
|
23
|
+
/** Seconds on the output timeline where this clip's first frame lands. */
|
|
24
|
+
outputStart: number;
|
|
25
|
+
/** Seconds this clip occupies on the output timeline (source frames / fps / speed). */
|
|
26
|
+
outputDuration: number;
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Per-clip UI-overlay override; undefined means "follow the global --ui flag". */
|
|
29
|
+
ui?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface ClipTimelineResult {
|
|
32
|
+
timings: ClipTiming[];
|
|
33
|
+
totalDuration: number;
|
|
34
|
+
warnings: string[];
|
|
35
|
+
}
|
|
36
|
+
export declare function computeClipTimeline(clips: EdlClip[], fps: number): ClipTimelineResult;
|
|
37
|
+
/**
|
|
38
|
+
* Output-timeline seconds where a source frame inside `timing` lands. Frames
|
|
39
|
+
* before the clip's first frame (sound tail pad) clamp to the clip start.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sourceFrameToOutputSec(timing: ClipTiming, sourceFrame: number, fps: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Output time at which a clip stops being the only source on screen. A
|
|
44
|
+
* crossfade INTO the next clip begins at that clip's outputStart — before this
|
|
45
|
+
* clip's last frame — so anything that must not double up (UI overlays) hands
|
|
46
|
+
* over there rather than at this clip's own end.
|
|
47
|
+
*/
|
|
48
|
+
export declare function exclusiveOutputEnd(timings: readonly ClipTiming[], index: number): number;
|
|
49
|
+
export interface PlacedSound {
|
|
50
|
+
sound: TimelineSound;
|
|
51
|
+
clipIndex: number;
|
|
52
|
+
/** Seconds on the output timeline where this sound instance starts. */
|
|
53
|
+
outputTimeSec: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Place timeline sounds onto the output timeline. A clip picks up sounds with
|
|
57
|
+
* frame in [in - tailPadFrames, out); sounds from the tail pad (before the
|
|
58
|
+
* clip's first frame) clamp to the clip's output start. The same sound can be
|
|
59
|
+
* placed once per clip whose source range covers it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function placeSounds(timings: ClipTiming[], sounds: TimelineSound[], fps: number, tailPadFrames?: number): PlacedSound[];
|
|
62
|
+
//# sourceMappingURL=edl-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edl-time.d.ts","sourceRoot":"","sources":["../../src/trailer/edl-time.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGlE,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,SAAS,OAAO,CAAC;AAC9B,eAAO,MAAM,SAAS,IAAM,CAAC;AAE7B,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,UAAU,EAAE,KAAK,GAAG,WAAW,CAAC;IAChC,8EAA8E;IAC9E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,WAAW,EAAE,MAAM,CAAC;IACpB,uFAAuF;IACvF,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAyDrF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnG;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKxF;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,aAAa,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,UAAU,EAAE,EACrB,MAAM,EAAE,aAAa,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,aAAa,GAAE,MAAgC,GAC9C,WAAW,EAAE,CAWf"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { TrailerError } from './errors.js';
|
|
2
|
+
export const DEFAULT_CROSSFADE_SEC = 0.25;
|
|
3
|
+
export const DEFAULT_TAIL_PAD_FRAMES = 30;
|
|
4
|
+
export const MIN_SPEED = 0.25;
|
|
5
|
+
export const MAX_SPEED = 2.0;
|
|
6
|
+
export function computeClipTimeline(clips, fps) {
|
|
7
|
+
if (clips.length === 0)
|
|
8
|
+
throw new TrailerError('EDL has no clips');
|
|
9
|
+
if (!Number.isFinite(fps) || fps <= 0)
|
|
10
|
+
throw new Error(`invalid fps: ${fps}`);
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const timings = [];
|
|
13
|
+
let cursor = 0; // output-timeline end of the previous clip
|
|
14
|
+
clips.forEach((clip, index) => {
|
|
15
|
+
if (!Number.isInteger(clip.in) || !Number.isInteger(clip.out) || clip.in < 0 || clip.out <= clip.in) {
|
|
16
|
+
throw new Error(`clip ${index}: invalid frame range in=${clip.in} out=${clip.out}`);
|
|
17
|
+
}
|
|
18
|
+
let speed = clip.speed ?? 1;
|
|
19
|
+
if (speed < MIN_SPEED || speed > MAX_SPEED) {
|
|
20
|
+
const clamped = Math.min(MAX_SPEED, Math.max(MIN_SPEED, speed));
|
|
21
|
+
warnings.push(`clip ${index}: speed ${speed} outside [${MIN_SPEED}, ${MAX_SPEED}], clamped to ${clamped}`);
|
|
22
|
+
speed = clamped;
|
|
23
|
+
}
|
|
24
|
+
const outputDuration = (clip.out - clip.in) / fps / speed;
|
|
25
|
+
let transition = 'cut';
|
|
26
|
+
let transitionDuration = 0;
|
|
27
|
+
if (index === 0) {
|
|
28
|
+
if (clip.transition === 'crossfade')
|
|
29
|
+
warnings.push('clip 0: transition on the first clip is ignored');
|
|
30
|
+
}
|
|
31
|
+
else if (clip.transition === 'crossfade') {
|
|
32
|
+
transition = 'crossfade';
|
|
33
|
+
transitionDuration = clip.transitionDuration ?? DEFAULT_CROSSFADE_SEC;
|
|
34
|
+
if (transitionDuration <= 0) {
|
|
35
|
+
warnings.push(`clip ${index}: non-positive transitionDuration ${transitionDuration}, treating as cut`);
|
|
36
|
+
transition = 'cut';
|
|
37
|
+
transitionDuration = 0;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const maxFade = Math.min(timings[index - 1].outputDuration, outputDuration);
|
|
41
|
+
if (transitionDuration > maxFade) {
|
|
42
|
+
warnings.push(`clip ${index}: transitionDuration ${transitionDuration} longer than an adjacent clip, clamped to ${maxFade}`);
|
|
43
|
+
transitionDuration = maxFade;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const outputStart = index === 0 ? 0 : cursor - transitionDuration;
|
|
48
|
+
const timing = {
|
|
49
|
+
index,
|
|
50
|
+
in: clip.in,
|
|
51
|
+
out: clip.out,
|
|
52
|
+
speed,
|
|
53
|
+
transition,
|
|
54
|
+
transitionDuration,
|
|
55
|
+
outputStart,
|
|
56
|
+
outputDuration
|
|
57
|
+
};
|
|
58
|
+
if (clip.label !== undefined)
|
|
59
|
+
timing.label = clip.label;
|
|
60
|
+
if (clip.ui !== undefined)
|
|
61
|
+
timing.ui = clip.ui;
|
|
62
|
+
timings.push(timing);
|
|
63
|
+
cursor = outputStart + outputDuration;
|
|
64
|
+
});
|
|
65
|
+
return { timings, totalDuration: cursor, warnings };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Output-timeline seconds where a source frame inside `timing` lands. Frames
|
|
69
|
+
* before the clip's first frame (sound tail pad) clamp to the clip start.
|
|
70
|
+
*/
|
|
71
|
+
export function sourceFrameToOutputSec(timing, sourceFrame, fps) {
|
|
72
|
+
return timing.outputStart + Math.max(0, sourceFrame - timing.in) / fps / timing.speed;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Output time at which a clip stops being the only source on screen. A
|
|
76
|
+
* crossfade INTO the next clip begins at that clip's outputStart — before this
|
|
77
|
+
* clip's last frame — so anything that must not double up (UI overlays) hands
|
|
78
|
+
* over there rather than at this clip's own end.
|
|
79
|
+
*/
|
|
80
|
+
export function exclusiveOutputEnd(timings, index) {
|
|
81
|
+
const timing = timings[index];
|
|
82
|
+
const ownEnd = timing.outputStart + timing.outputDuration;
|
|
83
|
+
const next = timings[index + 1];
|
|
84
|
+
return next === undefined ? ownEnd : Math.min(ownEnd, next.outputStart);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Place timeline sounds onto the output timeline. A clip picks up sounds with
|
|
88
|
+
* frame in [in - tailPadFrames, out); sounds from the tail pad (before the
|
|
89
|
+
* clip's first frame) clamp to the clip's output start. The same sound can be
|
|
90
|
+
* placed once per clip whose source range covers it.
|
|
91
|
+
*/
|
|
92
|
+
export function placeSounds(timings, sounds, fps, tailPadFrames = DEFAULT_TAIL_PAD_FRAMES) {
|
|
93
|
+
const placements = [];
|
|
94
|
+
for (const timing of timings) {
|
|
95
|
+
for (const sound of sounds) {
|
|
96
|
+
if (sound.frame < timing.in - tailPadFrames || sound.frame >= timing.out)
|
|
97
|
+
continue;
|
|
98
|
+
const outputTimeSec = sourceFrameToOutputSec(timing, sound.frame, fps);
|
|
99
|
+
placements.push({ sound, clipIndex: timing.index, outputTimeSec });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
placements.sort((a, b) => a.outputTimeSec - b.outputTimeSec);
|
|
103
|
+
return placements;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=edl-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edl-time.js","sourceRoot":"","sources":["../../src/trailer/edl-time.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An error whose message is meant for the person running the trailer pipeline, not a stack trace.
|
|
3
|
+
*
|
|
4
|
+
* The same shape as the CLI's `TrailerError` — which is what these modules threw while they lived in
|
|
5
|
+
* `cli/src/trailer/` — so the CLI prints it as one clean line with its exit code and reports `code`
|
|
6
|
+
* in its command-outcome telemetry, and a server host can surface the message as a job failure.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TrailerError extends Error {
|
|
9
|
+
readonly exitCode: number;
|
|
10
|
+
readonly code: string | undefined;
|
|
11
|
+
constructor(message: string, exitCode?: number, code?: string);
|
|
12
|
+
}
|
|
13
|
+
/** The `code` off a Node fs/child-process error, without pulling in the NodeJS global namespace. */
|
|
14
|
+
export declare function errnoCode(error: unknown): string | undefined;
|
|
15
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/trailer/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEtB,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI,EAAE,IAAI,CAAC,EAAE,MAAM;CAMzD;AAED,oGAAoG;AACpG,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAI5D"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An error whose message is meant for the person running the trailer pipeline, not a stack trace.
|
|
3
|
+
*
|
|
4
|
+
* The same shape as the CLI's `TrailerError` — which is what these modules threw while they lived in
|
|
5
|
+
* `cli/src/trailer/` — so the CLI prints it as one clean line with its exit code and reports `code`
|
|
6
|
+
* in its command-outcome telemetry, and a server host can surface the message as a job failure.
|
|
7
|
+
*/
|
|
8
|
+
export class TrailerError extends Error {
|
|
9
|
+
constructor(message, exitCode = 1, code) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'TrailerError';
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** The `code` off a Node fs/child-process error, without pulling in the NodeJS global namespace. */
|
|
17
|
+
export function errnoCode(error) {
|
|
18
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
19
|
+
? String(error.code)
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/trailer/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAIrC,YAAY,OAAe,EAAE,QAAQ,GAAG,CAAC,EAAE,IAAa;QACtD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,oGAAoG;AACpG,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK;QACnE,CAAC,CAAC,MAAM,CAAE,KAA2B,CAAC,IAAI,CAAC;QAC3C,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type FfmpegTool = 'ffmpeg' | 'ffprobe';
|
|
2
|
+
export declare function missingFfmpegMessage(tool?: FfmpegTool): string;
|
|
3
|
+
/**
|
|
4
|
+
* Refuse up front when a tool is missing, before any frames are read or sparks spent. `spawnSync`
|
|
5
|
+
* with `-version` is the cheapest probe that also proves the binary actually runs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureFfmpeg(tools?: readonly FfmpegTool[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* The first of `tools` that is not on PATH, or null when they all are — the non-throwing half of
|
|
10
|
+
* {@link ensureFfmpeg}, for callers that skip a step rather than fail when ffmpeg is absent.
|
|
11
|
+
*/
|
|
12
|
+
export declare function missingFfmpegTool(tools?: readonly FfmpegTool[]): FfmpegTool | null;
|
|
13
|
+
export declare function runFfmpeg(args: string[]): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Run a tool and keep stdout. {@link runFfmpeg} discards it — everything in the trailer pipeline
|
|
16
|
+
* writes files — so the PCM decode and every ffprobe query come through here instead.
|
|
17
|
+
*/
|
|
18
|
+
export declare function captureFfmpegTool(tool: FfmpegTool, args: string[]): Promise<Buffer>;
|
|
19
|
+
//# sourceMappingURL=ffmpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/trailer/ffmpeg.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9C,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,UAAqB,GAAG,MAAM,CAMxE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,GAAE,SAAS,UAAU,EAAe,GAAG,IAAI,CAG5E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,SAAS,UAAU,EAAe,GAAG,UAAU,GAAG,IAAI,CAM9F;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB7D;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBzF"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin ffmpeg process wrapper. Always argument arrays, never shell strings.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg is the one external tool this CLI needs and the one a creator's machine is least likely to
|
|
5
|
+
* have, so its absence is reported as a sentence with the install command rather than as an ENOENT
|
|
6
|
+
* stack trace from deep inside a filtergraph.
|
|
7
|
+
*
|
|
8
|
+
* Two callers with opposite stakes, hence both `ensureFfmpeg` and `missingFfmpegTool`: the trailer
|
|
9
|
+
* lane cannot do its job at all without ffmpeg and refuses up front, while `generate video`'s clip
|
|
10
|
+
* review is a nicety on top of an already-paid generation and must degrade to a note.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn, spawnSync } from 'child_process';
|
|
13
|
+
import { TrailerError, errnoCode } from './errors.js';
|
|
14
|
+
const STDERR_KEEP_BYTES = 65536;
|
|
15
|
+
const STDERR_REPORT_LINES = 30;
|
|
16
|
+
export function missingFfmpegMessage(tool = 'ffmpeg') {
|
|
17
|
+
return (`${tool} was not found on PATH. \`bitmagic trailer\` cuts and encodes with ffmpeg (and ffprobe) — ` +
|
|
18
|
+
'install it (macOS: brew install ffmpeg · Debian/Ubuntu: sudo apt install ffmpeg · ' +
|
|
19
|
+
'Windows: winget install Gyan.FFmpeg) and run the command again.');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Refuse up front when a tool is missing, before any frames are read or sparks spent. `spawnSync`
|
|
23
|
+
* with `-version` is the cheapest probe that also proves the binary actually runs.
|
|
24
|
+
*/
|
|
25
|
+
export function ensureFfmpeg(tools = ['ffmpeg']) {
|
|
26
|
+
const missing = missingFfmpegTool(tools);
|
|
27
|
+
if (missing !== null)
|
|
28
|
+
throw new TrailerError(missingFfmpegMessage(missing), 1, 'ffmpeg_missing');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The first of `tools` that is not on PATH, or null when they all are — the non-throwing half of
|
|
32
|
+
* {@link ensureFfmpeg}, for callers that skip a step rather than fail when ffmpeg is absent.
|
|
33
|
+
*/
|
|
34
|
+
export function missingFfmpegTool(tools = ['ffmpeg']) {
|
|
35
|
+
for (const tool of tools) {
|
|
36
|
+
const probe = spawnSync(tool, ['-version'], { stdio: 'ignore' });
|
|
37
|
+
if (probe.error !== undefined && errnoCode(probe.error) === 'ENOENT')
|
|
38
|
+
return tool;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
export async function runFfmpeg(args) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
45
|
+
let stderr = '';
|
|
46
|
+
child.stderr.on('data', (chunk) => {
|
|
47
|
+
stderr += chunk.toString('utf-8');
|
|
48
|
+
if (stderr.length > STDERR_KEEP_BYTES)
|
|
49
|
+
stderr = stderr.slice(-STDERR_KEEP_BYTES / 2);
|
|
50
|
+
});
|
|
51
|
+
child.on('error', (err) => {
|
|
52
|
+
if (errnoCode(err) === 'ENOENT')
|
|
53
|
+
reject(new TrailerError(missingFfmpegMessage('ffmpeg'), 1, 'ffmpeg_missing'));
|
|
54
|
+
else
|
|
55
|
+
reject(new Error(`failed to start ffmpeg: ${err.message}`));
|
|
56
|
+
});
|
|
57
|
+
child.on('close', (code) => {
|
|
58
|
+
if (code === 0) {
|
|
59
|
+
resolve();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const tail = stderr.trim().split('\n').slice(-STDERR_REPORT_LINES).join('\n');
|
|
63
|
+
reject(new TrailerError(`ffmpeg exited with code ${code}:\n${tail}`));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Run a tool and keep stdout. {@link runFfmpeg} discards it — everything in the trailer pipeline
|
|
70
|
+
* writes files — so the PCM decode and every ffprobe query come through here instead.
|
|
71
|
+
*/
|
|
72
|
+
export async function captureFfmpegTool(tool, args) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const child = spawn(tool, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
75
|
+
const chunks = [];
|
|
76
|
+
let stderr = '';
|
|
77
|
+
child.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
78
|
+
child.stderr.on('data', (chunk) => {
|
|
79
|
+
stderr += chunk.toString('utf-8');
|
|
80
|
+
if (stderr.length > STDERR_KEEP_BYTES)
|
|
81
|
+
stderr = stderr.slice(-STDERR_KEEP_BYTES / 2);
|
|
82
|
+
});
|
|
83
|
+
child.on('error', (err) => {
|
|
84
|
+
if (errnoCode(err) === 'ENOENT')
|
|
85
|
+
reject(new TrailerError(missingFfmpegMessage(tool), 1, 'ffmpeg_missing'));
|
|
86
|
+
else
|
|
87
|
+
reject(new Error(`failed to start ${tool}: ${err.message}`));
|
|
88
|
+
});
|
|
89
|
+
child.on('close', (code) => {
|
|
90
|
+
if (code === 0) {
|
|
91
|
+
resolve(Buffer.concat(chunks));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const tail = stderr.trim().split('\n').slice(-STDERR_REPORT_LINES).join('\n');
|
|
95
|
+
reject(new Error(`${tool} exited with code ${code}:\n${tail}`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=ffmpeg.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/trailer/ffmpeg.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HUD replay: turn the recorded `hud[]` op track back into UI state.
|
|
3
|
+
*
|
|
4
|
+
* Captured frames are UI-less (the HUD is DOM, not WebGL), so the engine logs
|
|
5
|
+
* semantic HUD *state changes* instead of pixels. This module reduces that
|
|
6
|
+
* sparse op stream back to the complete UI state at any source frame
|
|
7
|
+
* (`reduceHudState`), and picks the frames worth rasterizing per EDL clip
|
|
8
|
+
* (`collectUiKeyframes`). Output-timeline placement comes from edl-time.ts —
|
|
9
|
+
* this file never re-derives crossfade/speed math.
|
|
10
|
+
*
|
|
11
|
+
* Timers are the one derived value: they are logged as start/pause/reset ops,
|
|
12
|
+
* never per tick, and the displayed value is computed from FRAME time (which
|
|
13
|
+
* is more correct than the widget's wall clock under a dilated recording).
|
|
14
|
+
*/
|
|
15
|
+
import type { ClipTiming } from './edl-time.js';
|
|
16
|
+
import type { HudElementKind, HudOp } from './timeline-types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Engine-owned chrome that lives in the HUD element registry but must never
|
|
19
|
+
* reach a trailer: interactive controls the viewer can't click in a video.
|
|
20
|
+
* `mute-button` is `MuteControl`'s custom element (engine/ui/MuteControl.ts).
|
|
21
|
+
*/
|
|
22
|
+
export declare const CHROME_ELEMENT_IDS: ReadonlySet<string>;
|
|
23
|
+
export interface HudHealthState {
|
|
24
|
+
current: number;
|
|
25
|
+
max: number;
|
|
26
|
+
visible: boolean;
|
|
27
|
+
/** Pixel width from showHealth({width}); absent = the engine default. */
|
|
28
|
+
width?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One HUD element. Keys beyond id/type/anchor/visible are per-kind and are
|
|
32
|
+
* omitted when the recording never set them.
|
|
33
|
+
*/
|
|
34
|
+
export interface HudElementState {
|
|
35
|
+
id: string;
|
|
36
|
+
type: HudElementKind;
|
|
37
|
+
anchor: string;
|
|
38
|
+
visible: boolean;
|
|
39
|
+
label?: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
width?: number;
|
|
42
|
+
showText?: boolean;
|
|
43
|
+
percent?: number;
|
|
44
|
+
icon?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
iconSize?: number;
|
|
47
|
+
/** Timer only: the DERIVED displayed seconds; the harness formats it. */
|
|
48
|
+
seconds?: number;
|
|
49
|
+
countDown?: boolean;
|
|
50
|
+
format?: string;
|
|
51
|
+
html?: string;
|
|
52
|
+
css?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface HudToastState {
|
|
55
|
+
message: string;
|
|
56
|
+
variant?: string;
|
|
57
|
+
/** IGameHUD anchor; absent on recordings that predate anchor capture. */
|
|
58
|
+
anchor?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface HudReplayState {
|
|
61
|
+
version: 1;
|
|
62
|
+
hudVisible: boolean;
|
|
63
|
+
/** null until the recording touches health at all. */
|
|
64
|
+
health: HudHealthState | null;
|
|
65
|
+
/** Verbatim from the `theme` op; null when the recording carried none. */
|
|
66
|
+
theme: unknown;
|
|
67
|
+
/** Creation order (= stacking order within an anchor); removed ones dropped. */
|
|
68
|
+
elements: HudElementState[];
|
|
69
|
+
toasts: HudToastState[];
|
|
70
|
+
/**
|
|
71
|
+
* Game-authored head stylesheets, first-logged order, last write per id.
|
|
72
|
+
* The harness injects these before drawing — they are what turn
|
|
73
|
+
* custom-element class names into pixels.
|
|
74
|
+
*
|
|
75
|
+
* Shared between snapshots while unchanged, so treat it as frozen: never
|
|
76
|
+
* mutate the array or its entries in place.
|
|
77
|
+
*/
|
|
78
|
+
stylesheets: ReadonlyArray<HudStylesheetState>;
|
|
79
|
+
}
|
|
80
|
+
export interface HudStylesheetState {
|
|
81
|
+
id: string;
|
|
82
|
+
css: string;
|
|
83
|
+
}
|
|
84
|
+
export interface RasterGeometry {
|
|
85
|
+
/** CSS-pixel viewport for the harness browser context. */
|
|
86
|
+
viewportWidth: number;
|
|
87
|
+
viewportHeight: number;
|
|
88
|
+
deviceScaleFactor: number;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Where to lay the HUD out for rasterization.
|
|
92
|
+
*
|
|
93
|
+
* During capture only the CANVAS is resized to the recording resolution — the
|
|
94
|
+
* DOM HUD keeps laying out against the player's window. Rastering at the
|
|
95
|
+
* recording size therefore renders every fixed-pixel HUD metric too small
|
|
96
|
+
* relative to the footage (and flips vw-clamps and media queries). With the
|
|
97
|
+
* recorded window known, raster at an ASPECT-CORRECTED viewport: the window's
|
|
98
|
+
* height (so element size relative to screen height matches what the player
|
|
99
|
+
* saw) at the recording's aspect (raw window dimensions would stretch glyphs
|
|
100
|
+
* non-uniformly under the final scale whenever the aspects differ). The
|
|
101
|
+
* deviceScaleFactor keeps the PNG at or above the recording resolution so
|
|
102
|
+
* ffmpeg only ever downscales.
|
|
103
|
+
*
|
|
104
|
+
* No recorded window → the recording resolution at 1x, bit-for-bit the old
|
|
105
|
+
* behavior.
|
|
106
|
+
*/
|
|
107
|
+
export declare function computeRasterGeometry(recordingWidth: number, recordingHeight: number, window?: {
|
|
108
|
+
innerWidth: number;
|
|
109
|
+
innerHeight: number;
|
|
110
|
+
devicePixelRatio: number;
|
|
111
|
+
}): RasterGeometry;
|
|
112
|
+
/** The engine records `frame` on a 60fps source clock. */
|
|
113
|
+
export declare const DEFAULT_HUD_FPS = 60;
|
|
114
|
+
/** IGameHUD.showToast's default duration. */
|
|
115
|
+
export declare const DEFAULT_TOAST_MS = 2500;
|
|
116
|
+
/**
|
|
117
|
+
* The complete HUD state at `frame`. Ops after `frame` are ignored; unknown
|
|
118
|
+
* op names are skipped rather than throwing.
|
|
119
|
+
*/
|
|
120
|
+
export declare function reduceHudState(ops: readonly HudOp[], frame: number, fps?: number): HudReplayState;
|
|
121
|
+
/** True when a state would rasterize to a fully transparent frame. */
|
|
122
|
+
export declare function isBlankHudState(state: HudReplayState): boolean;
|
|
123
|
+
export interface UiKeyframe {
|
|
124
|
+
/** Source frame whose state this is. */
|
|
125
|
+
sourceFrame: number;
|
|
126
|
+
/**
|
|
127
|
+
* `sourceFrame` as milliseconds on the SOURCE clock. The harness seeds
|
|
128
|
+
* animation phase from it, so motion stays locked to the footage through a
|
|
129
|
+
* speed ramp — output time would run it fast or slow against the frames.
|
|
130
|
+
*/
|
|
131
|
+
frameTimeMs: number;
|
|
132
|
+
clipIndex: number;
|
|
133
|
+
/** Output-timeline seconds this overlay becomes visible. */
|
|
134
|
+
outputStartSec: number;
|
|
135
|
+
/** Output-timeline seconds it stops being visible (next keyframe / clip end). */
|
|
136
|
+
outputEndSec: number;
|
|
137
|
+
state: HudReplayState;
|
|
138
|
+
}
|
|
139
|
+
export interface CollectUiKeyframesOptions {
|
|
140
|
+
/** Source-clock fps. Default 60. */
|
|
141
|
+
fps?: number;
|
|
142
|
+
/** Ceiling on keyframes per second of OUTPUT time. Default 30. 0 disables. */
|
|
143
|
+
maxPerSecond?: number;
|
|
144
|
+
/** Hard ceiling per clip. Defaults to what the clip's duration allows at `maxPerSecond`. */
|
|
145
|
+
maxPerClip?: number;
|
|
146
|
+
/** Per-clip opt-out; defaults to every clip. */
|
|
147
|
+
clipEnabled?: (clipIndex: number) => boolean;
|
|
148
|
+
/** Where truncation notices go. Defaults to stderr. */
|
|
149
|
+
log?: (line: string) => void;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 30, not 15: measured on a real cut, the selected footage held 227 distinct
|
|
153
|
+
* HUD states across 15.3 s of output (14.8/s average, four of seven clips over
|
|
154
|
+
* 15/s), so a 15/s ceiling was discarding roughly 45% of the HUD motion the
|
|
155
|
+
* game actually rendered.
|
|
156
|
+
*
|
|
157
|
+
* ffmpeg used to be the reason for a low ceiling — one PNG input plus one
|
|
158
|
+
* serial overlay node per keyframe cost ~23 MB of RSS each and corrupted the
|
|
159
|
+
* render above ~400. The overlays now ride one densified image2 sequence whose
|
|
160
|
+
* cost is flat in keyframe count (measured 2.83 s / 0.94 GB at both 126 and
|
|
161
|
+
* 902 distinct rasters), so the remaining cost is ~25 ms of Playwright per
|
|
162
|
+
* keyframe plus its PNG on disk. 0 disables the limit entirely.
|
|
163
|
+
*/
|
|
164
|
+
export declare const DEFAULT_MAX_KEYFRAMES_PER_SECOND = 30;
|
|
165
|
+
/**
|
|
166
|
+
* Sole per-clip ceiling, a backstop for `--ui-keyframe-rate 0`: at any real
|
|
167
|
+
* rate the limiter below admits at most rate·duration+1 keyframes per clip on
|
|
168
|
+
* its own, so nothing else can bind. A fixed 240 used to freeze the overlay
|
|
169
|
+
* after 16 s of any clip busy enough to sustain the default rate — exactly the
|
|
170
|
+
* clip a trailer is cut from. 60 s of output at 60/s.
|
|
171
|
+
*/
|
|
172
|
+
export declare const MAX_KEYFRAMES_PER_CLIP_CEILING = 3600;
|
|
173
|
+
/**
|
|
174
|
+
* Walk every enabled clip's source range and emit a keyframe wherever the
|
|
175
|
+
* reduced HUD state changes.
|
|
176
|
+
*
|
|
177
|
+
* A clip's overlays stop at `exclusiveOutputEnd`, so during a crossfade the
|
|
178
|
+
* incoming clip's UI takes over at the fade start and exactly one overlay is
|
|
179
|
+
* ever active — two stacked HUDs would double-draw every element.
|
|
180
|
+
*/
|
|
181
|
+
export declare function collectUiKeyframes(ops: readonly HudOp[], clips: readonly ClipTiming[], opts?: CollectUiKeyframesOptions): UiKeyframe[];
|
|
182
|
+
//# sourceMappingURL=hud-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hud-state.d.ts","sourceRoot":"","sources":["../../src/trailer/hud-state.ts"],"names":[],"mappings":"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"}
|