@dojo-video-engine/player 0.0.0-stage → 1.1.0
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/LICENSE +21 -0
- package/README.md +91 -2
- package/dist/audio.d.ts +101 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +329 -0
- package/dist/audio.js.map +1 -0
- package/dist/index.d.ts +202 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +587 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 dojo-video-engine contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,92 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dojo-video-engine/player
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Real-time playback for dojo-video-engine scenes: a deterministic frame clock plus a canvas render loop with play/pause/seek/loop.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createPlayer } from "@dojo-video-engine/player";
|
|
7
|
+
|
|
8
|
+
const context = canvas.getContext("2d")!;
|
|
9
|
+
const player = await createPlayer({ context, scene, loop: true });
|
|
10
|
+
|
|
11
|
+
player.play();
|
|
12
|
+
player.on("frame", (frame) => (slider.value = String(frame)));
|
|
13
|
+
player.pause();
|
|
14
|
+
await player.seek(45);
|
|
15
|
+
await player.disposeAsync();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
When the Scene is already prepared by the Canvas2D runtime, borrow that exact
|
|
19
|
+
revision instead of resolving a second media graph:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createPlayerFromRuntime } from "@dojo-video-engine/player";
|
|
23
|
+
|
|
24
|
+
const player = await createPlayerFromRuntime(runtime, { context, loop: true });
|
|
25
|
+
// The runtime keeps this revision's media alive until player disposal.
|
|
26
|
+
await player.disposeAsync();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Runtime updates, runtime disposal, and the optional caller `signal` dispose the
|
|
30
|
+
borrowed player before releasing its scope. A runtime-backed player therefore
|
|
31
|
+
cannot outlive or retain stale decoded assets from the prepared revision.
|
|
32
|
+
|
|
33
|
+
For extension scenes, pass the host registry as `extensions`; pass
|
|
34
|
+
`worker: true` when the player runs in a worker and definitions must declare
|
|
35
|
+
worker compatibility.
|
|
36
|
+
|
|
37
|
+
## Design
|
|
38
|
+
|
|
39
|
+
- **Wall-clock time exists only in the clock.** `FrameClock` maps `performance.now()` to integer frames anchored at the last play/seek; rendering stays the engine's pure `(scene, frame)` function. Playback at 30 fps shows the same pixels as export at 30 fps.
|
|
40
|
+
- **Clock ticks skip stale work; explicit seeks are serialized.** The loop awaits
|
|
41
|
+
`prepareFrame()` and re-reads the clock after a slow decode. Concurrent
|
|
42
|
+
`seek()` calls each resolve only after their requested frame paints.
|
|
43
|
+
- **First paint does not wait for audio.** Preview audio mixing begins alongside
|
|
44
|
+
the initial render. If it finishes after playback starts, it joins at the
|
|
45
|
+
current scene position instead of delaying the visible player.
|
|
46
|
+
- **Injectable time and scheduler.** `now`, `requestFrame`, and `cancelFrame` are constructor options, so the full play/pause/seek/ended/loop behavior is unit-tested in Node with a fake driver — no timers, no flake.
|
|
47
|
+
- **Asset ownership is explicit.** Pass `assets` and you own their lifetime;
|
|
48
|
+
omit them and the player resolves reachable proxy sources by default and
|
|
49
|
+
releases them transactionally. `dispose()` remains the void compatibility
|
|
50
|
+
facade; `disposeAsync()` observes completion/errors. Budgets, `AssetPolicy`,
|
|
51
|
+
caches, and cancellation are forwarded to the resolver.
|
|
52
|
+
- **Extension ownership follows assets.** An internally resolved registry runs resolve/prepare/draw/dispose through the same player lifecycle; caller-provided `assets` remain caller-owned.
|
|
53
|
+
|
|
54
|
+
Events: `frame`, `play`, `pause`, `ended` (non-looping playback pauses on the
|
|
55
|
+
final frame). `currentFrame` is the last rendered frame. `onError()` and
|
|
56
|
+
`player.error` expose asynchronous render-loop failure without an unhandled
|
|
57
|
+
promise rejection.
|
|
58
|
+
|
|
59
|
+
## Audio preview
|
|
60
|
+
|
|
61
|
+
Scenes with audible nodes (audio nodes, or video nodes whose clips carry a soundtrack) play sound during preview automatically:
|
|
62
|
+
|
|
63
|
+
- Audio is mixed through the exact pure bus/pan/ducking functions export uses. The default backend mixes bounded chunks around the current position; seeking discards stale work and starts at the requested chunk.
|
|
64
|
+
- **Audio is the master clock.** Each tick compares the frame clock against the audio position; past one frame of divergence, the frame clock re-anchors to audio. A skipped video frame is invisible; skipped audio is not.
|
|
65
|
+
- **Best-effort by design.** No `AudioContext` (or a custom `audio: false`) means silent preview; the `AudioContext` is created lazily inside `play()`, which is normally a user gesture, satisfying autoplay policies. The exported file remains the audio source of truth.
|
|
66
|
+
- Bring your own implementation via the `audio` option (`AudioPreview` interface) — that's also how the deterministic tests drive it.
|
|
67
|
+
|
|
68
|
+
`ChunkedWebAudioPreview` is the default: it mixes only the bounded chunk around
|
|
69
|
+
the current play/seek position and chains future chunks. `WebAudioPreview`
|
|
70
|
+
remains available when an application explicitly wants a reusable whole-scene
|
|
71
|
+
buffer for short projects.
|
|
72
|
+
|
|
73
|
+
## API Stability
|
|
74
|
+
|
|
75
|
+
Stable for 1.x integrations:
|
|
76
|
+
|
|
77
|
+
- `createPlayer()`, `Player`, `PlayerOptions`, `PlayerEvent`, `FrameClock`, and event names `frame`, `play`, `pause`, and `ended`.
|
|
78
|
+
- Prepared-runtime compatibility: `createPlayerFromRuntime()` and
|
|
79
|
+
`RuntimePlayerOptions`.
|
|
80
|
+
- `play()`, `pause()`, `seek(frame)`, `dispose()`, `disposeAsync()`,
|
|
81
|
+
`currentFrame`, `on(event, callback)`, and `onError(callback)`.
|
|
82
|
+
- Audio extension point: `AudioPreview`, `ChunkedWebAudioPreview`, and `WebAudioPreview`.
|
|
83
|
+
|
|
84
|
+
Evolving additively in 1.x:
|
|
85
|
+
|
|
86
|
+
- Scheduler/time injection options are stable enough for tests, but may gain a narrower helper API as downstream host integrations converge.
|
|
87
|
+
- Chunk scheduling may gain lookahead/cross-chunk overlap tuning while keeping `AudioPreview` as the integration boundary.
|
|
88
|
+
|
|
89
|
+
Internal/not public:
|
|
90
|
+
|
|
91
|
+
- Render-loop bookkeeping and audio drift correction internals are implementation details. Prefer the `Player` methods/events over reaching into private state.
|
|
92
|
+
- Files outside the package root export are implementation details.
|
package/dist/audio.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ResolvedAssets } from "@dojo-video-engine/renderer-canvas2d";
|
|
2
|
+
import type { Scene } from "@dojo-video-engine/schema";
|
|
3
|
+
/**
|
|
4
|
+
* Best-effort audio for preview playback. The export remains the audio
|
|
5
|
+
* source of truth; implementations of this interface only need to play
|
|
6
|
+
* scene-time-aligned sound and report their clock.
|
|
7
|
+
*/
|
|
8
|
+
export interface AudioPreview {
|
|
9
|
+
/** Prepares the scene's audio. Resolves false when there is nothing audible. */
|
|
10
|
+
load(scene: Scene, assets: ResolvedAssets | undefined, signal?: AbortSignal): Promise<boolean>;
|
|
11
|
+
/** Starts playback at the given scene time in seconds. */
|
|
12
|
+
start(atSeconds: number): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
/**
|
|
15
|
+
* The audio clock's current scene-time position in seconds, or null when
|
|
16
|
+
* not playing. The player treats this as the steadier reference and
|
|
17
|
+
* re-anchors its frame clock to it on drift.
|
|
18
|
+
*/
|
|
19
|
+
position(): number | null;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function previewAudioChunkRange(scene: Pick<Scene, "duration" | "fps">, atSeconds: number, chunkSeconds?: number): {
|
|
23
|
+
chunkStart: number;
|
|
24
|
+
startFrame: number;
|
|
25
|
+
endFrame: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Web Audio implementation: the scene's audible nodes are mixed once with
|
|
29
|
+
* the exact pure mix functions the export uses (`mixSceneAudio`), cached as
|
|
30
|
+
* one AudioBuffer, and played through a single AudioBufferSourceNode.
|
|
31
|
+
* Seeking restarts the source at an offset — no re-mixing.
|
|
32
|
+
*/
|
|
33
|
+
export declare class WebAudioPreview implements AudioPreview {
|
|
34
|
+
private readonly createContext;
|
|
35
|
+
private context;
|
|
36
|
+
private buffer;
|
|
37
|
+
private adoptedBuffer;
|
|
38
|
+
private source;
|
|
39
|
+
private startedAtContextTime;
|
|
40
|
+
private startOffset;
|
|
41
|
+
constructor(createContext?: () => AudioContext);
|
|
42
|
+
/** True when this environment can play audio at all. */
|
|
43
|
+
static supported(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Reuses a mix produced by an earlier preview (read from `mixedBuffer`),
|
|
46
|
+
* so load() skips decoding and mixing entirely. Callers own cache
|
|
47
|
+
* invalidation: adopt only when the scene's audible content is unchanged.
|
|
48
|
+
*/
|
|
49
|
+
adoptMixedBuffer(buffer: AudioBuffer | null): void;
|
|
50
|
+
/**
|
|
51
|
+
* The mixed scene audio after load() (null when the scene is silent).
|
|
52
|
+
* AudioBuffers are context-independent, so callers may cache this across
|
|
53
|
+
* players and hand it to a new preview via adoptMixedBuffer().
|
|
54
|
+
*/
|
|
55
|
+
get mixedBuffer(): AudioBuffer | null;
|
|
56
|
+
load(scene: Scene, assets: ResolvedAssets | undefined, signal?: AbortSignal): Promise<boolean>;
|
|
57
|
+
start(atSeconds: number): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
position(): number | null;
|
|
60
|
+
dispose(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Bounded-memory preview backend. One playing chunk and one prefetched chunk
|
|
64
|
+
* share an AudioContext timeline, so mixing never waits for the previous
|
|
65
|
+
* source's ended event. Seeking discards both chunks and their in-flight mix.
|
|
66
|
+
*/
|
|
67
|
+
export declare class ChunkedWebAudioPreview implements AudioPreview {
|
|
68
|
+
private readonly chunkSeconds;
|
|
69
|
+
private readonly createContext;
|
|
70
|
+
private context;
|
|
71
|
+
private scene;
|
|
72
|
+
private assets;
|
|
73
|
+
private chunks;
|
|
74
|
+
private timer;
|
|
75
|
+
private mixController;
|
|
76
|
+
private unlinkLoadSignal;
|
|
77
|
+
private loadSignal;
|
|
78
|
+
private lastMixError;
|
|
79
|
+
private generation;
|
|
80
|
+
private fillingGeneration;
|
|
81
|
+
private playing;
|
|
82
|
+
private clockStarted;
|
|
83
|
+
private startedAtContextTime;
|
|
84
|
+
private startOffset;
|
|
85
|
+
private nextFrame;
|
|
86
|
+
constructor(chunkSeconds?: number, createContext?: () => AudioContext);
|
|
87
|
+
static supported(): boolean;
|
|
88
|
+
get error(): Error | null;
|
|
89
|
+
load(scene: Scene, assets: ResolvedAssets | undefined, signal?: AbortSignal): Promise<boolean>;
|
|
90
|
+
start(atSeconds: number): void;
|
|
91
|
+
stop(): void;
|
|
92
|
+
position(): number | null;
|
|
93
|
+
dispose(): void;
|
|
94
|
+
private sceneTime;
|
|
95
|
+
private current;
|
|
96
|
+
private retireElapsedChunks;
|
|
97
|
+
private fillLookahead;
|
|
98
|
+
private mixAhead;
|
|
99
|
+
private scheduleBoundary;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=audio.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
|
package/dist/audio.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { collectAudioPlacements, mixSceneAudio, } from "@dojo-video-engine/export";
|
|
2
|
+
export function previewAudioChunkRange(scene, atSeconds, chunkSeconds = 30) {
|
|
3
|
+
const chunkStart = Math.floor(Math.max(0, atSeconds) / chunkSeconds) * chunkSeconds;
|
|
4
|
+
return {
|
|
5
|
+
chunkStart,
|
|
6
|
+
startFrame: Math.floor(chunkStart * scene.fps),
|
|
7
|
+
endFrame: Math.min(scene.duration - 1, Math.ceil((chunkStart + chunkSeconds) * scene.fps) - 1),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Web Audio implementation: the scene's audible nodes are mixed once with
|
|
12
|
+
* the exact pure mix functions the export uses (`mixSceneAudio`), cached as
|
|
13
|
+
* one AudioBuffer, and played through a single AudioBufferSourceNode.
|
|
14
|
+
* Seeking restarts the source at an offset — no re-mixing.
|
|
15
|
+
*/
|
|
16
|
+
export class WebAudioPreview {
|
|
17
|
+
createContext;
|
|
18
|
+
context;
|
|
19
|
+
buffer = null;
|
|
20
|
+
adoptedBuffer = false;
|
|
21
|
+
source = null;
|
|
22
|
+
startedAtContextTime = 0;
|
|
23
|
+
startOffset = 0;
|
|
24
|
+
constructor(createContext = () => new AudioContext()) {
|
|
25
|
+
this.createContext = createContext;
|
|
26
|
+
}
|
|
27
|
+
/** True when this environment can play audio at all. */
|
|
28
|
+
static supported() {
|
|
29
|
+
return typeof AudioContext !== "undefined";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Reuses a mix produced by an earlier preview (read from `mixedBuffer`),
|
|
33
|
+
* so load() skips decoding and mixing entirely. Callers own cache
|
|
34
|
+
* invalidation: adopt only when the scene's audible content is unchanged.
|
|
35
|
+
*/
|
|
36
|
+
adoptMixedBuffer(buffer) {
|
|
37
|
+
this.buffer = buffer;
|
|
38
|
+
this.adoptedBuffer = true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The mixed scene audio after load() (null when the scene is silent).
|
|
42
|
+
* AudioBuffers are context-independent, so callers may cache this across
|
|
43
|
+
* players and hand it to a new preview via adoptMixedBuffer().
|
|
44
|
+
*/
|
|
45
|
+
get mixedBuffer() {
|
|
46
|
+
return this.buffer;
|
|
47
|
+
}
|
|
48
|
+
async load(scene, assets, signal) {
|
|
49
|
+
if (!this.adoptedBuffer) {
|
|
50
|
+
this.buffer = assets
|
|
51
|
+
? await mixSceneAudio(scene, assets, 0, scene.duration - 1, signal)
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
return this.buffer !== null;
|
|
55
|
+
}
|
|
56
|
+
start(atSeconds) {
|
|
57
|
+
if (!this.buffer) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Created lazily inside start(), which the player calls from play() —
|
|
61
|
+
// normally a user gesture, satisfying autoplay policies.
|
|
62
|
+
this.context ??= this.createContext();
|
|
63
|
+
void this.context.resume?.();
|
|
64
|
+
this.stop();
|
|
65
|
+
const offset = Math.max(0, Math.min(atSeconds, this.buffer.duration));
|
|
66
|
+
const source = this.context.createBufferSource();
|
|
67
|
+
source.buffer = this.buffer;
|
|
68
|
+
source.connect(this.context.destination);
|
|
69
|
+
source.start(0, offset);
|
|
70
|
+
this.source = source;
|
|
71
|
+
this.startedAtContextTime = this.context.currentTime;
|
|
72
|
+
this.startOffset = offset;
|
|
73
|
+
}
|
|
74
|
+
stop() {
|
|
75
|
+
if (this.source) {
|
|
76
|
+
try {
|
|
77
|
+
this.source.stop();
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Stopping a source that already ended throws in some engines.
|
|
81
|
+
}
|
|
82
|
+
this.source.disconnect();
|
|
83
|
+
this.source = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
position() {
|
|
87
|
+
if (!this.source || !this.context) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return (this.startOffset + (this.context.currentTime - this.startedAtContextTime));
|
|
91
|
+
}
|
|
92
|
+
dispose() {
|
|
93
|
+
this.stop();
|
|
94
|
+
void this.context?.close?.();
|
|
95
|
+
this.context = undefined;
|
|
96
|
+
this.buffer = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Bounded-memory preview backend. One playing chunk and one prefetched chunk
|
|
101
|
+
* share an AudioContext timeline, so mixing never waits for the previous
|
|
102
|
+
* source's ended event. Seeking discards both chunks and their in-flight mix.
|
|
103
|
+
*/
|
|
104
|
+
export class ChunkedWebAudioPreview {
|
|
105
|
+
chunkSeconds;
|
|
106
|
+
createContext;
|
|
107
|
+
context;
|
|
108
|
+
scene = null;
|
|
109
|
+
assets;
|
|
110
|
+
chunks = [];
|
|
111
|
+
timer = null;
|
|
112
|
+
mixController = null;
|
|
113
|
+
unlinkLoadSignal = null;
|
|
114
|
+
loadSignal;
|
|
115
|
+
lastMixError = null;
|
|
116
|
+
generation = 0;
|
|
117
|
+
fillingGeneration = null;
|
|
118
|
+
playing = false;
|
|
119
|
+
clockStarted = false;
|
|
120
|
+
startedAtContextTime = 0;
|
|
121
|
+
startOffset = 0;
|
|
122
|
+
nextFrame = 0;
|
|
123
|
+
constructor(chunkSeconds = 30, createContext = () => new AudioContext()) {
|
|
124
|
+
this.chunkSeconds = chunkSeconds;
|
|
125
|
+
this.createContext = createContext;
|
|
126
|
+
if (!Number.isFinite(chunkSeconds) || chunkSeconds <= 0) {
|
|
127
|
+
throw new RangeError("Audio preview chunk duration must be positive.");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
static supported() {
|
|
131
|
+
return typeof AudioContext !== "undefined";
|
|
132
|
+
}
|
|
133
|
+
get error() {
|
|
134
|
+
return this.lastMixError;
|
|
135
|
+
}
|
|
136
|
+
async load(scene, assets, signal) {
|
|
137
|
+
if (signal?.aborted)
|
|
138
|
+
throw signal.reason;
|
|
139
|
+
this.stop();
|
|
140
|
+
this.unlinkLoadSignal?.();
|
|
141
|
+
this.loadSignal = signal;
|
|
142
|
+
const abort = () => this.stop();
|
|
143
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
144
|
+
this.unlinkLoadSignal = signal
|
|
145
|
+
? () => signal.removeEventListener("abort", abort)
|
|
146
|
+
: null;
|
|
147
|
+
this.scene = scene;
|
|
148
|
+
this.assets = assets;
|
|
149
|
+
return Boolean(assets && collectAudioPlacements(scene).length > 0);
|
|
150
|
+
}
|
|
151
|
+
start(atSeconds) {
|
|
152
|
+
if (!this.scene || !this.assets || this.loadSignal?.aborted)
|
|
153
|
+
return;
|
|
154
|
+
this.context ??= this.createContext();
|
|
155
|
+
void this.context.resume?.();
|
|
156
|
+
this.stop();
|
|
157
|
+
this.lastMixError = null;
|
|
158
|
+
this.startOffset = Math.max(0, Math.min(atSeconds, this.scene.duration / this.scene.fps));
|
|
159
|
+
if (this.startOffset >= this.scene.duration / this.scene.fps)
|
|
160
|
+
return;
|
|
161
|
+
this.playing = true;
|
|
162
|
+
this.clockStarted = false;
|
|
163
|
+
this.nextFrame = 0;
|
|
164
|
+
this.mixController = new AbortController();
|
|
165
|
+
this.fillLookahead(this.generation, this.mixController.signal);
|
|
166
|
+
}
|
|
167
|
+
stop() {
|
|
168
|
+
this.generation += 1;
|
|
169
|
+
this.playing = false;
|
|
170
|
+
this.clockStarted = false;
|
|
171
|
+
this.mixController?.abort(typeof DOMException === "function"
|
|
172
|
+
? new DOMException("Audio preview work was superseded.", "AbortError")
|
|
173
|
+
: new Error("Audio preview work was superseded."));
|
|
174
|
+
this.mixController = null;
|
|
175
|
+
if (this.timer !== null)
|
|
176
|
+
clearTimeout(this.timer);
|
|
177
|
+
this.timer = null;
|
|
178
|
+
for (const chunk of this.chunks) {
|
|
179
|
+
if (!chunk.source)
|
|
180
|
+
continue;
|
|
181
|
+
chunk.source.onended = null;
|
|
182
|
+
try {
|
|
183
|
+
chunk.source.stop();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Already ended.
|
|
187
|
+
}
|
|
188
|
+
chunk.source.disconnect();
|
|
189
|
+
}
|
|
190
|
+
this.chunks = [];
|
|
191
|
+
}
|
|
192
|
+
position() {
|
|
193
|
+
if (!this.playing || !this.context || !this.scene)
|
|
194
|
+
return null;
|
|
195
|
+
// Hold the requested position while the first mix is being prepared. The
|
|
196
|
+
// player follows this clock instead of skipping the beginning of the audio.
|
|
197
|
+
const position = this.clockStarted ? this.sceneTime() : this.startOffset;
|
|
198
|
+
return position < this.scene.duration / this.scene.fps ? position : null;
|
|
199
|
+
}
|
|
200
|
+
dispose() {
|
|
201
|
+
this.stop();
|
|
202
|
+
this.unlinkLoadSignal?.();
|
|
203
|
+
this.unlinkLoadSignal = null;
|
|
204
|
+
this.loadSignal = undefined;
|
|
205
|
+
void this.context?.close?.();
|
|
206
|
+
this.context = undefined;
|
|
207
|
+
this.scene = null;
|
|
208
|
+
this.assets = undefined;
|
|
209
|
+
}
|
|
210
|
+
sceneTime() {
|
|
211
|
+
return (this.startOffset +
|
|
212
|
+
Math.max(0, this.context.currentTime - this.startedAtContextTime));
|
|
213
|
+
}
|
|
214
|
+
current(generation, signal) {
|
|
215
|
+
return this.playing && generation === this.generation && !signal.aborted;
|
|
216
|
+
}
|
|
217
|
+
retireElapsedChunks() {
|
|
218
|
+
const now = this.context.currentTime;
|
|
219
|
+
while (this.chunks.length && this.chunks[0].endContextTime <= now) {
|
|
220
|
+
const chunk = this.chunks.shift();
|
|
221
|
+
if (chunk.source) {
|
|
222
|
+
chunk.source.onended = null;
|
|
223
|
+
chunk.source.disconnect();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
fillLookahead(generation, signal) {
|
|
228
|
+
if (!this.current(generation, signal) ||
|
|
229
|
+
this.fillingGeneration === generation)
|
|
230
|
+
return;
|
|
231
|
+
this.fillingGeneration = generation;
|
|
232
|
+
void this.mixAhead(generation, signal)
|
|
233
|
+
.catch((error) => {
|
|
234
|
+
if (!this.current(generation, signal))
|
|
235
|
+
return;
|
|
236
|
+
this.lastMixError =
|
|
237
|
+
error instanceof Error ? error : new Error(String(error));
|
|
238
|
+
this.stop();
|
|
239
|
+
})
|
|
240
|
+
.finally(() => {
|
|
241
|
+
if (this.fillingGeneration === generation)
|
|
242
|
+
this.fillingGeneration = null;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
async mixAhead(generation, signal) {
|
|
246
|
+
const scene = this.scene;
|
|
247
|
+
const assets = this.assets;
|
|
248
|
+
const context = this.context;
|
|
249
|
+
while (this.current(generation, signal)) {
|
|
250
|
+
this.retireElapsedChunks();
|
|
251
|
+
if (this.clockStarted && this.sceneTime() >= scene.duration / scene.fps) {
|
|
252
|
+
this.stop();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (this.chunks.length >= 2 || this.nextFrame >= scene.duration)
|
|
256
|
+
return;
|
|
257
|
+
const sceneTime = this.clockStarted
|
|
258
|
+
? Math.max(this.nextFrame / scene.fps, this.sceneTime())
|
|
259
|
+
: this.startOffset;
|
|
260
|
+
if (sceneTime >= scene.duration / scene.fps) {
|
|
261
|
+
this.stop();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const range = previewAudioChunkRange(scene, sceneTime, this.chunkSeconds);
|
|
265
|
+
// Fractional frame rates can straddle a nominal chunk boundary. Do not
|
|
266
|
+
// decode or schedule an overlapping frame at the start of the next chunk.
|
|
267
|
+
const startFrame = Math.max(this.nextFrame, range.startFrame);
|
|
268
|
+
const endFrame = Math.max(startFrame, range.endFrame);
|
|
269
|
+
const buffer = await mixSceneAudio(scene, assets, startFrame, endFrame, signal);
|
|
270
|
+
if (!this.current(generation, signal))
|
|
271
|
+
return;
|
|
272
|
+
if (!this.clockStarted) {
|
|
273
|
+
this.startedAtContextTime = context.currentTime;
|
|
274
|
+
this.clockStarted = true;
|
|
275
|
+
}
|
|
276
|
+
this.nextFrame = endFrame + 1;
|
|
277
|
+
const endContextTime = this.startedAtContextTime +
|
|
278
|
+
this.nextFrame / scene.fps -
|
|
279
|
+
this.startOffset;
|
|
280
|
+
// A late mix follows the established clock. Skip an entirely expired
|
|
281
|
+
// chunk, or start at its current offset; never shift subsequent audio.
|
|
282
|
+
if (endContextTime <= context.currentTime)
|
|
283
|
+
continue;
|
|
284
|
+
let source = null;
|
|
285
|
+
if (buffer) {
|
|
286
|
+
source = context.createBufferSource();
|
|
287
|
+
source.buffer = buffer;
|
|
288
|
+
source.connect(context.destination);
|
|
289
|
+
const when = Math.max(context.currentTime, this.startedAtContextTime + startFrame / scene.fps - this.startOffset);
|
|
290
|
+
const offset = this.startOffset +
|
|
291
|
+
when -
|
|
292
|
+
this.startedAtContextTime -
|
|
293
|
+
startFrame / scene.fps;
|
|
294
|
+
try {
|
|
295
|
+
source.start(when, Math.max(0, offset), endContextTime - when);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
source.disconnect();
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.chunks.push({ endContextTime, source });
|
|
303
|
+
this.scheduleBoundary(generation, signal);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
scheduleBoundary(generation, signal) {
|
|
307
|
+
if (this.timer !== null)
|
|
308
|
+
clearTimeout(this.timer);
|
|
309
|
+
this.timer = null;
|
|
310
|
+
const boundary = this.chunks[0]?.endContextTime;
|
|
311
|
+
if (!this.current(generation, signal) || boundary === undefined)
|
|
312
|
+
return;
|
|
313
|
+
this.timer = setTimeout(() => {
|
|
314
|
+
this.timer = null;
|
|
315
|
+
if (!this.current(generation, signal))
|
|
316
|
+
return;
|
|
317
|
+
this.retireElapsedChunks();
|
|
318
|
+
if (this.sceneTime() >= this.scene.duration / this.scene.fps) {
|
|
319
|
+
this.stop();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.fillLookahead(generation, signal);
|
|
323
|
+
// AudioContext time can pause while a wall-clock timer keeps running.
|
|
324
|
+
// Recheck it before releasing a chunk or scheduling additional work.
|
|
325
|
+
this.scheduleBoundary(generation, signal);
|
|
326
|
+
}, Math.max(1, (boundary - this.context.currentTime) * 1000));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
|