@geastack/core 0.1.32 → 0.1.34
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/app_frame_perf.cpp +12 -0
- package/audio-worklet.d.ts +49 -0
- package/gea_app_entry.cpp +36 -3
- package/gea_sources.mjs +2 -2
- package/include/app.h +6 -0
- package/include/audio.h +11 -0
- package/include/audio_stream.h +73 -0
- package/include/pixel.h +40 -6
- package/index.d.ts +228 -8
- package/package.json +5 -2
- package/runtime/audio-worklet.ts +51 -0
- package/runtime-surface.ts +7 -0
- package/scripts/build-gea-vite-geatsc.mjs +51 -3
- package/scripts/gea-bundle-type-hints.mjs +9 -1
- package/scripts/gea-embedded-compat-staging.mjs +2 -0
- package/scripts/gea-static-asset.mjs +15 -0
- package/scripts/gea-vite-module-graph-plugin.mjs +1 -14
- package/scripts/gea-worker-modules.mjs +192 -0
- package/scripts/generate-gea-embedded-assets.mjs +2 -1
- package/typed-arrays.d.ts +13 -0
- package/workers.d.ts +66 -0
package/app_frame_perf.cpp
CHANGED
|
@@ -30,6 +30,12 @@ const char *applicationFramePhaseName(ApplicationFramePhase phase)
|
|
|
30
30
|
case ApplicationFramePhase::StyleRecompute: return "style_recompute";
|
|
31
31
|
case ApplicationFramePhase::DocumentFrame: return "document_frame";
|
|
32
32
|
case ApplicationFramePhase::RefreshMounted: return "refresh_mounted";
|
|
33
|
+
case ApplicationFramePhase::CycleCollection: return "cycle_collection";
|
|
34
|
+
case ApplicationFramePhase::CallbackMicrotasks: return "callback_microtasks";
|
|
35
|
+
case ApplicationFramePhase::WebSocketCallbacks: return "websocket_callbacks";
|
|
36
|
+
case ApplicationFramePhase::RtcCallbacks: return "rtc_callbacks";
|
|
37
|
+
case ApplicationFramePhase::VideoPresentation: return "video_presentation";
|
|
38
|
+
case ApplicationFramePhase::HttpRequests: return "http_requests";
|
|
33
39
|
case ApplicationFramePhase::Idle: return "idle";
|
|
34
40
|
}
|
|
35
41
|
return "idle";
|
|
@@ -59,6 +65,12 @@ void applicationFramePerfStatsAdd(ApplicationFramePerfStats &stats, ApplicationF
|
|
|
59
65
|
case ApplicationFramePhase::RefreshMounted:
|
|
60
66
|
stats.refreshMountedUs += durationUs;
|
|
61
67
|
break;
|
|
68
|
+
case ApplicationFramePhase::CycleCollection:
|
|
69
|
+
case ApplicationFramePhase::CallbackMicrotasks:
|
|
70
|
+
case ApplicationFramePhase::WebSocketCallbacks:
|
|
71
|
+
case ApplicationFramePhase::RtcCallbacks:
|
|
72
|
+
case ApplicationFramePhase::VideoPresentation:
|
|
73
|
+
case ApplicationFramePhase::HttpRequests:
|
|
62
74
|
case ApplicationFramePhase::Idle:
|
|
63
75
|
break;
|
|
64
76
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AudioContext, AudioDestinationNode } from './index'
|
|
2
|
+
import type { MessagePort } from './workers'
|
|
3
|
+
|
|
4
|
+
export interface AudioContextOptions {
|
|
5
|
+
sampleRate?: number
|
|
6
|
+
latencyHint?: 'interactive' | 'balanced' | 'playback' | number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AudioWorklet {
|
|
10
|
+
addModule(moduleURL: string | URL): Promise<void>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AudioWorkletNodeOptions {
|
|
14
|
+
numberOfInputs?: number
|
|
15
|
+
numberOfOutputs?: number
|
|
16
|
+
channelCount?: number
|
|
17
|
+
outputChannelCount?: number[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AudioWorkletNode {
|
|
21
|
+
readonly port: MessagePort
|
|
22
|
+
onprocessorerror: ((event: { readonly message: string }) => void) | null
|
|
23
|
+
connect(destination: AudioDestinationNode): AudioDestinationNode
|
|
24
|
+
disconnect(): void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AudioWorkletNodeConstructor {
|
|
28
|
+
new (context: AudioContext, name: string, options?: AudioWorkletNodeOptions): AudioWorkletNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MediaStreamAudioSourceNode {
|
|
32
|
+
connect(destination: AudioWorkletNode): AudioWorkletNode
|
|
33
|
+
disconnect(): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare global {
|
|
37
|
+
const AudioWorkletNode: AudioWorkletNodeConstructor
|
|
38
|
+
abstract class AudioWorkletProcessor {
|
|
39
|
+
readonly port: MessagePort
|
|
40
|
+
constructor()
|
|
41
|
+
abstract process(inputs: Float32Array[][], outputs: Float32Array[][],
|
|
42
|
+
parameters: Record<string, Float32Array>): boolean
|
|
43
|
+
}
|
|
44
|
+
function registerProcessor(name: string, processorCtor: new () => AudioWorkletProcessor): void
|
|
45
|
+
/** AudioWorkletGlobalScope clock, in the context's sample-rate domain. */
|
|
46
|
+
const sampleRate: number
|
|
47
|
+
const currentFrame: number
|
|
48
|
+
const currentTime: number
|
|
49
|
+
}
|
package/gea_app_entry.cpp
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include "gea_perf_config.h" // GEA_EMBEDDED_PERF master + per-subsystem perf flags
|
|
7
7
|
#include "gea/embedded.h"
|
|
8
8
|
#include "host/backends.h"
|
|
9
|
+
#include "host/worker.h"
|
|
9
10
|
#include "ui/style.h"
|
|
10
11
|
#include "ui/tree_internal.h" // Tree::mount for the post-mount clean repaint in init()
|
|
11
12
|
|
|
@@ -64,7 +65,23 @@ public:
|
|
|
64
65
|
{
|
|
65
66
|
if (active_) gea_cycle_collection_defer_begin();
|
|
66
67
|
}
|
|
67
|
-
~FrameCycleCollectionDeferral() {
|
|
68
|
+
~FrameCycleCollectionDeferral() {
|
|
69
|
+
#if defined(ESP_PLATFORM) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
|
|
70
|
+
const auto started = nowUs();
|
|
71
|
+
#endif
|
|
72
|
+
if (active_) {
|
|
73
|
+
// Scope-exit collection runs after frame phases finish. Keep it
|
|
74
|
+
// visible to the watchdog instead of reporting a stalled frame as idle.
|
|
75
|
+
const auto previous = applicationFramePhaseRead();
|
|
76
|
+
applicationFramePhaseSet(ApplicationFramePhase::CycleCollection);
|
|
77
|
+
gea_cycle_collection_defer_end();
|
|
78
|
+
applicationFramePhaseSet(previous);
|
|
79
|
+
}
|
|
80
|
+
#if defined(ESP_PLATFORM) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
|
|
81
|
+
const auto elapsed = nowUs() - started;
|
|
82
|
+
if (elapsed > 30000) ESP_LOGW("frame_phase", "cycle_collection duration_ms=%lld", (long long)(elapsed / 1000));
|
|
83
|
+
#endif
|
|
84
|
+
}
|
|
68
85
|
|
|
69
86
|
private:
|
|
70
87
|
bool active_;
|
|
@@ -209,7 +226,7 @@ void logFramePerfWindow(int64_t windowEndUs)
|
|
|
209
226
|
// pays zero for phase instrumentation.
|
|
210
227
|
std::int64_t phaseClockUs()
|
|
211
228
|
{
|
|
212
|
-
#if GEA_FRAME_PHASE_TIMING
|
|
229
|
+
#if GEA_FRAME_PHASE_TIMING || CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
|
|
213
230
|
return nowUs();
|
|
214
231
|
#else
|
|
215
232
|
return 0;
|
|
@@ -222,6 +239,11 @@ std::int64_t phaseClockUs()
|
|
|
222
239
|
// neither is set.
|
|
223
240
|
void recordFramePhase(ApplicationFramePhase phase, int64_t durationUs)
|
|
224
241
|
{
|
|
242
|
+
#if defined(ESP_PLATFORM) && CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
|
|
243
|
+
if (durationUs > 30000)
|
|
244
|
+
ESP_LOGW("frame_phase", "%s duration_ms=%lld", applicationFramePhaseName(phase),
|
|
245
|
+
static_cast<long long>(durationUs / 1000));
|
|
246
|
+
#endif
|
|
225
247
|
#if GEA_EMBEDDED_FRAME_SCHEDULER_PERF_LOG
|
|
226
248
|
applicationFramePerfStatsAdd(phase, durationUs);
|
|
227
249
|
#endif
|
|
@@ -273,11 +295,22 @@ void Application::frame(int timestampMs)
|
|
|
273
295
|
startUs = phaseClockUs();
|
|
274
296
|
applicationFramePhaseSet(ApplicationFramePhase::AnimationFrameCallbacks);
|
|
275
297
|
gea::host::runAnimationFrameCallbacks(static_cast<double>(timestampMs));
|
|
298
|
+
applicationFramePhaseSet(ApplicationFramePhase::CallbackMicrotasks);
|
|
276
299
|
generated::drainMicrotasks();
|
|
277
|
-
//
|
|
300
|
+
// The build already proves whether network services are reachable. Keep their
|
|
301
|
+
// callback pumps out of offline frame loops, while still servicing local workers.
|
|
278
302
|
#if !defined(GEA_EMBEDDED_NETWORK_SERVICES_DISABLED) || !GEA_EMBEDDED_NETWORK_SERVICES_DISABLED
|
|
303
|
+
applicationFramePhaseSet(ApplicationFramePhase::WebSocketCallbacks);
|
|
279
304
|
gea::host::websocket::runCallbacks();
|
|
305
|
+
applicationFramePhaseSet(ApplicationFramePhase::RtcCallbacks);
|
|
280
306
|
gea::host::rtc::runCallbacks();
|
|
307
|
+
#else
|
|
308
|
+
gea::host::workers::Context::runMainPending();
|
|
309
|
+
#endif
|
|
310
|
+
applicationFramePhaseSet(ApplicationFramePhase::VideoPresentation);
|
|
311
|
+
gea::host::video::presentFrames();
|
|
312
|
+
#if !defined(GEA_EMBEDDED_NETWORK_SERVICES_DISABLED) || !GEA_EMBEDDED_NETWORK_SERVICES_DISABLED
|
|
313
|
+
applicationFramePhaseSet(ApplicationFramePhase::HttpRequests);
|
|
281
314
|
gea::host::http::runRequests();
|
|
282
315
|
#endif
|
|
283
316
|
recordFramePhase(ApplicationFramePhase::AnimationFrameCallbacks, phaseClockUs() - startUs);
|
package/gea_sources.mjs
CHANGED
|
@@ -87,8 +87,8 @@ export function cxxSources(env = process.env) {
|
|
|
87
87
|
// @geastack/host — platform layer + services
|
|
88
88
|
...cpp(host, ['input', 'wifi', 'bluetooth', 'geolocation']),
|
|
89
89
|
...cpp(join(host, 'host'), [
|
|
90
|
-
'timers', 'input', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
|
|
91
|
-
'ble', 'apps', 'audio', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
|
|
90
|
+
'timers', 'worker', 'input', 'display', 'fetch', 'media', 'rtc', 'websocket', 'http', 'wifi',
|
|
91
|
+
'ble', 'apps', 'audio', 'audio_worklet', 'video', 'mjpeg', 'image', 'touch', 'imu', 'memory', 'camera', 'geolocation',
|
|
92
92
|
'tile_loader'
|
|
93
93
|
]),
|
|
94
94
|
// framework services (built on the platform abstraction — no target-specific deps)
|
package/include/app.h
CHANGED
|
@@ -31,6 +31,12 @@ enum class ApplicationFramePhase : std::uint8_t {
|
|
|
31
31
|
StyleRecompute,
|
|
32
32
|
DocumentFrame,
|
|
33
33
|
RefreshMounted,
|
|
34
|
+
CycleCollection,
|
|
35
|
+
CallbackMicrotasks,
|
|
36
|
+
WebSocketCallbacks,
|
|
37
|
+
RtcCallbacks,
|
|
38
|
+
VideoPresentation,
|
|
39
|
+
HttpRequests,
|
|
34
40
|
};
|
|
35
41
|
|
|
36
42
|
struct ApplicationFramePerfStats {
|
package/include/audio.h
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include <cstddef>
|
|
6
6
|
#include <cstdint>
|
|
7
7
|
#include <string>
|
|
8
|
+
#include <functional>
|
|
8
9
|
|
|
9
10
|
namespace gea::platform::audio {
|
|
10
11
|
|
|
@@ -72,6 +73,14 @@ public:
|
|
|
72
73
|
static bool playFile(const std::string &path);
|
|
73
74
|
static bool playPcm(const std::int16_t *samples, std::size_t sample_count, int sample_rate, int channels);
|
|
74
75
|
static void stopPlayback();
|
|
76
|
+
static void flushPlayback();
|
|
77
|
+
// Continuous 16 kHz mono source; zero means the platform could not attach it.
|
|
78
|
+
static std::uint64_t playPcmStream(std::function<std::size_t(std::int16_t*, std::size_t)> pull);
|
|
79
|
+
static void stopPcmStream(std::uint64_t stream);
|
|
80
|
+
static bool pcmStreamSettled(std::uint64_t stream);
|
|
81
|
+
// Maximum mixer and device buffering delays, in seconds.
|
|
82
|
+
static double processingLatency();
|
|
83
|
+
static double outputLatency();
|
|
75
84
|
};
|
|
76
85
|
|
|
77
86
|
#ifdef GEA_AUDIO_DRIVER_INTERNAL
|
|
@@ -80,8 +89,10 @@ public:
|
|
|
80
89
|
static bool open(int sample_rate, int channels, int bits_per_sample);
|
|
81
90
|
static bool write(const std::int16_t *pcm, std::size_t sample_count, int timeout_ms);
|
|
82
91
|
static void close();
|
|
92
|
+
static bool flush();
|
|
83
93
|
static int volume();
|
|
84
94
|
static void setVolume(int volume_percent);
|
|
95
|
+
static double outputLatency(int sample_rate);
|
|
85
96
|
};
|
|
86
97
|
#endif
|
|
87
98
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <array>
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
#include <functional>
|
|
8
|
+
#include <map>
|
|
9
|
+
#include <mutex>
|
|
10
|
+
|
|
11
|
+
namespace gea::platform::audio {
|
|
12
|
+
|
|
13
|
+
// The RTC decoder supplies 16 kHz mono PCM. Pulling from independent readers
|
|
14
|
+
// avoids copying every incoming packet into another playback queue.
|
|
15
|
+
class PcmStreamMixer {
|
|
16
|
+
public:
|
|
17
|
+
using Pull = std::function<std::size_t(std::int16_t*, std::size_t)>;
|
|
18
|
+
using Handle = std::uint64_t;
|
|
19
|
+
static constexpr std::size_t chunkFrames = 512;
|
|
20
|
+
|
|
21
|
+
Handle add(Pull pull) {
|
|
22
|
+
if (!pull) return 0;
|
|
23
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
24
|
+
const auto id = ++next_;
|
|
25
|
+
sources_.emplace(id, Source{std::move(pull)});
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
void remove(Handle id) { std::lock_guard<std::mutex> lock(mutex_); sources_.erase(id); }
|
|
29
|
+
void clear() { std::lock_guard<std::mutex> lock(mutex_); sources_.clear(); }
|
|
30
|
+
bool active() const { std::lock_guard<std::mutex> lock(mutex_); return !sources_.empty(); }
|
|
31
|
+
bool settled(Handle id) const {
|
|
32
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
33
|
+
const auto it = sources_.find(id);
|
|
34
|
+
return it == sources_.end() || (!it->second.inFlight && !it->second.failed);
|
|
35
|
+
}
|
|
36
|
+
// Called by the sole output consumer AFTER its driver write returns. A
|
|
37
|
+
// reader being empty only proves its last block was pulled, not submitted.
|
|
38
|
+
void didWrite(bool success) {
|
|
39
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
40
|
+
for (auto& [id, source] : sources_) {
|
|
41
|
+
if (source.inFlight && !success) source.failed = true;
|
|
42
|
+
source.inFlight = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mix into existing stereo samples (e.g. oscillator output). Return the
|
|
47
|
+
// frames actually read: a short network packet must not acquire silence
|
|
48
|
+
// padding that lengthens speech and accumulates latency.
|
|
49
|
+
std::size_t mix(std::int16_t* stereo, std::size_t frames) {
|
|
50
|
+
frames = std::min(frames, chunkFrames);
|
|
51
|
+
std::array<std::int16_t, chunkFrames> mono;
|
|
52
|
+
std::size_t available = 0;
|
|
53
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
54
|
+
for (auto& [id, source] : sources_) {
|
|
55
|
+
const auto count = std::min(frames, source.pull(mono.data(), frames));
|
|
56
|
+
source.inFlight = source.inFlight || count > 0;
|
|
57
|
+
available = std::max(available, count);
|
|
58
|
+
for (std::size_t i = 0; i < count; ++i) {
|
|
59
|
+
for (std::size_t channel = 0; channel < 2; ++channel) {
|
|
60
|
+
const auto sum = static_cast<int>(stereo[i * 2 + channel]) + mono[i];
|
|
61
|
+
stereo[i * 2 + channel] = static_cast<std::int16_t>(std::clamp(sum, -32768, 32767));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return available;
|
|
66
|
+
}
|
|
67
|
+
private:
|
|
68
|
+
mutable std::mutex mutex_;
|
|
69
|
+
struct Source { Pull pull; bool inFlight = false; bool failed = false; };
|
|
70
|
+
std::map<Handle, Source> sources_;
|
|
71
|
+
Handle next_ = 0;
|
|
72
|
+
};
|
|
73
|
+
}
|
package/include/pixel.h
CHANGED
|
@@ -658,17 +658,51 @@ inline std::uint16_t fromNative(native_t pixel)
|
|
|
658
658
|
#endif
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
//
|
|
661
|
+
// Non-overlapping native pixel spans, including cropped rows whose source and
|
|
662
|
+
// destination have different vector alignment. Keep every load within the span:
|
|
663
|
+
// a scalar head precedes the first aligned-down load, and the scalar tail leaves
|
|
664
|
+
// room for the unaligned vector load's second aligned block.
|
|
665
|
+
inline void copyNative(native_t *dst, const native_t *src, int count)
|
|
666
|
+
{
|
|
667
|
+
if (count <= 0) return;
|
|
668
|
+
#if GEA_PIXEL_PIE_FILL16 && !GEA_PIXEL_FORMAT_IS_8888 && !GEA_PIXEL_FORMAT_IS_GRAY
|
|
669
|
+
if (count >= 32) {
|
|
670
|
+
for (int i = 0; i < 8; ++i) { *dst++ = *src++; --count; }
|
|
671
|
+
while ((reinterpret_cast<std::uintptr_t>(dst) & 15u) != 0) {
|
|
672
|
+
*dst++ = *src++;
|
|
673
|
+
--count;
|
|
674
|
+
}
|
|
675
|
+
const int vectors = (count - 8) >> 3;
|
|
676
|
+
if (vectors > 0) {
|
|
677
|
+
const native_t *cursor = src;
|
|
678
|
+
asm volatile(
|
|
679
|
+
"ee.ld.128.usar.ip q0, %[src], 16\n"
|
|
680
|
+
"loopnez %[vectors], 1f\n"
|
|
681
|
+
"ee.ld.128.usar.ip q1, %[src], 16\n"
|
|
682
|
+
"ee.src.q.qup q2, q0, q1\n"
|
|
683
|
+
"ee.vst.128.ip q2, %[dst], 16\n"
|
|
684
|
+
"ee.orq q0, q1, q1\n"
|
|
685
|
+
"1:\n"
|
|
686
|
+
: [src] "+&r"(cursor), [dst] "+&r"(dst)
|
|
687
|
+
: [vectors] "r"(vectors)
|
|
688
|
+
: "memory");
|
|
689
|
+
src += vectors * 8;
|
|
690
|
+
count -= vectors * 8;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
#endif
|
|
694
|
+
std::memcpy(dst, src, static_cast<std::size_t>(count) * sizeof(native_t));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Row copies across the RGB565-scratch <-> native-framebuffer boundary. RGB565
|
|
698
|
+
// uses the native span copy above; other framebuffer formats convert per pixel.
|
|
665
699
|
inline void copyNativeToRgb565(std::uint16_t *dst, const native_t *src, int count)
|
|
666
700
|
{
|
|
667
701
|
if (count <= 0) return;
|
|
668
702
|
#if GEA_PIXEL_FORMAT_IS_8888 || GEA_PIXEL_FORMAT_IS_GRAY
|
|
669
703
|
for (int i = 0; i < count; i++) dst[i] = fromNative(src[i]);
|
|
670
704
|
#else
|
|
671
|
-
|
|
705
|
+
copyNative(dst, src, count);
|
|
672
706
|
#endif
|
|
673
707
|
}
|
|
674
708
|
|
|
@@ -678,7 +712,7 @@ inline void copyRgb565ToNative(native_t *dst, const std::uint16_t *src, int coun
|
|
|
678
712
|
#if GEA_PIXEL_FORMAT_IS_8888 || GEA_PIXEL_FORMAT_IS_GRAY
|
|
679
713
|
for (int i = 0; i < count; i++) dst[i] = toNative(src[i]);
|
|
680
714
|
#else
|
|
681
|
-
|
|
715
|
+
copyNative(dst, src, count);
|
|
682
716
|
#endif
|
|
683
717
|
}
|
|
684
718
|
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/// <reference path="./css.d.ts" />
|
|
2
2
|
|
|
3
|
+
export type * from './workers'
|
|
4
|
+
export type * from './audio-worklet'
|
|
5
|
+
/** Gea-only hardware output cancellation; call from an AudioWorklet message handler. */
|
|
6
|
+
export declare function flushAudioWorkletOutput(): void
|
|
7
|
+
export type * from './typed-arrays'
|
|
8
|
+
import type { AudioContextOptions, AudioWorklet, MediaStreamAudioSourceNode } from './audio-worklet'
|
|
9
|
+
|
|
3
10
|
// Opt-in native 64-bit integer. Structurally a `number` (assign/read without
|
|
4
11
|
// casts), but where a binding, parameter, field, or return is annotated `int`,
|
|
5
12
|
// geatsc lowers it to a C++ `long long` and emits exact integer arithmetic
|
|
@@ -70,6 +77,7 @@ export interface GeaIntrinsicElements {
|
|
|
70
77
|
h5: NativeViewProps
|
|
71
78
|
h6: NativeViewProps
|
|
72
79
|
audio: AudioProps
|
|
80
|
+
video: VideoProps
|
|
73
81
|
button: NativeButtonProps
|
|
74
82
|
img: ImageProps
|
|
75
83
|
input: NativeInputElementProps
|
|
@@ -117,9 +125,13 @@ declare global {
|
|
|
117
125
|
// (`double setTimeout(TimerCallback, double)`), so both the id and the delay
|
|
118
126
|
// are `number`, and the callback takes no arguments (unlike the browser's,
|
|
119
127
|
// which forwards extra `setTimeout` arguments to it).
|
|
128
|
+
/** @gea-host-no-property-writes */
|
|
120
129
|
function setTimeout(callback: () => void, delayMs: number): number
|
|
130
|
+
/** @gea-host-no-property-writes */
|
|
121
131
|
function clearTimeout(id: number): void
|
|
132
|
+
/** @gea-host-no-property-writes */
|
|
122
133
|
function setInterval(callback: () => void, delayMs: number): number
|
|
134
|
+
/** @gea-host-no-property-writes */
|
|
123
135
|
function clearInterval(id: number): void
|
|
124
136
|
|
|
125
137
|
/**
|
|
@@ -364,6 +376,7 @@ export interface Document {
|
|
|
364
376
|
querySelector(selector: string): Element | null
|
|
365
377
|
querySelectorAll(selector: string): readonly Element[]
|
|
366
378
|
createElement(tag: 'audio'): HTMLAudioElement
|
|
379
|
+
createElement(tag: 'video'): HTMLVideoElement
|
|
367
380
|
createElement(tag: 'virtual-list'): VirtualListElement
|
|
368
381
|
createElement(tag: string): Element
|
|
369
382
|
createTextNode(text: string): Node
|
|
@@ -596,13 +609,28 @@ export interface AudioProps extends DataAttributes {
|
|
|
596
609
|
autoplay?: boolean | string | number
|
|
597
610
|
controls?: boolean | string | number
|
|
598
611
|
loop?: boolean | string | number
|
|
599
|
-
ref?: HTMLAudioElement | null
|
|
612
|
+
ref?: HTMLAudioElement | (typeof globalThis extends { HTMLAudioElement: { prototype: infer E } } ? E : never) | null
|
|
613
|
+
autoPlay?: boolean
|
|
600
614
|
children?: any
|
|
601
615
|
}
|
|
602
616
|
|
|
617
|
+
export interface VideoProps extends DataAttributes {
|
|
618
|
+
id?: string
|
|
619
|
+
class?: ClassValue
|
|
620
|
+
style?: Style
|
|
621
|
+
autoplay?: boolean
|
|
622
|
+
muted?: boolean
|
|
623
|
+
playsinline?: boolean
|
|
624
|
+
autoPlay?: boolean
|
|
625
|
+
playsInline?: boolean
|
|
626
|
+
ref?: HTMLVideoElement | (typeof globalThis extends { HTMLVideoElement: { prototype: infer E } } ? E : never) | null
|
|
627
|
+
}
|
|
628
|
+
|
|
603
629
|
export type ImageSource = string | ArrayBuffer | Uint8Array | GeaEmbeddedImage
|
|
604
630
|
|
|
605
631
|
export interface ImageProps extends DataAttributes {
|
|
632
|
+
alt?: string
|
|
633
|
+
key?: string | number
|
|
606
634
|
id?: string
|
|
607
635
|
class?: ClassValue
|
|
608
636
|
// Optional: a static template can leave this unset and apply the source
|
|
@@ -1300,15 +1328,26 @@ export interface OscillatorNode {
|
|
|
1300
1328
|
}
|
|
1301
1329
|
|
|
1302
1330
|
export interface AudioContext {
|
|
1331
|
+
/** Conservative maximum software buffering delay, in seconds. */
|
|
1332
|
+
readonly baseLatency: number
|
|
1333
|
+
/** Device buffering estimate in seconds; zero when unavailable. */
|
|
1334
|
+
readonly outputLatency: number
|
|
1335
|
+
readonly sampleRate: number
|
|
1336
|
+
readonly state: 'running' | 'suspended' | 'closed'
|
|
1337
|
+
readonly audioWorklet: AudioWorklet
|
|
1303
1338
|
readonly currentTime: number
|
|
1304
1339
|
readonly destination: AudioDestinationNode
|
|
1305
1340
|
createOscillator(): OscillatorNode
|
|
1306
1341
|
createBufferSource(): AudioBufferSourceNode
|
|
1307
1342
|
decodeAudioData(audioData: ArrayBuffer | Uint8Array | GeaAudioBlob): Promise<AudioBuffer>
|
|
1343
|
+
createMediaStreamSource(stream: MediaStream): MediaStreamAudioSourceNode
|
|
1344
|
+
resume(): Promise<void>
|
|
1345
|
+
suspend(): Promise<void>
|
|
1346
|
+
close(): Promise<void>
|
|
1308
1347
|
}
|
|
1309
1348
|
|
|
1310
1349
|
export interface AudioContextConstructor {
|
|
1311
|
-
new (): AudioContext
|
|
1350
|
+
new (options?: AudioContextOptions): AudioContext
|
|
1312
1351
|
}
|
|
1313
1352
|
|
|
1314
1353
|
export declare const audioContext: AudioContext
|
|
@@ -1531,6 +1570,7 @@ export interface ButtonProps {
|
|
|
1531
1570
|
}
|
|
1532
1571
|
|
|
1533
1572
|
export interface NativeButtonProps extends NativeEventAttributes {
|
|
1573
|
+
disabled?: boolean
|
|
1534
1574
|
key?: string | number
|
|
1535
1575
|
class?: ClassValue
|
|
1536
1576
|
style?: Style
|
|
@@ -1653,9 +1693,13 @@ export interface FetchResponse {
|
|
|
1653
1693
|
bytes(): Uint8Array
|
|
1654
1694
|
}
|
|
1655
1695
|
|
|
1696
|
+
/** @gea-host-no-property-writes */
|
|
1656
1697
|
export declare function fetchAsync(url: string, init?: FetchRequestInit): number
|
|
1698
|
+
/** @gea-host-no-property-writes */
|
|
1657
1699
|
export declare function fetchReady(id: number): boolean
|
|
1700
|
+
/** @gea-host-no-property-writes */
|
|
1658
1701
|
export declare function fetchResult(id: number): FetchResponse
|
|
1702
|
+
/** @gea-host-no-property-writes */
|
|
1659
1703
|
export declare function fetchRelease(id: number): void
|
|
1660
1704
|
|
|
1661
1705
|
/**
|
|
@@ -1692,6 +1736,7 @@ export declare function fetchUploadTotal(id: number): number
|
|
|
1692
1736
|
|
|
1693
1737
|
declare global {
|
|
1694
1738
|
function fetch(url: string, init?: FetchRequestInit): FetchResponse
|
|
1739
|
+
/** @gea-host-no-property-writes */
|
|
1695
1740
|
function fetchAsync(url: string, init?: FetchRequestInit): number
|
|
1696
1741
|
function fetchUploadFileAsync(
|
|
1697
1742
|
url: string,
|
|
@@ -1705,8 +1750,11 @@ declare global {
|
|
|
1705
1750
|
function fetchUploadProgress(id: number): number
|
|
1706
1751
|
function fetchUploadSent(id: number): number
|
|
1707
1752
|
function fetchUploadTotal(id: number): number
|
|
1753
|
+
/** @gea-host-no-property-writes */
|
|
1708
1754
|
function fetchReady(id: number): boolean
|
|
1755
|
+
/** @gea-host-no-property-writes */
|
|
1709
1756
|
function fetchResult(id: number): FetchResponse
|
|
1757
|
+
/** @gea-host-no-property-writes */
|
|
1710
1758
|
function fetchRelease(id: number): void
|
|
1711
1759
|
function atob(data: string): string
|
|
1712
1760
|
function btoa(data: string | ArrayBuffer | Uint8Array): string
|
|
@@ -1727,18 +1775,20 @@ export interface WebSocketErrorEvent {
|
|
|
1727
1775
|
}
|
|
1728
1776
|
|
|
1729
1777
|
export interface WebSocketInstance {
|
|
1778
|
+
readonly bufferedAmount: number
|
|
1730
1779
|
readonly url: string
|
|
1731
1780
|
readonly readyState: number
|
|
1732
1781
|
onopen: (() => void) | null
|
|
1733
1782
|
onmessage: ((event: WebSocketMessageEvent) => void) | null
|
|
1734
1783
|
onclose: ((event: WebSocketCloseEvent) => void) | null
|
|
1735
1784
|
onerror: ((event: WebSocketErrorEvent) => void) | null
|
|
1785
|
+
/** @gea-host-no-property-writes */
|
|
1736
1786
|
send(data: string): void
|
|
1737
1787
|
close(code?: number, reason?: string): void
|
|
1738
1788
|
}
|
|
1739
1789
|
|
|
1740
1790
|
export interface WebSocketConstructor {
|
|
1741
|
-
new (url: string): WebSocketInstance
|
|
1791
|
+
new (url: string, protocols?: string | string[]): WebSocketInstance
|
|
1742
1792
|
readonly CONNECTING: 0
|
|
1743
1793
|
readonly OPEN: 1
|
|
1744
1794
|
readonly CLOSING: 2
|
|
@@ -1805,7 +1855,7 @@ export declare const http: HttpModule
|
|
|
1805
1855
|
|
|
1806
1856
|
export interface MediaStreamTrack {
|
|
1807
1857
|
readonly id: string
|
|
1808
|
-
readonly kind:
|
|
1858
|
+
readonly kind: string
|
|
1809
1859
|
enabled: boolean
|
|
1810
1860
|
readonly readyState: 'live' | 'ended'
|
|
1811
1861
|
stop(): void
|
|
@@ -1813,12 +1863,19 @@ export interface MediaStreamTrack {
|
|
|
1813
1863
|
|
|
1814
1864
|
export interface MediaStream {
|
|
1815
1865
|
readonly id: string
|
|
1866
|
+
readonly active: boolean
|
|
1816
1867
|
getAudioTracks(): MediaStreamTrack[]
|
|
1868
|
+
getVideoTracks(): MediaStreamTrack[]
|
|
1817
1869
|
getTracks(): MediaStreamTrack[]
|
|
1870
|
+
getTrackById(id: string): MediaStreamTrack | null
|
|
1871
|
+
addTrack(track: MediaStreamTrack): void
|
|
1872
|
+
removeTrack(track: MediaStreamTrack): void
|
|
1818
1873
|
}
|
|
1819
1874
|
|
|
1820
1875
|
export interface MediaStreamConstructor {
|
|
1821
1876
|
new (): MediaStream
|
|
1877
|
+
new (stream: MediaStream): MediaStream
|
|
1878
|
+
new (tracks: MediaStreamTrack[]): MediaStream
|
|
1822
1879
|
}
|
|
1823
1880
|
|
|
1824
1881
|
export interface MediaStreamConstraints {
|
|
@@ -1863,10 +1920,34 @@ export interface MediaRecorderConstructor {
|
|
|
1863
1920
|
|
|
1864
1921
|
export interface HTMLAudioElement extends Element {
|
|
1865
1922
|
src: string
|
|
1923
|
+
srcObject: MediaStream | null
|
|
1924
|
+
autoplay: boolean
|
|
1925
|
+
readonly paused: boolean
|
|
1866
1926
|
currentTime: number
|
|
1867
1927
|
volume: number
|
|
1868
1928
|
loop: boolean
|
|
1869
|
-
play(): void
|
|
1929
|
+
play(): Promise<void>
|
|
1930
|
+
pause(): void
|
|
1931
|
+
/** Freeze the current PCM queue endpoint; later RTP silence is excluded. */
|
|
1932
|
+
beginDrain(): void
|
|
1933
|
+
/** Discard queued speech while keeping the live output clock running. */
|
|
1934
|
+
clearBufferedAudio(): void
|
|
1935
|
+
/** Queued PCM has reached the output driver. Hardware DMA drains on capture handoff. */
|
|
1936
|
+
readonly drained: boolean
|
|
1937
|
+
/** RMS of PCM being pulled for playback, normalized to 0–1. */
|
|
1938
|
+
readonly audioLevel: number
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
export interface HTMLVideoElement extends Element {
|
|
1942
|
+
/** Native HTTP multipart JPEG streaming; decoded on native network/video workers. */
|
|
1943
|
+
src: string
|
|
1944
|
+
srcObject: MediaStream | null
|
|
1945
|
+
autoplay: boolean
|
|
1946
|
+
readonly paused: boolean
|
|
1947
|
+
readonly videoWidth: number
|
|
1948
|
+
readonly videoHeight: number
|
|
1949
|
+
onplaying: (() => void) | null
|
|
1950
|
+
play(): Promise<void>
|
|
1870
1951
|
pause(): void
|
|
1871
1952
|
}
|
|
1872
1953
|
|
|
@@ -1875,6 +1956,13 @@ export interface AudioConstructor {
|
|
|
1875
1956
|
}
|
|
1876
1957
|
|
|
1877
1958
|
declare global {
|
|
1959
|
+
interface HTMLAudioElement {
|
|
1960
|
+
beginDrain(): void
|
|
1961
|
+
clearBufferedAudio(): void
|
|
1962
|
+
readonly drained: boolean
|
|
1963
|
+
/** RMS of PCM being pulled for playback, normalized to 0–1. */
|
|
1964
|
+
readonly audioLevel: number
|
|
1965
|
+
}
|
|
1878
1966
|
interface Blob {
|
|
1879
1967
|
readonly path: string
|
|
1880
1968
|
}
|
|
@@ -1907,10 +1995,54 @@ export interface RTCSessionDescriptionInit {
|
|
|
1907
1995
|
sdp?: string
|
|
1908
1996
|
}
|
|
1909
1997
|
|
|
1998
|
+
export interface RTCSessionDescription {
|
|
1999
|
+
readonly type: RTCSessionDescriptionInit['type']
|
|
2000
|
+
readonly sdp: string
|
|
2001
|
+
toJSON(): RTCSessionDescriptionInit
|
|
2002
|
+
}
|
|
2003
|
+
export interface RTCSessionDescriptionConstructor {
|
|
2004
|
+
new (init: RTCSessionDescriptionInit): RTCSessionDescription
|
|
2005
|
+
}
|
|
2006
|
+
declare global {
|
|
2007
|
+
const RTCSessionDescription: RTCSessionDescriptionConstructor
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1910
2010
|
export interface RTCIceCandidateInit {
|
|
1911
|
-
candidate
|
|
2011
|
+
candidate?: string
|
|
1912
2012
|
sdpMid?: string | null
|
|
1913
2013
|
sdpMLineIndex?: number | null
|
|
2014
|
+
usernameFragment?: string | null
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
export interface RTCLocalIceCandidateInit extends RTCIceCandidateInit {
|
|
2018
|
+
relayProtocol?: 'udp' | 'tcp' | 'tls' | null
|
|
2019
|
+
url?: string | null
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
export interface RTCIceCandidate {
|
|
2023
|
+
readonly candidate: string
|
|
2024
|
+
readonly sdpMid: string | null
|
|
2025
|
+
readonly sdpMLineIndex: number | null
|
|
2026
|
+
readonly foundation: string | null
|
|
2027
|
+
readonly component: 'rtp' | 'rtcp' | null
|
|
2028
|
+
readonly priority: number | null
|
|
2029
|
+
readonly address: string | null
|
|
2030
|
+
readonly protocol: 'udp' | 'tcp' | null
|
|
2031
|
+
readonly port: number | null
|
|
2032
|
+
readonly type: 'host' | 'srflx' | 'prflx' | 'relay' | null
|
|
2033
|
+
readonly tcpType: 'active' | 'passive' | 'so' | null
|
|
2034
|
+
readonly relatedAddress: string | null
|
|
2035
|
+
readonly relatedPort: number | null
|
|
2036
|
+
readonly usernameFragment: string | null
|
|
2037
|
+
readonly relayProtocol: 'udp' | 'tcp' | 'tls' | null
|
|
2038
|
+
readonly url: string | null
|
|
2039
|
+
toJSON(): RTCIceCandidateInit
|
|
2040
|
+
}
|
|
2041
|
+
export interface RTCIceCandidateConstructor {
|
|
2042
|
+
new (init?: RTCLocalIceCandidateInit): RTCIceCandidate
|
|
2043
|
+
}
|
|
2044
|
+
declare global {
|
|
2045
|
+
const RTCIceCandidate: RTCIceCandidateConstructor
|
|
1914
2046
|
}
|
|
1915
2047
|
|
|
1916
2048
|
export interface RTCIceServer {
|
|
@@ -1921,15 +2053,61 @@ export interface RTCIceServer {
|
|
|
1921
2053
|
|
|
1922
2054
|
export interface RTCConfiguration {
|
|
1923
2055
|
iceServers?: RTCIceServer[]
|
|
2056
|
+
iceTransportPolicy?: 'all' | 'relay'
|
|
1924
2057
|
}
|
|
1925
2058
|
|
|
1926
2059
|
export interface RTCPeerConnectionIceEvent {
|
|
1927
|
-
readonly candidate:
|
|
2060
|
+
readonly candidate: RTCIceCandidate | null
|
|
1928
2061
|
}
|
|
1929
2062
|
|
|
1930
2063
|
export interface RTCTrackEvent {
|
|
1931
2064
|
readonly track: MediaStreamTrack
|
|
1932
2065
|
readonly streams: readonly MediaStream[]
|
|
2066
|
+
readonly receiver: RTCRtpReceiver
|
|
2067
|
+
readonly transceiver: RTCRtpTransceiver
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
export interface RTCDataChannelInit {
|
|
2071
|
+
ordered?: boolean
|
|
2072
|
+
maxPacketLifeTime?: number
|
|
2073
|
+
maxRetransmits?: number
|
|
2074
|
+
protocol?: string
|
|
2075
|
+
negotiated?: boolean
|
|
2076
|
+
id?: number
|
|
2077
|
+
}
|
|
2078
|
+
export interface RTCDataChannelMessageEvent { readonly data: string | ArrayBuffer }
|
|
2079
|
+
export interface RTCDataChannel {
|
|
2080
|
+
readonly label: string
|
|
2081
|
+
readonly protocol: string
|
|
2082
|
+
readonly ordered: boolean
|
|
2083
|
+
readonly id: number | null
|
|
2084
|
+
readonly maxPacketLifeTime: number | null
|
|
2085
|
+
readonly maxRetransmits: number | null
|
|
2086
|
+
readonly readyState: 'connecting' | 'open' | 'closing' | 'closed'
|
|
2087
|
+
binaryType: 'arraybuffer'
|
|
2088
|
+
readonly bufferedAmount: number
|
|
2089
|
+
bufferedAmountLowThreshold: number
|
|
2090
|
+
onopen: (() => void) | null
|
|
2091
|
+
onclose: (() => void) | null
|
|
2092
|
+
onmessage: ((event: RTCDataChannelMessageEvent) => void) | null
|
|
2093
|
+
onbufferedamountlow: (() => void) | null
|
|
2094
|
+
send(data: string | ArrayBuffer | Uint8Array): void
|
|
2095
|
+
close(): void
|
|
2096
|
+
}
|
|
2097
|
+
export interface RTCRtpSender {
|
|
2098
|
+
readonly track: MediaStreamTrack | null
|
|
2099
|
+
replaceTrack(track: MediaStreamTrack | null): Promise<void>
|
|
2100
|
+
}
|
|
2101
|
+
export interface RTCRtpReceiver { readonly track: MediaStreamTrack | null }
|
|
2102
|
+
export interface RTCRtpTransceiverInit { direction?: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive' }
|
|
2103
|
+
export interface RTCRtpTransceiver {
|
|
2104
|
+
readonly sender: RTCRtpSender
|
|
2105
|
+
readonly receiver: RTCRtpReceiver
|
|
2106
|
+
readonly mid: string | null
|
|
2107
|
+
direction: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive'
|
|
2108
|
+
readonly currentDirection: 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive' | null
|
|
2109
|
+
readonly stopped: boolean
|
|
2110
|
+
stop(): void
|
|
1933
2111
|
}
|
|
1934
2112
|
|
|
1935
2113
|
export interface RTCPeerConnectionInstance {
|
|
@@ -1941,7 +2119,19 @@ export interface RTCPeerConnectionInstance {
|
|
|
1941
2119
|
onconnectionstatechange: (() => void) | null
|
|
1942
2120
|
oniceconnectionstatechange: (() => void) | null
|
|
1943
2121
|
|
|
1944
|
-
|
|
2122
|
+
readonly signalingState: 'stable' | 'have-local-offer' | 'have-remote-offer' | 'closed'
|
|
2123
|
+
readonly localDescription: RTCSessionDescription | null
|
|
2124
|
+
readonly remoteDescription: RTCSessionDescription | null
|
|
2125
|
+
onsignalingstatechange: (() => void) | null
|
|
2126
|
+
onnegotiationneeded: (() => void) | null
|
|
2127
|
+
ondatachannel: ((event: { channel: RTCDataChannel }) => void) | null
|
|
2128
|
+
addTrack(track: MediaStreamTrack, stream?: MediaStream): RTCRtpSender
|
|
2129
|
+
removeTrack(sender: RTCRtpSender): void
|
|
2130
|
+
addTransceiver(trackOrKind: MediaStreamTrack | 'audio', init?: RTCRtpTransceiverInit): RTCRtpTransceiver
|
|
2131
|
+
getTransceivers(): RTCRtpTransceiver[]
|
|
2132
|
+
getSenders(): RTCRtpSender[]
|
|
2133
|
+
getReceivers(): RTCRtpReceiver[]
|
|
2134
|
+
createDataChannel(label: string, init?: RTCDataChannelInit): RTCDataChannel
|
|
1945
2135
|
createOffer(): Promise<RTCSessionDescriptionInit>
|
|
1946
2136
|
createAnswer(): Promise<RTCSessionDescriptionInit>
|
|
1947
2137
|
setLocalDescription(desc: RTCSessionDescriptionInit): Promise<void>
|
|
@@ -1958,3 +2148,33 @@ declare global {
|
|
|
1958
2148
|
const RTCPeerConnection: RTCPeerConnectionConstructor
|
|
1959
2149
|
function Image(props: ImageProps): GeaJsxElement
|
|
1960
2150
|
}
|
|
2151
|
+
|
|
2152
|
+
|
|
2153
|
+
/** Streaming mono signed PCM16, little endian, carried as base64 text. */
|
|
2154
|
+
export interface PcmAudioStream {
|
|
2155
|
+
setInput(stream: MediaStream): void
|
|
2156
|
+
/** Capture and upload on the native socket worker, independently of UI callbacks. */
|
|
2157
|
+
pipeTo(socket: WebSocketInstance, prefix: string, suffix: string): void
|
|
2158
|
+
/** Decode matching PCM packets on the native receive worker; control messages still reach JS. */
|
|
2159
|
+
receiveFrom(socket: WebSocketInstance, prefix: string, suffix: string): void
|
|
2160
|
+
/** Read the next 40ms capture block, or an empty string if not ready. */
|
|
2161
|
+
readBase64(): string
|
|
2162
|
+
writeBase64(data: string): void
|
|
2163
|
+
resetPlayback(interrupted?: boolean): void
|
|
2164
|
+
close(): void
|
|
2165
|
+
readonly queuedMs: number
|
|
2166
|
+
/** Conservative audible duration, excluding mixer and device DMA latency. */
|
|
2167
|
+
readonly playedMs: number
|
|
2168
|
+
readonly drained: boolean
|
|
2169
|
+
readonly audioLevel: number
|
|
2170
|
+
readonly capturePendingMs: number
|
|
2171
|
+
readonly captureDroppedSamples: number
|
|
2172
|
+
readonly capturePackets: number
|
|
2173
|
+
}
|
|
2174
|
+
export interface PcmAudioStreamConstructor {
|
|
2175
|
+
new (sampleRate: number): PcmAudioStream
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
declare global {
|
|
2179
|
+
const PcmAudioStream: PcmAudioStreamConstructor
|
|
2180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geastack/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "GeaStack app API, reactivity headers, runtime sources, and type declarations.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"app_frame_perf.cpp",
|
|
36
36
|
"css.d.ts",
|
|
37
|
+
"workers.d.ts",
|
|
38
|
+
"audio-worklet.d.ts",
|
|
39
|
+
"typed-arrays.d.ts",
|
|
37
40
|
"events.cpp",
|
|
38
41
|
"gea_sources.sh",
|
|
39
42
|
"gea_sources.mjs",
|
|
@@ -63,7 +66,7 @@
|
|
|
63
66
|
"@geajs/core": "1.5.1",
|
|
64
67
|
"@geajs/vite-plugin": "1.5.0",
|
|
65
68
|
"@geastack/compiler": "^1.0.20",
|
|
66
|
-
"@geastack/geatsc-plugin-gea": "^0.1.
|
|
69
|
+
"@geastack/geatsc-plugin-gea": "^0.1.15",
|
|
67
70
|
"opentype.js": "^2.0.0",
|
|
68
71
|
"vite": "^8.0.0"
|
|
69
72
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { MessagePort } from '../workers.js'
|
|
2
|
+
|
|
3
|
+
/** Native implementations of the AudioWorkletGlobalScope's two class APIs.
|
|
4
|
+
* The module loader supplies these lexical bindings to compiled worklet modules;
|
|
5
|
+
* applications use the browser globals, without imports or platform branches.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare function __geaAudioWorkletProcessorPort(): MessagePort
|
|
9
|
+
declare function __geaFlushAudioWorkletOutput(): void
|
|
10
|
+
|
|
11
|
+
/** Gea hardware cancellation extension; capture remains active. */
|
|
12
|
+
export function flushAudioWorkletOutput(): void {
|
|
13
|
+
__geaFlushAudioWorkletOutput()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare function __geaRegisterAudioWorkletProcessor(
|
|
17
|
+
name: string,
|
|
18
|
+
create: () => AudioWorkletProcessor,
|
|
19
|
+
process: (
|
|
20
|
+
processor: AudioWorkletProcessor,
|
|
21
|
+
inputs: Float32Array[][],
|
|
22
|
+
outputs: Float32Array[][]
|
|
23
|
+
) => boolean
|
|
24
|
+
): void
|
|
25
|
+
|
|
26
|
+
export abstract class AudioWorkletProcessor {
|
|
27
|
+
readonly port: MessagePort
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.port = __geaAudioWorkletProcessorPort()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
abstract process(
|
|
34
|
+
inputs: Float32Array[][],
|
|
35
|
+
outputs: Float32Array[][],
|
|
36
|
+
parameters: Record<string, Float32Array>
|
|
37
|
+
): boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The native module builder validates the concrete registration constructor.
|
|
41
|
+
// AudioParam descriptors are explicitly unsupported, before code generation.
|
|
42
|
+
export function registerProcessor(
|
|
43
|
+
name: string,
|
|
44
|
+
constructor: new () => AudioWorkletProcessor
|
|
45
|
+
): void {
|
|
46
|
+
__geaRegisterAudioWorkletProcessor(
|
|
47
|
+
name,
|
|
48
|
+
() => new constructor(),
|
|
49
|
+
(processor, inputs, outputs) => processor.process(inputs, outputs, {})
|
|
50
|
+
)
|
|
51
|
+
}
|
package/runtime-surface.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from './runtime/compiler'
|
|
2
|
+
export type * from './workers'
|
|
3
|
+
export type * from './audio-worklet'
|
|
4
|
+
export { flushAudioWorkletOutput } from './runtime/audio-worklet'
|
|
2
5
|
export * from './runtime/host'
|
|
3
6
|
export * from './runtime/ble'
|
|
4
7
|
export * from './runtime/images'
|
|
@@ -109,6 +112,10 @@ export type {
|
|
|
109
112
|
GeolocationPosition,
|
|
110
113
|
GeolocationPositionError,
|
|
111
114
|
HTMLAudioElement,
|
|
115
|
+
PcmAudioStream,
|
|
116
|
+
PcmAudioStreamConstructor,
|
|
117
|
+
HTMLVideoElement,
|
|
118
|
+
VideoProps,
|
|
112
119
|
HttpModule,
|
|
113
120
|
HttpRequestHandler,
|
|
114
121
|
HttpServer,
|
|
@@ -8,6 +8,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
8
8
|
import { inlineJsonImports, transformGeaEmbeddedCompatSource } from './gea-embedded-compat-transform.mjs'
|
|
9
9
|
import { normalizeEmbeddedJsxOptions, shouldIgnoreCompatStagingDirectory } from './gea-embedded-compat-staging.mjs'
|
|
10
10
|
import { dotEnvDefines, inlineProcessEnv } from './dotenv-defines.mjs'
|
|
11
|
+
import { collectWorkerModuleSources, writeWorkerRegistry, validateWorkletRegistrations } from './gea-worker-modules.mjs'
|
|
11
12
|
import { resolveCompilerRuntimeEntry } from './gea-native-style-plugin.mjs'
|
|
12
13
|
import {
|
|
13
14
|
MODULE_HINT_SCOPE,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
restoreModuleGraphElAnnotations,
|
|
21
22
|
} from './gea-bundle-type-hints.mjs'
|
|
22
23
|
|
|
24
|
+
const nativeModules = new Map()
|
|
25
|
+
let nativeModuleSources = new Map()
|
|
23
26
|
const args = process.argv.slice(2)
|
|
24
27
|
const requireFromLib = createRequire(new URL('../package.json', import.meta.url))
|
|
25
28
|
const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -246,7 +249,11 @@ function copyCompatSourceTree(srcDir, dstDir, envDefines = {}) {
|
|
|
246
249
|
// (the fully native store/record lowering path). Then bake in .env vars
|
|
247
250
|
// (process.env.<KEY> -> literal) so the module-graph snapshots geatsc
|
|
248
251
|
// compiles carry the value — Vite's `define` only rewrites the bundle.
|
|
249
|
-
|
|
252
|
+
const native = nativeModuleSources.get(src) ?? { code, modules: [] }
|
|
253
|
+
for (const module of native.modules) {
|
|
254
|
+
nativeModules.set(`${module.kind}:${module.url}`, { ...module, stagedEntry: path.resolve(path.dirname(dst), path.relative(path.dirname(src), module.entry)) })
|
|
255
|
+
}
|
|
256
|
+
fs.writeFileSync(dst, inlineProcessEnv(transformGeaEmbeddedCompatSource(inlineJsonImports(native.code, src), src), envDefines))
|
|
250
257
|
} else {
|
|
251
258
|
fs.copyFileSync(src, dst)
|
|
252
259
|
if (path.extname(entry.name) === '.css') copyCssUrlAssets(src, dst)
|
|
@@ -304,7 +311,7 @@ function linkCompatNodeModules(appDir, stagedAppDir, compatSrcDir) {
|
|
|
304
311
|
// staging its containing directory wholesale (which also brings the css/asset
|
|
305
312
|
// files discoverLocalImport skips).
|
|
306
313
|
function escapedCompatImportDirs(appDir, entry) {
|
|
307
|
-
const escaped = collectReachableSourceFiles(appDir, entry).filter((file) => path.relative(appDir, file).startsWith('..'))
|
|
314
|
+
const escaped = [...new Set([...collectReachableSourceFiles(appDir, entry), ...nativeModuleSources.keys()])].filter((file) => path.relative(appDir, file).startsWith('..'))
|
|
308
315
|
const dirs = [...new Set(escaped.map((file) => path.dirname(file)))].sort()
|
|
309
316
|
return dirs.filter((dir, index) => !dirs.slice(0, index).some((outer) => outer !== dir && !path.relative(outer, dir).startsWith('..')))
|
|
310
317
|
}
|
|
@@ -4393,6 +4400,8 @@ let stagedEntryFile = null
|
|
|
4393
4400
|
let fontSourceDir = appDir
|
|
4394
4401
|
|
|
4395
4402
|
if (geaEmbeddedCompat) {
|
|
4403
|
+
nativeModuleSources = collectWorkerModuleSources(path.resolve(appDir, entry),
|
|
4404
|
+
(file) => collectReachableSourceFiles(path.dirname(file), path.basename(file)))
|
|
4396
4405
|
const compatSrcDir = path.join(outDir, 'gea-embedded-compat-src')
|
|
4397
4406
|
fs.rmSync(compatSrcDir, { recursive: true, force: true })
|
|
4398
4407
|
// Relative imports may escape the app dir (examples/shared components).
|
|
@@ -4587,7 +4596,46 @@ if (hasFlag('--gea-ir-backend') || hasFlag('--gea-replace-renderers')) {
|
|
|
4587
4596
|
// measurement scripts are always run with this
|
|
4588
4597
|
// limit; the build has to state the same one, not depend on the caller's
|
|
4589
4598
|
// NODE_OPTIONS.
|
|
4590
|
-
|
|
4599
|
+
// Native interface call graphs can exceed V8's default ~1 MiB stack during
|
|
4600
|
+
// closure analysis; keep the compiler stack below the host's 8 MiB limit.
|
|
4601
|
+
run('node', ['--stack_size=8192', '--max-old-space-size=32768', ...geatscArgs])
|
|
4602
|
+
|
|
4603
|
+
if (nativeModules.size) {
|
|
4604
|
+
const nativeSources = []
|
|
4605
|
+
for (const module of nativeModules.values()) {
|
|
4606
|
+
if (!fs.existsSync(module.stagedEntry)) fail(`missing ${module.kind} module: ${module.entry}`)
|
|
4607
|
+
if (module.kind === 'worklet') {
|
|
4608
|
+
const implementation = path.join(coreRoot, 'runtime/audio-worklet.ts')
|
|
4609
|
+
const bindings = `import { AudioWorkletProcessor, registerProcessor } from ${JSON.stringify(implementation)}\n`
|
|
4610
|
+
const source = fs.readFileSync(module.stagedEntry, 'utf8')
|
|
4611
|
+
fs.writeFileSync(module.stagedEntry, bindings + source)
|
|
4612
|
+
const compilerRequire = createRequire(pathToFileURL(geatscBin))
|
|
4613
|
+
validateWorkletRegistrations(compilerRequire('typescript'), module.stagedEntry, implementation)
|
|
4614
|
+
}
|
|
4615
|
+
const moduleOut = path.join(outDir, `${module.kind}-${module.id}`)
|
|
4616
|
+
const moduleArgs = [
|
|
4617
|
+
geatscBin, 'compile', module.stagedEntry, '--out-dir', moduleOut,
|
|
4618
|
+
'--target', 'cpp', '--plugin', geaPluginSpecifier,
|
|
4619
|
+
'--entry-symbol', module.symbol, '--isolate-symbols', '--realm-storage',
|
|
4620
|
+
'--plugin-option', `gea.microtasks-namespace=gea::framework::app::generated::gea_${module.kind}_${module.id}`,
|
|
4621
|
+
]
|
|
4622
|
+
if (cppBoard) moduleArgs.push('--cpp-board', cppBoard)
|
|
4623
|
+
if (cxxStandard) moduleArgs.push('--cxx-standard', cxxStandard)
|
|
4624
|
+
run('node', ['--stack_size=8192', '--max-old-space-size=32768', ...moduleArgs])
|
|
4625
|
+
const list = fs.readFileSync(path.join(moduleOut, 'geatsc-sources.txt'), 'utf8').split(/\r?\n/).filter(Boolean)
|
|
4626
|
+
nativeSources.push(...list.filter((source) => path.basename(source) !== 'gea_runtime_builtins.cpp'))
|
|
4627
|
+
}
|
|
4628
|
+
const listPath = path.join(outDir, 'geatsc-sources.txt')
|
|
4629
|
+
const sources = fs.readFileSync(listPath, 'utf8').split(/\r?\n/).filter(Boolean)
|
|
4630
|
+
const registry = path.join(outDir, 'gea_worker_modules.cpp')
|
|
4631
|
+
writeWorkerRegistry([...nativeModules.values()], registry)
|
|
4632
|
+
// The app entry and its native registrations must be in one archive member:
|
|
4633
|
+
// constructor-only members are not extracted by ordinary static linking.
|
|
4634
|
+
const nativeEntry = path.join(outDir, 'gea_native_entry.cpp')
|
|
4635
|
+
fs.writeFileSync(nativeEntry, `#include ${JSON.stringify(sources[0])}\n#include ${JSON.stringify(registry)}\n`)
|
|
4636
|
+
fs.writeFileSync(listPath, [nativeEntry, ...sources.slice(1), ...nativeSources].join('\n') + '\n')
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4591
4639
|
|
|
4592
4640
|
process.stdout.write(`gea-vite-bundle=${bundlePath}\n`)
|
|
4593
4641
|
if (moduleGraphOutDir) process.stdout.write(`module-graph=${path.join(moduleGraphOutDir, 'gea-module-graph.json')}\n`)
|
|
@@ -156,8 +156,16 @@ export function discoverLocalImport(source, srcId, localImports) {
|
|
|
156
156
|
if (source.endsWith('.css')) return
|
|
157
157
|
const base = resolveLocalImportBase(source, srcId)
|
|
158
158
|
if (!base) return
|
|
159
|
+
// TypeScript's ESM imports name the emitted .js/.mjs/.cjs file while the
|
|
160
|
+
// source tree contains .ts/.mts/.cts. Follow those edges before staging.
|
|
161
|
+
const extension = path.extname(base)
|
|
162
|
+
const sourceExtensions = extension === '.js' ? ['.ts', '.tsx', '.d.ts']
|
|
163
|
+
: extension === '.mjs' ? ['.mts', '.d.mts']
|
|
164
|
+
: extension === '.cjs' ? ['.cts', '.d.cts'] : []
|
|
165
|
+
const stem = base.slice(0, base.length - extension.length)
|
|
159
166
|
for (const candidate of [
|
|
160
167
|
base,
|
|
168
|
+
...sourceExtensions.map((extension) => stem + extension),
|
|
161
169
|
base + '.tsx',
|
|
162
170
|
base + '.ts',
|
|
163
171
|
base + '.jsx',
|
|
@@ -177,7 +185,7 @@ export function discoverLocalImport(source, srcId, localImports) {
|
|
|
177
185
|
// non-ASCII byte (`wOFF` header → "Unexpected character" at column 4).
|
|
178
186
|
// Assets aren't parseable modules; skip them (the added-extension
|
|
179
187
|
// candidates below are always JS/TS, so a genuine module still resolves).
|
|
180
|
-
if (!/\.(?:
|
|
188
|
+
if (!/\.(?:[cm]?[jt]s|jsx|tsx)$/i.test(candidate)) break
|
|
181
189
|
localImports.push(candidate)
|
|
182
190
|
break
|
|
183
191
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
// One identity for Vite's native module graph and the firmware lookup table.
|
|
6
|
+
export function describeStaticAsset(source) {
|
|
7
|
+
const contents = fs.readFileSync(source)
|
|
8
|
+
const sha256 = createHash('sha256').update(contents).digest('hex')
|
|
9
|
+
return {
|
|
10
|
+
source,
|
|
11
|
+
bytes: contents.length,
|
|
12
|
+
sha256,
|
|
13
|
+
url: `gea-asset://sha256/${sha256}/${encodeURIComponent(path.basename(source))}`,
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeStaticAsset } from './gea-static-asset.mjs'
|
|
1
2
|
import crypto from 'node:crypto'
|
|
2
3
|
import fs from 'node:fs'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -424,20 +425,6 @@ function entryReachableModuleIds(pluginContext, rawIdsByNormalizedId, allIds) {
|
|
|
424
425
|
return allIds.filter((id) => reachable.has(id))
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
function describeStaticAsset(source) {
|
|
428
|
-
const contents = fs.readFileSync(source)
|
|
429
|
-
const sha256 = crypto.createHash('sha256').update(contents).digest('hex')
|
|
430
|
-
// The hash makes the logical URL collision-safe and stable for unchanged
|
|
431
|
-
// bytes. The basename remains available to native bundle adapters, while the
|
|
432
|
-
// manifest is the authoritative URL -> source mapping.
|
|
433
|
-
const basename = encodeURIComponent(path.basename(source))
|
|
434
|
-
return {
|
|
435
|
-
source,
|
|
436
|
-
bytes: contents.length,
|
|
437
|
-
sha256,
|
|
438
|
-
url: `gea-asset://sha256/${sha256}/${basename}`,
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
428
|
|
|
442
429
|
async function captureResolvedImports(pluginContext, state, id, code) {
|
|
443
430
|
const normalized = normalizeModuleId(id)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(new URL("../package.json", import.meta.url));
|
|
8
|
+
const { parse } = require("@babel/parser");
|
|
9
|
+
const traversal = require("@babel/traverse");
|
|
10
|
+
const traverse = traversal.default ?? traversal;
|
|
11
|
+
|
|
12
|
+
/** Follow ordinary imports and native module edges without inspecting unused tests. */
|
|
13
|
+
export function collectWorkerModuleSources(
|
|
14
|
+
entry,
|
|
15
|
+
importedFiles,
|
|
16
|
+
readSource = (file) => fs.readFileSync(file, "utf8"),
|
|
17
|
+
) {
|
|
18
|
+
const pending = [entry];
|
|
19
|
+
const visited = new Set();
|
|
20
|
+
const sources = new Map();
|
|
21
|
+
while (pending.length) {
|
|
22
|
+
const file = pending.pop();
|
|
23
|
+
if (visited.has(file)) continue;
|
|
24
|
+
visited.add(file);
|
|
25
|
+
pending.push(...importedFiles(file));
|
|
26
|
+
if (!/\.[cm]?[jt]sx?$/.test(file)) continue;
|
|
27
|
+
const resolved = resolveWorkerModules(readSource(file), file);
|
|
28
|
+
sources.set(file, resolved);
|
|
29
|
+
pending.push(...resolved.modules.map((module) => module.entry));
|
|
30
|
+
}
|
|
31
|
+
return sources;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Native equivalent of a bundler's static worker URL resolution. */
|
|
35
|
+
export function resolveWorkerModules(
|
|
36
|
+
code,
|
|
37
|
+
fileName,
|
|
38
|
+
resolveModule = (specifier) =>
|
|
39
|
+
path.resolve(path.dirname(fileName), specifier),
|
|
40
|
+
) {
|
|
41
|
+
const source = parse(code, {
|
|
42
|
+
sourceType: "module",
|
|
43
|
+
plugins: ["typescript", "jsx"],
|
|
44
|
+
});
|
|
45
|
+
const modules = [];
|
|
46
|
+
const edits = [];
|
|
47
|
+
const staticURL = (argument) => {
|
|
48
|
+
if (argument?.type === "StringLiteral") return argument.value;
|
|
49
|
+
if (
|
|
50
|
+
argument?.type !== "NewExpression" ||
|
|
51
|
+
argument.callee.type !== "Identifier" ||
|
|
52
|
+
argument.callee.name !== "URL"
|
|
53
|
+
)
|
|
54
|
+
return null;
|
|
55
|
+
const [url, base] = argument.arguments;
|
|
56
|
+
if (
|
|
57
|
+
url?.type !== "StringLiteral" ||
|
|
58
|
+
base?.type !== "MemberExpression" ||
|
|
59
|
+
base.computed ||
|
|
60
|
+
base.property.name !== "url"
|
|
61
|
+
)
|
|
62
|
+
return null;
|
|
63
|
+
if (
|
|
64
|
+
base.object.type !== "MetaProperty" ||
|
|
65
|
+
base.object.meta.name !== "import" ||
|
|
66
|
+
base.object.property.name !== "meta"
|
|
67
|
+
)
|
|
68
|
+
return null;
|
|
69
|
+
return url.value;
|
|
70
|
+
};
|
|
71
|
+
const visit = (astPath) => {
|
|
72
|
+
const node = astPath.node;
|
|
73
|
+
const binding = astPath.scope.getBinding("Worker");
|
|
74
|
+
const typeOnlyBinding =
|
|
75
|
+
binding &&
|
|
76
|
+
(binding.path.node.importKind === "type" ||
|
|
77
|
+
binding.path.parent.importKind === "type");
|
|
78
|
+
const worker =
|
|
79
|
+
node.type === "NewExpression" &&
|
|
80
|
+
node.callee.type === "Identifier" &&
|
|
81
|
+
node.callee.name === "Worker" &&
|
|
82
|
+
(!binding || typeOnlyBinding);
|
|
83
|
+
const call = node.callee;
|
|
84
|
+
const worklet =
|
|
85
|
+
node.type === "CallExpression" &&
|
|
86
|
+
call.type === "MemberExpression" &&
|
|
87
|
+
!call.computed &&
|
|
88
|
+
call.property.name === "addModule" &&
|
|
89
|
+
call.object.type === "MemberExpression" &&
|
|
90
|
+
!call.object.computed &&
|
|
91
|
+
call.object.property.name === "audioWorklet";
|
|
92
|
+
if (!worker && !worklet) return;
|
|
93
|
+
const argument = node.arguments[0];
|
|
94
|
+
const specifier = staticURL(argument);
|
|
95
|
+
if (specifier === null)
|
|
96
|
+
throw new Error(
|
|
97
|
+
`${fileName}: native worker/worklet module URL must be statically resolvable`,
|
|
98
|
+
);
|
|
99
|
+
if (!specifier.startsWith("."))
|
|
100
|
+
throw new Error(
|
|
101
|
+
`${fileName}: native worker/worklet modules must use a relative module URL`,
|
|
102
|
+
);
|
|
103
|
+
const entry = resolveModule(specifier);
|
|
104
|
+
const url = pathToFileURL(entry).href;
|
|
105
|
+
const kind = worker ? "worker" : "worklet";
|
|
106
|
+
const id = crypto
|
|
107
|
+
.createHash("sha256")
|
|
108
|
+
.update(`${kind}:${url}`)
|
|
109
|
+
.digest("hex")
|
|
110
|
+
.slice(0, 16);
|
|
111
|
+
modules.push({
|
|
112
|
+
kind,
|
|
113
|
+
entry,
|
|
114
|
+
url,
|
|
115
|
+
symbol: `gea_${kind}_${id}_top_level`,
|
|
116
|
+
id,
|
|
117
|
+
});
|
|
118
|
+
edits.push({
|
|
119
|
+
start: argument.start,
|
|
120
|
+
end: argument.end,
|
|
121
|
+
text: JSON.stringify(url),
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
traverse(source, { NewExpression: visit, CallExpression: visit });
|
|
125
|
+
let transformed = code;
|
|
126
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
127
|
+
transformed =
|
|
128
|
+
transformed.slice(0, edit.start) +
|
|
129
|
+
edit.text +
|
|
130
|
+
transformed.slice(edit.end);
|
|
131
|
+
return { code: transformed, modules };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function writeWorkerRegistry(modules, output) {
|
|
135
|
+
const includes = ['#include "host/worker.h"'];
|
|
136
|
+
if (modules.some((module) => module.kind === "worklet"))
|
|
137
|
+
includes.push('#include "host/audio_worklet.h"');
|
|
138
|
+
const declarations = modules.map((module) => `void ${module.symbol}();`);
|
|
139
|
+
const registrations = modules.map((module) => {
|
|
140
|
+
const target = module.kind === "worker" ? "workers" : "audio_worklet";
|
|
141
|
+
return ` gea::host::${target}::registerModule(${JSON.stringify(module.url)}, ${module.symbol});`;
|
|
142
|
+
});
|
|
143
|
+
const content = `${includes.join("\n")}\n${declarations.join("\n")}\nnamespace {\nstruct RegisterWorkerModules {\nRegisterWorkerModules() {\n${registrations.join("\n")}\n}\n};\nRegisterWorkerModules registeredWorkerModules;\n}\n`;
|
|
144
|
+
if (!fs.existsSync(output) || fs.readFileSync(output, "utf8") !== content)
|
|
145
|
+
fs.writeFileSync(output, content);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Validate the supported native registration subset using symbol/type identity. */
|
|
149
|
+
export function validateWorkletRegistrations(ts, entry, implementation) {
|
|
150
|
+
const program = ts.createProgram([entry], {
|
|
151
|
+
target: ts.ScriptTarget.ESNext,
|
|
152
|
+
module: ts.ModuleKind.ESNext,
|
|
153
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
154
|
+
allowJs: true,
|
|
155
|
+
skipLibCheck: true,
|
|
156
|
+
noEmit: true,
|
|
157
|
+
});
|
|
158
|
+
const checker = program.getTypeChecker();
|
|
159
|
+
const platformFile = fs.realpathSync(implementation);
|
|
160
|
+
const fail = (node, message) => {
|
|
161
|
+
const source = node.getSourceFile();
|
|
162
|
+
const location = source.getLineAndCharacterOfPosition(node.getStart());
|
|
163
|
+
throw new Error(`${source.fileName}:${location.line + 1}: ${message}`);
|
|
164
|
+
};
|
|
165
|
+
const visit = (node) => {
|
|
166
|
+
if (ts.isCallExpression(node)) {
|
|
167
|
+
const declaration = checker.getResolvedSignature(node)?.declaration;
|
|
168
|
+
if (
|
|
169
|
+
declaration &&
|
|
170
|
+
fs.realpathSync(declaration.getSourceFile().fileName) === platformFile &&
|
|
171
|
+
declaration.name?.getText() === "registerProcessor"
|
|
172
|
+
) {
|
|
173
|
+
let constructor = node.arguments[1];
|
|
174
|
+
if (!constructor) fail(node, "registerProcessor requires a processor class");
|
|
175
|
+
while (ts.isParenthesizedExpression(constructor) || ts.isAsExpression(constructor) || ts.isTypeAssertionExpression(constructor))
|
|
176
|
+
constructor = constructor.expression;
|
|
177
|
+
let symbol = checker.getSymbolAtLocation(constructor);
|
|
178
|
+
if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
179
|
+
const classDeclaration = ts.isClassExpression(constructor) ? constructor : symbol?.valueDeclaration;
|
|
180
|
+
if (!classDeclaration || (!ts.isClassDeclaration(classDeclaration) && !ts.isClassExpression(classDeclaration)))
|
|
181
|
+
fail(node, "Native registerProcessor requires a directly named class constructor; indirect constructor values are unsupported");
|
|
182
|
+
const type = checker.getTypeAtLocation(constructor);
|
|
183
|
+
if (type.getProperty("parameterDescriptors"))
|
|
184
|
+
fail(node, "AudioParam parameterDescriptors are not supported by this native AudioWorklet host");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
ts.forEachChild(node, visit);
|
|
188
|
+
};
|
|
189
|
+
for (const source of program.getSourceFiles()) {
|
|
190
|
+
if (!source.isDeclarationFile && fs.realpathSync(source.fileName) !== platformFile) visit(source);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// shared binary) without duplicate-definition errors when they share
|
|
23
23
|
// an asset filename; the linker keeps one copy.
|
|
24
24
|
|
|
25
|
+
import { describeStaticAsset } from './gea-static-asset.mjs'
|
|
25
26
|
import fs from 'node:fs'
|
|
26
27
|
import path from 'node:path'
|
|
27
28
|
import process from 'node:process'
|
|
@@ -276,7 +277,7 @@ if (assets.length === 0) {
|
|
|
276
277
|
for (const file of assets) {
|
|
277
278
|
const rel = relPath(file)
|
|
278
279
|
const sym = assetSymbol(rel)
|
|
279
|
-
const aliases = [rel]
|
|
280
|
+
const aliases = [rel, describeStaticAsset(file).url]
|
|
280
281
|
if (rel.startsWith('public/')) aliases.push(rel.slice('public/'.length))
|
|
281
282
|
const condition = aliases
|
|
282
283
|
.map((alias) => `(std::strcmp(path, ${JSON.stringify(alias)}) == 0 || (path[0] == '/' && std::strcmp(path + 1, ${JSON.stringify(alias)}) == 0))`)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Standard binary base64 APIs, with the default base64 alphabet and padding. */
|
|
2
|
+
declare global {
|
|
3
|
+
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {
|
|
4
|
+
toBase64(): string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Uint8ArrayConstructor {
|
|
8
|
+
/** Decode using the standard default (loose) final-chunk handling. */
|
|
9
|
+
fromBase64(string: string): Uint8Array<ArrayBuffer>
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {}
|
package/workers.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Web worker APIs implemented by the native host. */
|
|
2
|
+
export type WorkerMessage = string | ArrayBuffer
|
|
3
|
+
|
|
4
|
+
export interface MessageEvent<T = WorkerMessage> {
|
|
5
|
+
readonly data: T
|
|
6
|
+
readonly ports: MessagePort[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WorkerErrorEvent {
|
|
10
|
+
readonly message: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MessagePort {
|
|
14
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
15
|
+
postMessage(message: WorkerMessage, transfer?: (ArrayBuffer | MessagePort)[]): void
|
|
16
|
+
start(): void
|
|
17
|
+
close(): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MessageChannel {
|
|
21
|
+
readonly port1: MessagePort
|
|
22
|
+
readonly port2: MessagePort
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MessageChannelConstructor {
|
|
26
|
+
new (): MessageChannel
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkerOptions {
|
|
30
|
+
type?: 'classic' | 'module'
|
|
31
|
+
name?: string
|
|
32
|
+
credentials?: 'omit' | 'same-origin' | 'include'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Worker {
|
|
36
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
37
|
+
onerror: ((event: WorkerErrorEvent) => void) | null
|
|
38
|
+
postMessage(message: WorkerMessage, transfer?: (ArrayBuffer | MessagePort)[]): void
|
|
39
|
+
terminate(): void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WorkerConstructor {
|
|
43
|
+
new (scriptURL: string | URL, options?: WorkerOptions): Worker
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DedicatedWorkerGlobalScope {
|
|
47
|
+
onmessage: ((event: MessageEvent) => void) | null
|
|
48
|
+
postMessage(message: WorkerMessage, transfer?: (ArrayBuffer | MessagePort)[]): void
|
|
49
|
+
close(): void
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
declare global {
|
|
53
|
+
interface ImportMeta {
|
|
54
|
+
readonly url: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface URL {
|
|
58
|
+
readonly href: string
|
|
59
|
+
toString(): string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const URL: { new (url: string | URL, base?: string | URL): URL }
|
|
63
|
+
const Worker: WorkerConstructor
|
|
64
|
+
const MessageChannel: MessageChannelConstructor
|
|
65
|
+
const self: DedicatedWorkerGlobalScope
|
|
66
|
+
}
|