@motionscript/browser 0.0.0-stage → 0.1.0-alpha.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 +192 -0
- package/LICENSE +201 -0
- package/NOTICE +49 -0
- package/README.md +140 -3
- package/dist/asset-bytes.d.ts +59 -0
- package/dist/asset-bytes.d.ts.map +1 -0
- package/dist/asset-bytes.js +78 -0
- package/dist/asset-bytes.js.map +1 -0
- package/dist/audio/bus-graph.d.ts +52 -0
- package/dist/audio/bus-graph.d.ts.map +1 -0
- package/dist/audio/bus-graph.js +108 -0
- package/dist/audio/bus-graph.js.map +1 -0
- package/dist/audio/effect-graph.d.ts +62 -0
- package/dist/audio/effect-graph.d.ts.map +1 -0
- package/dist/audio/effect-graph.js +234 -0
- package/dist/audio/effect-graph.js.map +1 -0
- package/dist/audio/index.d.ts +5 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +11 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/audio/loop-region.d.ts +31 -0
- package/dist/audio/loop-region.d.ts.map +1 -0
- package/dist/audio/loop-region.js +43 -0
- package/dist/audio/loop-region.js.map +1 -0
- package/dist/audio/mixer.d.ts +59 -0
- package/dist/audio/mixer.d.ts.map +1 -0
- package/dist/audio/mixer.js +192 -0
- package/dist/audio/mixer.js.map +1 -0
- package/dist/audio/player.d.ts +55 -0
- package/dist/audio/player.d.ts.map +1 -0
- package/dist/audio/player.js +217 -0
- package/dist/audio/player.js.map +1 -0
- package/dist/browser/THIRD_PARTY_LICENSES.txt +784 -0
- package/dist/browser/audio.js +2 -0
- package/dist/browser/audio.js.map +7 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js +2 -0
- package/dist/browser/chunks/chunk-F3AZKPHU.js.map +7 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js +2 -0
- package/dist/browser/chunks/chunk-KLXMRKTL.js.map +7 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js +163 -0
- package/dist/browser/chunks/chunk-L5UFQACH.js.map +7 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js +14 -0
- package/dist/browser/chunks/mediabunny-mp3-encoder-ZCIHVVTJ.js.map +7 -0
- package/dist/browser/esbuild.wasm +0 -0
- package/dist/browser/index.js +734 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/manifest.json +32 -0
- package/dist/browser/ms_renderer_bg.wasm +0 -0
- package/dist/concat.d.ts +121 -0
- package/dist/concat.d.ts.map +1 -0
- package/dist/concat.js +219 -0
- package/dist/concat.js.map +1 -0
- package/dist/deliver.d.ts +10 -0
- package/dist/deliver.d.ts.map +1 -0
- package/dist/deliver.js +22 -0
- package/dist/deliver.js.map +1 -0
- package/dist/dynamic-nodes.d.ts +9 -0
- package/dist/dynamic-nodes.d.ts.map +1 -0
- package/dist/dynamic-nodes.js +53 -0
- package/dist/dynamic-nodes.js.map +1 -0
- package/dist/engine.d.ts +87 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +193 -0
- package/dist/engine.js.map +1 -0
- package/dist/export-audio.d.ts +36 -0
- package/dist/export-audio.d.ts.map +1 -0
- package/dist/export-audio.js +56 -0
- package/dist/export-audio.js.map +1 -0
- package/dist/exporter.d.ts +282 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/exporter.js +308 -0
- package/dist/exporter.js.map +1 -0
- package/dist/formats.d.ts +36 -0
- package/dist/formats.d.ts.map +1 -0
- package/dist/formats.js +83 -0
- package/dist/formats.js.map +1 -0
- package/dist/frame-loop.d.ts +190 -0
- package/dist/frame-loop.d.ts.map +1 -0
- package/dist/frame-loop.js +304 -0
- package/dist/frame-loop.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/mix-timeline-audio.d.ts +42 -0
- package/dist/mix-timeline-audio.d.ts.map +1 -0
- package/dist/mix-timeline-audio.js +60 -0
- package/dist/mix-timeline-audio.js.map +1 -0
- package/dist/parallel/export-parallel.d.ts +32 -0
- package/dist/parallel/export-parallel.d.ts.map +1 -0
- package/dist/parallel/export-parallel.js +114 -0
- package/dist/parallel/export-parallel.js.map +1 -0
- package/dist/parallel/governor.d.ts +57 -0
- package/dist/parallel/governor.d.ts.map +1 -0
- package/dist/parallel/governor.js +79 -0
- package/dist/parallel/governor.js.map +1 -0
- package/dist/parallel/lane.d.ts +45 -0
- package/dist/parallel/lane.d.ts.map +1 -0
- package/dist/parallel/lane.js +2 -0
- package/dist/parallel/lane.js.map +1 -0
- package/dist/parallel/lanes.d.ts +20 -0
- package/dist/parallel/lanes.d.ts.map +1 -0
- package/dist/parallel/lanes.js +152 -0
- package/dist/parallel/lanes.js.map +1 -0
- package/dist/parallel/plan.d.ts +23 -0
- package/dist/parallel/plan.d.ts.map +1 -0
- package/dist/parallel/plan.js +30 -0
- package/dist/parallel/plan.js.map +1 -0
- package/dist/parallel/run-lanes.d.ts +38 -0
- package/dist/parallel/run-lanes.d.ts.map +1 -0
- package/dist/parallel/run-lanes.js +164 -0
- package/dist/parallel/run-lanes.js.map +1 -0
- package/dist/parallel/session.d.ts +9 -0
- package/dist/parallel/session.d.ts.map +1 -0
- package/dist/parallel/session.js +79 -0
- package/dist/parallel/session.js.map +1 -0
- package/dist/renderer.d.ts +22 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +23 -0
- package/dist/renderer.js.map +1 -0
- package/dist/screenshot.d.ts +45 -0
- package/dist/screenshot.d.ts.map +1 -0
- package/dist/screenshot.js +31 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/still.d.ts +159 -0
- package/dist/still.d.ts.map +1 -0
- package/dist/still.js +222 -0
- package/dist/still.js.map +1 -0
- package/dist/ticker.d.ts +74 -0
- package/dist/ticker.d.ts.map +1 -0
- package/dist/ticker.js +215 -0
- package/dist/ticker.js.map +1 -0
- package/package.json +75 -3
package/dist/ticker.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Ticker } from "@motionscript/core";
|
|
2
|
+
/**
|
|
3
|
+
* Ticker backed by a Web Audio AudioContext. Clip time follows
|
|
4
|
+
* `audioContext.currentTime` (within a few ms, see `startLoop`), so any
|
|
5
|
+
* AudioBufferSourceNodes scheduled on the same context play in sync.
|
|
6
|
+
*
|
|
7
|
+
* When paused we suspend the context (audio stops, currentTime freezes);
|
|
8
|
+
* when playing we resume and a rAF loop publishes ticks at the target fps.
|
|
9
|
+
*
|
|
10
|
+
* A single silent buffer covering the full timeline is started on play(),
|
|
11
|
+
* which keeps the context active and gives the browser a stable audio
|
|
12
|
+
* graph to schedule against even on scenes that contain no audio nodes.
|
|
13
|
+
*/
|
|
14
|
+
export class WebTicker extends Ticker {
|
|
15
|
+
contextOf;
|
|
16
|
+
ownsContext;
|
|
17
|
+
_context = null;
|
|
18
|
+
fps;
|
|
19
|
+
playStartCtxTime = 0;
|
|
20
|
+
seekOffset = 0;
|
|
21
|
+
speed = 1;
|
|
22
|
+
reverse = false;
|
|
23
|
+
rafId = null;
|
|
24
|
+
/** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */
|
|
25
|
+
seekEpoch = 0;
|
|
26
|
+
silentSource = null;
|
|
27
|
+
/**
|
|
28
|
+
* `context` may be a getter, called on the first `play`: a session hands
|
|
29
|
+
* over its audio sink's, which is created on first use, so a session that
|
|
30
|
+
* never plays never opens the audio device.
|
|
31
|
+
*/
|
|
32
|
+
constructor(opts) {
|
|
33
|
+
super();
|
|
34
|
+
const { context } = opts;
|
|
35
|
+
this.contextOf = typeof context === "function" ? context : () => context ?? new AudioContext();
|
|
36
|
+
this.ownsContext = !context;
|
|
37
|
+
this.fps = opts.fps;
|
|
38
|
+
}
|
|
39
|
+
getContext() {
|
|
40
|
+
this._context ??= this.contextOf();
|
|
41
|
+
return this._context;
|
|
42
|
+
}
|
|
43
|
+
get context() {
|
|
44
|
+
return this.getContext();
|
|
45
|
+
}
|
|
46
|
+
seek(t) {
|
|
47
|
+
const clamped = Math.max(0, Math.min(t, this.duration));
|
|
48
|
+
this.seekOffset = clamped;
|
|
49
|
+
// The clock is read only while playing; `play` anchors it afresh.
|
|
50
|
+
if (this.isPlaying)
|
|
51
|
+
this.playStartCtxTime = this.context.currentTime;
|
|
52
|
+
this.seekEpoch++;
|
|
53
|
+
this.setCurrentTime(clamped);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Anchors elapsed-time tracking to the current `context.currentTime`, starts the silent ticker
|
|
57
|
+
* + rAF tick loop and asks a suspended context to resume.
|
|
58
|
+
*
|
|
59
|
+
* Synchronous, as {@link Ticker.play} is: a `pause` or `seek` in the same turn must find a
|
|
60
|
+
* running clock to stop. A suspended context's `currentTime` is frozen, so the clock holds at
|
|
61
|
+
* the anchor until the resume lands — outside a user gesture that is never.
|
|
62
|
+
*/
|
|
63
|
+
play(speed = 1, reverse = false) {
|
|
64
|
+
this.speed = speed;
|
|
65
|
+
this.reverse = reverse;
|
|
66
|
+
this.seekOffset = this.currentTime;
|
|
67
|
+
if (this.context.state === "suspended") {
|
|
68
|
+
this.context.resume().then(() => {
|
|
69
|
+
// A pause that arrived while the resume was pending could not suspend it.
|
|
70
|
+
if (!this.isPlaying && this.context.state === "running")
|
|
71
|
+
void this.context.suspend();
|
|
72
|
+
}, (error) => {
|
|
73
|
+
console.error("[WebTicker] The audio context could not resume:", error);
|
|
74
|
+
this.pause();
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
this.playStartCtxTime = this.context.currentTime;
|
|
78
|
+
this.startSilentTicker();
|
|
79
|
+
super.play(speed, reverse);
|
|
80
|
+
this.startLoop();
|
|
81
|
+
}
|
|
82
|
+
/** Stops the tick loop and silent ticker, then suspends the context — freezing `currentTime` so elapsed-time math stays correct across the pause. */
|
|
83
|
+
pause() {
|
|
84
|
+
this.seekOffset = this.currentTime;
|
|
85
|
+
this.stopLoop();
|
|
86
|
+
this.stopSilentTicker();
|
|
87
|
+
if (this._context?.state === "running") {
|
|
88
|
+
this._context.suspend();
|
|
89
|
+
}
|
|
90
|
+
super.pause();
|
|
91
|
+
}
|
|
92
|
+
/** (Re)starts the silent buffer source covering the remaining timeline — keeps the AudioContext active and gives a stable graph for other AudioBufferSourceNodes to schedule against, per the class-level note. */
|
|
93
|
+
startSilentTicker() {
|
|
94
|
+
this.stopSilentTicker();
|
|
95
|
+
const remaining = Math.max(0.001, this.duration - this.seekOffset);
|
|
96
|
+
const sampleRate = this.context.sampleRate;
|
|
97
|
+
const buffer = this.context.createBuffer(1, Math.ceil(remaining * sampleRate), sampleRate);
|
|
98
|
+
const source = this.context.createBufferSource();
|
|
99
|
+
source.buffer = buffer;
|
|
100
|
+
source.connect(this.context.destination);
|
|
101
|
+
source.start();
|
|
102
|
+
this.silentSource = source;
|
|
103
|
+
}
|
|
104
|
+
stopSilentTicker() {
|
|
105
|
+
if (this.silentSource) {
|
|
106
|
+
try {
|
|
107
|
+
this.silentSource.onended = null;
|
|
108
|
+
this.silentSource.stop();
|
|
109
|
+
this.silentSource.disconnect();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// already stopped
|
|
113
|
+
}
|
|
114
|
+
this.silentSource = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Where the playhead is now, from elapsed `context.currentTime` (so it stays sample-accurate), and whether that is the end of the run. */
|
|
118
|
+
position() {
|
|
119
|
+
const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;
|
|
120
|
+
const next = this.reverse
|
|
121
|
+
? this.seekOffset - elapsed
|
|
122
|
+
: this.seekOffset + elapsed;
|
|
123
|
+
const time = Math.max(0, Math.min(next, this.duration));
|
|
124
|
+
return { time, ended: this.reverse ? time <= 0 : time >= this.duration };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Drives ticks off `requestAnimationFrame`, firing `tick()` when the playhead reaches another
|
|
128
|
+
* project frame — and once more at the end of the run, whether or not it landed on a boundary.
|
|
129
|
+
*
|
|
130
|
+
* `context.currentTime` moves in whole render quanta handed over in bursts: at 48 kHz on a
|
|
131
|
+
* 60 Hz display it advances 8, 10.67, 18.67 or 21.33 ms between animation frames, and a
|
|
132
|
+
* playhead read straight off it skipped up to a third of the frames. So the playhead advances
|
|
133
|
+
* by each animation frame's own interval, pulled 5% of the way to the audio clock every frame
|
|
134
|
+
* and snapped to it when they disagree by more than 50 ms (a stall, a seek). It stays within
|
|
135
|
+
* a few ms of the audio and moves one frame per frame.
|
|
136
|
+
*
|
|
137
|
+
* A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is
|
|
138
|
+
* re-read after the listeners and the tick callbacks have run: a run that was seeked out of
|
|
139
|
+
* its end carries on, and one that was not pauses itself.
|
|
140
|
+
*/
|
|
141
|
+
startLoop() {
|
|
142
|
+
if (this.rafId !== null)
|
|
143
|
+
return;
|
|
144
|
+
const frameOf = (t) => Math.floor(t * this.fps + 1e-6);
|
|
145
|
+
let lastFrame = frameOf(this.currentTime);
|
|
146
|
+
let steady = null;
|
|
147
|
+
const steadied = (now) => {
|
|
148
|
+
const audio = this.position();
|
|
149
|
+
let time = audio.time;
|
|
150
|
+
if (!audio.ended && steady && steady.epoch === this.seekEpoch) {
|
|
151
|
+
const step = ((now - steady.at) / 1000) * this.speed;
|
|
152
|
+
const predicted = this.reverse ? steady.time - step : steady.time + step;
|
|
153
|
+
const drift = audio.time - predicted;
|
|
154
|
+
const next = predicted + drift * 0.05;
|
|
155
|
+
if (Math.abs(drift) <= 0.05 && next > 0 && next < this.duration)
|
|
156
|
+
time = next;
|
|
157
|
+
}
|
|
158
|
+
steady = { time, at: now, epoch: this.seekEpoch };
|
|
159
|
+
return { time, ended: audio.ended };
|
|
160
|
+
};
|
|
161
|
+
const loop = async (now) => {
|
|
162
|
+
let epoch = this.seekEpoch;
|
|
163
|
+
const position = steadied(now);
|
|
164
|
+
let ended = position.ended;
|
|
165
|
+
// This loop runs at the display's refresh rate, which on a 120 or 144Hz panel is two
|
|
166
|
+
// or three times the rate anything is rendered at — so the clock advances every time
|
|
167
|
+
// round (the elapsed maths in `play`/`pause` depend on it being exact) while listeners
|
|
168
|
+
// hear about it once per frame. The end of the run always reports, or the final
|
|
169
|
+
// position would never reach a host. See `Ticker.setCurrentTime`.
|
|
170
|
+
const crossed = frameOf(position.time) !== lastFrame;
|
|
171
|
+
const report = crossed || ended;
|
|
172
|
+
this.setCurrentTime(position.time, report);
|
|
173
|
+
const reread = () => {
|
|
174
|
+
if (this.seekEpoch === epoch)
|
|
175
|
+
return;
|
|
176
|
+
epoch = this.seekEpoch;
|
|
177
|
+
lastFrame = frameOf(this.currentTime);
|
|
178
|
+
ended = this.position().ended;
|
|
179
|
+
};
|
|
180
|
+
reread();
|
|
181
|
+
if (report) {
|
|
182
|
+
lastFrame = frameOf(this.currentTime);
|
|
183
|
+
// Await so that a slow tick (asset load on a clip
|
|
184
|
+
// boundary) doesn't queue a second concurrent tick.
|
|
185
|
+
await this.tick();
|
|
186
|
+
reread();
|
|
187
|
+
}
|
|
188
|
+
// If pause() cleared the rafId while we were awaiting tick(),
|
|
189
|
+
// do not resume the loop.
|
|
190
|
+
if (this.rafId === null)
|
|
191
|
+
return;
|
|
192
|
+
if (ended) {
|
|
193
|
+
this.pause();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
this.rafId = requestAnimationFrame(loop);
|
|
197
|
+
};
|
|
198
|
+
this.rafId = requestAnimationFrame(loop);
|
|
199
|
+
}
|
|
200
|
+
stopLoop() {
|
|
201
|
+
if (this.rafId !== null) {
|
|
202
|
+
cancelAnimationFrame(this.rafId);
|
|
203
|
+
this.rafId = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
dispose() {
|
|
207
|
+
this.stopLoop();
|
|
208
|
+
this.stopSilentTicker();
|
|
209
|
+
if (this.ownsContext) {
|
|
210
|
+
void this._context?.close();
|
|
211
|
+
}
|
|
212
|
+
super.dispose();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=ticker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticker.js","sourceRoot":"","sources":["../src/ticker.ts"],"names":[],"mappings":"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","sourcesContent":["import { Ticker } from \"@motionscript/core\";\n\n/**\n * Ticker backed by a Web Audio AudioContext. Clip time follows\n * `audioContext.currentTime` (within a few ms, see `startLoop`), so any\n * AudioBufferSourceNodes scheduled on the same context play in sync.\n *\n * When paused we suspend the context (audio stops, currentTime freezes);\n * when playing we resume and a rAF loop publishes ticks at the target fps.\n *\n * A single silent buffer covering the full timeline is started on play(),\n * which keeps the context active and gives the browser a stable audio\n * graph to schedule against even on scenes that contain no audio nodes.\n */\nexport class WebTicker extends Ticker {\n private readonly contextOf: () => AudioContext;\n private readonly ownsContext: boolean;\n private _context: AudioContext | null = null;\n private fps: number;\n\n private playStartCtxTime: number = 0;\n private seekOffset: number = 0;\n private speed: number = 1;\n private reverse: boolean = false;\n\n private rafId: number | null = null;\n /** Bumped by every `seek`, so the loop can tell that a listener or tick callback moved the playhead under it. */\n private seekEpoch: number = 0;\n private silentSource: AudioBufferSourceNode | null = null;\n\n /**\n * `context` may be a getter, called on the first `play`: a session hands\n * over its audio sink's, which is created on first use, so a session that\n * never plays never opens the audio device.\n */\n constructor(opts: { context?: AudioContext | (() => AudioContext); fps: number }) {\n super();\n const { context } = opts;\n this.contextOf = typeof context === \"function\" ? context : () => context ?? new AudioContext();\n this.ownsContext = !context;\n this.fps = opts.fps;\n }\n\n getContext(): AudioContext {\n this._context ??= this.contextOf();\n return this._context;\n }\n\n private get context(): AudioContext {\n return this.getContext();\n }\n\n seek(t: number): void {\n const clamped = Math.max(0, Math.min(t, this.duration));\n this.seekOffset = clamped;\n // The clock is read only while playing; `play` anchors it afresh.\n if (this.isPlaying) this.playStartCtxTime = this.context.currentTime;\n this.seekEpoch++;\n this.setCurrentTime(clamped);\n }\n\n /**\n * Anchors elapsed-time tracking to the current `context.currentTime`, starts the silent ticker\n * + rAF tick loop and asks a suspended context to resume.\n *\n * Synchronous, as {@link Ticker.play} is: a `pause` or `seek` in the same turn must find a\n * running clock to stop. A suspended context's `currentTime` is frozen, so the clock holds at\n * the anchor until the resume lands — outside a user gesture that is never.\n */\n play(speed: number = 1, reverse: boolean = false): void {\n this.speed = speed;\n this.reverse = reverse;\n this.seekOffset = this.currentTime;\n\n if (this.context.state === \"suspended\") {\n this.context.resume().then(\n () => {\n // A pause that arrived while the resume was pending could not suspend it.\n if (!this.isPlaying && this.context.state === \"running\") void this.context.suspend();\n },\n (error) => {\n console.error(\"[WebTicker] The audio context could not resume:\", error);\n this.pause();\n },\n );\n }\n\n this.playStartCtxTime = this.context.currentTime;\n this.startSilentTicker();\n super.play(speed, reverse);\n this.startLoop();\n }\n\n /** Stops the tick loop and silent ticker, then suspends the context — freezing `currentTime` so elapsed-time math stays correct across the pause. */\n pause(): void {\n this.seekOffset = this.currentTime;\n this.stopLoop();\n this.stopSilentTicker();\n if (this._context?.state === \"running\") {\n this._context.suspend();\n }\n super.pause();\n }\n\n /** (Re)starts the silent buffer source covering the remaining timeline — keeps the AudioContext active and gives a stable graph for other AudioBufferSourceNodes to schedule against, per the class-level note. */\n private startSilentTicker(): void {\n this.stopSilentTicker();\n const remaining = Math.max(0.001, this.duration - this.seekOffset);\n const sampleRate = this.context.sampleRate;\n const buffer = this.context.createBuffer(1, Math.ceil(remaining * sampleRate), sampleRate);\n const source = this.context.createBufferSource();\n source.buffer = buffer;\n source.connect(this.context.destination);\n source.start();\n this.silentSource = source;\n }\n\n private stopSilentTicker(): void {\n if (this.silentSource) {\n try {\n this.silentSource.onended = null;\n this.silentSource.stop();\n this.silentSource.disconnect();\n } catch {\n // already stopped\n }\n this.silentSource = null;\n }\n }\n\n /** Where the playhead is now, from elapsed `context.currentTime` (so it stays sample-accurate), and whether that is the end of the run. */\n private position(): { time: number; ended: boolean } {\n const elapsed = (this.context.currentTime - this.playStartCtxTime) * this.speed;\n const next = this.reverse\n ? this.seekOffset - elapsed\n : this.seekOffset + elapsed;\n const time = Math.max(0, Math.min(next, this.duration));\n return { time, ended: this.reverse ? time <= 0 : time >= this.duration };\n }\n\n /**\n * Drives ticks off `requestAnimationFrame`, firing `tick()` when the playhead reaches another\n * project frame — and once more at the end of the run, whether or not it landed on a boundary.\n *\n * `context.currentTime` moves in whole render quanta handed over in bursts: at 48 kHz on a\n * 60 Hz display it advances 8, 10.67, 18.67 or 21.33 ms between animation frames, and a\n * playhead read straight off it skipped up to a third of the frames. So the playhead advances\n * by each animation frame's own interval, pulled 5% of the way to the audio clock every frame\n * and snapped to it when they disagree by more than 50 ms (a stall, a seek). It stays within\n * a few ms of the audio and moves one frame per frame.\n *\n * A listener may `seek` (a host looping wraps to 0 when it hears the end), so the end is\n * re-read after the listeners and the tick callbacks have run: a run that was seeked out of\n * its end carries on, and one that was not pauses itself.\n */\n private startLoop(): void {\n if (this.rafId !== null) return;\n const frameOf = (t: number) => Math.floor(t * this.fps + 1e-6);\n let lastFrame = frameOf(this.currentTime);\n let steady: { time: number; at: number; epoch: number } | null = null;\n const steadied = (now: number): { time: number; ended: boolean } => {\n const audio = this.position();\n let time = audio.time;\n if (!audio.ended && steady && steady.epoch === this.seekEpoch) {\n const step = ((now - steady.at) / 1000) * this.speed;\n const predicted = this.reverse ? steady.time - step : steady.time + step;\n const drift = audio.time - predicted;\n const next = predicted + drift * 0.05;\n if (Math.abs(drift) <= 0.05 && next > 0 && next < this.duration) time = next;\n }\n steady = { time, at: now, epoch: this.seekEpoch };\n return { time, ended: audio.ended };\n };\n\n const loop = async (now: number) => {\n let epoch = this.seekEpoch;\n const position = steadied(now);\n let ended = position.ended;\n // This loop runs at the display's refresh rate, which on a 120 or 144Hz panel is two\n // or three times the rate anything is rendered at — so the clock advances every time\n // round (the elapsed maths in `play`/`pause` depend on it being exact) while listeners\n // hear about it once per frame. The end of the run always reports, or the final\n // position would never reach a host. See `Ticker.setCurrentTime`.\n const crossed = frameOf(position.time) !== lastFrame;\n const report = crossed || ended;\n this.setCurrentTime(position.time, report);\n\n const reread = () => {\n if (this.seekEpoch === epoch) return;\n epoch = this.seekEpoch;\n lastFrame = frameOf(this.currentTime);\n ended = this.position().ended;\n };\n reread();\n\n if (report) {\n lastFrame = frameOf(this.currentTime);\n // Await so that a slow tick (asset load on a clip\n // boundary) doesn't queue a second concurrent tick.\n await this.tick();\n reread();\n }\n\n // If pause() cleared the rafId while we were awaiting tick(),\n // do not resume the loop.\n if (this.rafId === null) return;\n if (ended) {\n this.pause();\n return;\n }\n this.rafId = requestAnimationFrame(loop);\n };\n\n this.rafId = requestAnimationFrame(loop);\n }\n\n private stopLoop(): void {\n if (this.rafId !== null) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n }\n\n dispose(): void {\n this.stopLoop();\n this.stopSilentTicker();\n if (this.ownsContext) {\n void this._context?.close();\n }\n super.dispose();\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionscript/browser",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-alpha.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Motion Script in the browser: WebGPU rendering through @motionscript/renderer, video and still export, audio playback.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/motionscript-dev/motionscript.git",
|
|
9
|
+
"directory": "packages/browser"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://motionscript.dev",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/motionscript-dev/motionscript/issues"
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"keywords": [],
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": "Motionscript",
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"types": "dist/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"NOTICE",
|
|
26
|
+
"dist",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"!dist/**/*.tsbuildinfo"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./audio": {
|
|
36
|
+
"types": "./dist/audio/index.d.ts",
|
|
37
|
+
"default": "./dist/audio/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@motionscript/core": "^0.1.0-alpha.10"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@vitest/browser": "4.1.7",
|
|
45
|
+
"@vitest/browser-playwright": "^4.1.7",
|
|
46
|
+
"concurrently": "^9.0.0",
|
|
47
|
+
"eslint": "^9.39.4",
|
|
48
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
49
|
+
"eslint-plugin-import-x": "^4.16.1",
|
|
50
|
+
"playwright": "^1.60.0",
|
|
51
|
+
"rolldown": "1.0.3",
|
|
52
|
+
"tsc-alias": "^1.8.16",
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"typescript-eslint": "^8.48.0",
|
|
55
|
+
"vitest": "^4.1.5",
|
|
56
|
+
"@motionscript/core": "0.1.0-alpha.10"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@mediabunny/mp3-encoder": "^1.49.0",
|
|
60
|
+
"esbuild-wasm": "^0.28.1",
|
|
61
|
+
"mediabunny": "^1.40.1",
|
|
62
|
+
"@motionscript/renderer": "^0.1.0-alpha.10"
|
|
63
|
+
},
|
|
64
|
+
"motionscriptBrowser": {
|
|
65
|
+
"assets": {
|
|
66
|
+
"esbuild.wasm": "esbuild-wasm/esbuild.wasm",
|
|
67
|
+
"ms_renderer_bg.wasm": "@motionscript/renderer/ms_renderer_bg.wasm"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "eslint . && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && node ../../scripts/build-browser.mjs",
|
|
72
|
+
"dev": "concurrently \"tsc -p tsconfig.build.json --watch\" \"tsc-alias -p tsconfig.build.json -w\"",
|
|
73
|
+
"clean": "rimraf --glob dist .turbo *.tsbuildinfo",
|
|
74
|
+
"test": "vitest",
|
|
75
|
+
"lint": "eslint .",
|
|
76
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
77
|
+
}
|
|
6
78
|
}
|