@napolab/texture-bridge-renderer 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/preview.html +167 -160
- package/dist/index.cjs +115 -2
- package/dist/index.d.cts +47 -1
- package/dist/index.d.mts +47 -1
- package/dist/index.mjs +114 -3
- package/package.json +3 -2
package/dist/assets/preview.html
CHANGED
|
@@ -1,66 +1,71 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html>
|
|
3
|
-
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Preview (GPU Zero-Copy)</title>
|
|
6
|
+
<style>
|
|
7
|
+
* {
|
|
8
|
+
margin: 0;
|
|
9
|
+
padding: 0;
|
|
10
|
+
}
|
|
11
|
+
body {
|
|
12
|
+
background: #111;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
height: 100vh;
|
|
18
|
+
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
19
|
+
}
|
|
20
|
+
#info {
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: 10px;
|
|
23
|
+
left: 10px;
|
|
24
|
+
color: #0f0;
|
|
25
|
+
font-family: "SF Mono", Menlo, monospace;
|
|
26
|
+
font-size: 12px;
|
|
27
|
+
background: rgba(0, 0, 0, 0.7);
|
|
28
|
+
padding: 8px 12px;
|
|
29
|
+
border-radius: 4px;
|
|
30
|
+
z-index: 100;
|
|
31
|
+
}
|
|
32
|
+
canvas {
|
|
33
|
+
max-width: 100%;
|
|
34
|
+
max-height: calc(100vh - 20px);
|
|
35
|
+
object-fit: contain;
|
|
36
|
+
background: #000;
|
|
37
|
+
}
|
|
38
|
+
.error {
|
|
39
|
+
color: #f44;
|
|
40
|
+
}
|
|
41
|
+
</style>
|
|
42
|
+
</head>
|
|
43
|
+
<body>
|
|
44
|
+
<div id="info">Initializing WebGPU...</div>
|
|
45
|
+
<canvas id="preview"></canvas>
|
|
46
|
+
|
|
47
|
+
<script type="module">
|
|
48
|
+
const params = new URLSearchParams(location.search);
|
|
49
|
+
const W = parseInt(params.get("w"), 10) || 1920;
|
|
50
|
+
const H = parseInt(params.get("h"), 10) || 1080;
|
|
51
|
+
|
|
52
|
+
const canvas = document.getElementById("preview");
|
|
53
|
+
canvas.width = W;
|
|
54
|
+
canvas.height = H;
|
|
55
|
+
|
|
56
|
+
const infoEl = document.getElementById("info");
|
|
57
|
+
|
|
58
|
+
let device = null;
|
|
59
|
+
let context = null;
|
|
60
|
+
let pipeline = null;
|
|
61
|
+
let sampler = null;
|
|
62
|
+
let bindGroupLayout = null;
|
|
63
|
+
|
|
64
|
+
let frameCount = 0;
|
|
65
|
+
let lastTime = performance.now();
|
|
66
|
+
let fps = 0;
|
|
67
|
+
|
|
68
|
+
const shaderCode = `
|
|
64
69
|
struct VertexOutput {
|
|
65
70
|
@builtin(position) position: vec4f,
|
|
66
71
|
@location(0) texCoord: vec2f,
|
|
@@ -94,121 +99,123 @@
|
|
|
94
99
|
}
|
|
95
100
|
`;
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const adapter = await navigator.gpu.requestAdapter();
|
|
103
|
-
if (!adapter) {
|
|
104
|
-
throw new Error('No GPU adapter found');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
device = await adapter.requestDevice();
|
|
108
|
-
context = canvas.getContext('webgpu');
|
|
109
|
-
|
|
110
|
-
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
111
|
-
context.configure({ device, format, alphaMode: 'opaque' });
|
|
112
|
-
|
|
113
|
-
const shaderModule = device.createShaderModule({ code: shaderCode });
|
|
114
|
-
|
|
115
|
-
sampler = device.createSampler({
|
|
116
|
-
magFilter: 'linear',
|
|
117
|
-
minFilter: 'linear',
|
|
118
|
-
});
|
|
102
|
+
async function initWebGPU() {
|
|
103
|
+
if (!navigator.gpu) {
|
|
104
|
+
throw new Error("WebGPU not supported");
|
|
105
|
+
}
|
|
119
106
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
],
|
|
125
|
-
});
|
|
107
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
108
|
+
if (!adapter) {
|
|
109
|
+
throw new Error("No GPU adapter found");
|
|
110
|
+
}
|
|
126
111
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
vertex: { module: shaderModule, entryPoint: 'vertexMain' },
|
|
130
|
-
fragment: {
|
|
131
|
-
module: shaderModule,
|
|
132
|
-
entryPoint: 'fragmentMain',
|
|
133
|
-
targets: [{ format }],
|
|
134
|
-
},
|
|
135
|
-
primitive: { topology: 'triangle-list' },
|
|
136
|
-
});
|
|
112
|
+
device = await adapter.requestDevice();
|
|
113
|
+
context = canvas.getContext("webgpu");
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
|
|
115
|
+
const format = navigator.gpu.getPreferredCanvasFormat();
|
|
116
|
+
context.configure({ device, format, alphaMode: "opaque" });
|
|
140
117
|
|
|
141
|
-
|
|
142
|
-
if (!device || !pipeline || !videoFrame) return;
|
|
118
|
+
const shaderModule = device.createShaderModule({ code: shaderCode });
|
|
143
119
|
|
|
144
|
-
|
|
145
|
-
|
|
120
|
+
sampler = device.createSampler({
|
|
121
|
+
magFilter: "linear",
|
|
122
|
+
minFilter: "linear",
|
|
123
|
+
});
|
|
146
124
|
|
|
147
|
-
|
|
148
|
-
layout: bindGroupLayout,
|
|
125
|
+
bindGroupLayout = device.createBindGroupLayout({
|
|
149
126
|
entries: [
|
|
150
|
-
{ binding: 0,
|
|
151
|
-
{ binding: 1,
|
|
127
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, externalTexture: {} },
|
|
128
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
|
|
152
129
|
],
|
|
153
130
|
});
|
|
154
131
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}],
|
|
132
|
+
pipeline = device.createRenderPipeline({
|
|
133
|
+
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
|
|
134
|
+
vertex: { module: shaderModule, entryPoint: "vertexMain" },
|
|
135
|
+
fragment: {
|
|
136
|
+
module: shaderModule,
|
|
137
|
+
entryPoint: "fragmentMain",
|
|
138
|
+
targets: [{ format }],
|
|
139
|
+
},
|
|
140
|
+
primitive: { topology: "triangle-list" },
|
|
165
141
|
});
|
|
166
142
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
renderPass.draw(3);
|
|
170
|
-
renderPass.end();
|
|
143
|
+
infoEl.textContent = "WebGPU ready, waiting for frames...";
|
|
144
|
+
}
|
|
171
145
|
|
|
172
|
-
|
|
146
|
+
function renderFrame(videoFrame) {
|
|
147
|
+
if (!device || !pipeline || !videoFrame) return;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const externalTexture = device.importExternalTexture({ source: videoFrame });
|
|
151
|
+
|
|
152
|
+
const bindGroup = device.createBindGroup({
|
|
153
|
+
layout: bindGroupLayout,
|
|
154
|
+
entries: [
|
|
155
|
+
{ binding: 0, resource: externalTexture },
|
|
156
|
+
{ binding: 1, resource: sampler },
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const commandEncoder = device.createCommandEncoder();
|
|
161
|
+
const textureView = context.getCurrentTexture().createView();
|
|
162
|
+
|
|
163
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
164
|
+
colorAttachments: [
|
|
165
|
+
{
|
|
166
|
+
view: textureView,
|
|
167
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
168
|
+
loadOp: "clear",
|
|
169
|
+
storeOp: "store",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
renderPass.setPipeline(pipeline);
|
|
175
|
+
renderPass.setBindGroup(0, bindGroup);
|
|
176
|
+
renderPass.draw(3);
|
|
177
|
+
renderPass.end();
|
|
178
|
+
|
|
179
|
+
device.queue.submit([commandEncoder.finish()]);
|
|
180
|
+
|
|
181
|
+
frameCount++;
|
|
182
|
+
const now = performance.now();
|
|
183
|
+
if (now - lastTime >= 1000) {
|
|
184
|
+
fps = (frameCount * 1000) / (now - lastTime);
|
|
185
|
+
frameCount = 0;
|
|
186
|
+
lastTime = now;
|
|
187
|
+
}
|
|
173
188
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
fps = frameCount * 1000 / (now - lastTime);
|
|
178
|
-
frameCount = 0;
|
|
179
|
-
lastTime = now;
|
|
189
|
+
infoEl.textContent = `GPU Zero-Copy | FPS: ${fps.toFixed(1)} | ${videoFrame.displayWidth}x${videoFrame.displayHeight}`;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error("[preview] render error:", err);
|
|
180
192
|
}
|
|
181
|
-
|
|
182
|
-
infoEl.textContent = `GPU Zero-Copy | FPS: ${fps.toFixed(1)} | ${videoFrame.displayWidth}x${videoFrame.displayHeight}`;
|
|
183
|
-
} catch (err) {
|
|
184
|
-
console.error('[preview] render error:', err);
|
|
185
193
|
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function main() {
|
|
189
|
-
try {
|
|
190
|
-
await initWebGPU();
|
|
191
|
-
|
|
192
|
-
window.electronAPI.onTextureFrame((imported) => {
|
|
193
|
-
try {
|
|
194
|
-
const videoFrame = imported.getVideoFrame();
|
|
195
|
-
renderFrame(videoFrame);
|
|
196
|
-
videoFrame.close();
|
|
197
|
-
imported.release();
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.error('[preview] texture error:', err);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
194
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
195
|
+
async function main() {
|
|
196
|
+
try {
|
|
197
|
+
await initWebGPU();
|
|
198
|
+
|
|
199
|
+
window.electronAPI.onTextureFrame((imported) => {
|
|
200
|
+
try {
|
|
201
|
+
const videoFrame = imported.getVideoFrame();
|
|
202
|
+
renderFrame(videoFrame);
|
|
203
|
+
videoFrame.close();
|
|
204
|
+
imported.release();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error("[preview] texture error:", err);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
window.electronAPI.previewReady();
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error("[preview] init error:", err);
|
|
213
|
+
infoEl.className = "error";
|
|
214
|
+
infoEl.textContent = `WebGPU Error: ${err.message}`;
|
|
215
|
+
}
|
|
208
216
|
}
|
|
209
|
-
}
|
|
210
217
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
</body>
|
|
218
|
+
main();
|
|
219
|
+
</script>
|
|
220
|
+
</body>
|
|
214
221
|
</html>
|
package/dist/index.cjs
CHANGED
|
@@ -121,6 +121,11 @@ var FpsCounter = class {
|
|
|
121
121
|
this.count = 0;
|
|
122
122
|
this.lastTime = Date.now();
|
|
123
123
|
}
|
|
124
|
+
/** Reset the counter, discarding any accumulated frames and time. */
|
|
125
|
+
reset() {
|
|
126
|
+
this.count = 0;
|
|
127
|
+
this.lastTime = Date.now();
|
|
128
|
+
}
|
|
124
129
|
/** Call on every frame. Returns FPS when 1 second has elapsed, otherwise null. */
|
|
125
130
|
tick() {
|
|
126
131
|
this.count++;
|
|
@@ -216,7 +221,7 @@ async function createTextureBridge(options) {
|
|
|
216
221
|
const bridge = new TextureBridgeImpl(renderWindow, sender, previewManager, options);
|
|
217
222
|
renderWindow.webContents.on("paint", (event) => {
|
|
218
223
|
const texture = event.texture;
|
|
219
|
-
if (!texture) return;
|
|
224
|
+
if (!texture?.textureInfo) return;
|
|
220
225
|
try {
|
|
221
226
|
(0, _napolab_texture_bridge_core.sendTextureFromPaintEvent)(bridge.sender, texture.textureInfo);
|
|
222
227
|
bridge.previewManager?.sendFrame(texture);
|
|
@@ -238,4 +243,112 @@ async function createTextureBridge(options) {
|
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
//#endregion
|
|
241
|
-
|
|
246
|
+
//#region src/receiver.ts
|
|
247
|
+
var TextureReceiverBridgeImpl = class extends events.EventEmitter {
|
|
248
|
+
constructor(receiver, pollIntervalMs) {
|
|
249
|
+
super();
|
|
250
|
+
this.fpsCounter = new FpsCounter();
|
|
251
|
+
this._disposed = false;
|
|
252
|
+
this._timer = null;
|
|
253
|
+
this.receiver = receiver;
|
|
254
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
255
|
+
}
|
|
256
|
+
get isDisposed() {
|
|
257
|
+
return this._disposed;
|
|
258
|
+
}
|
|
259
|
+
start() {
|
|
260
|
+
if (this._disposed || this._timer) return;
|
|
261
|
+
this.fpsCounter.reset();
|
|
262
|
+
this._timer = setInterval(() => this._poll(), this.pollIntervalMs);
|
|
263
|
+
}
|
|
264
|
+
stop() {
|
|
265
|
+
if (this._timer) {
|
|
266
|
+
clearInterval(this._timer);
|
|
267
|
+
this._timer = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
dispose() {
|
|
271
|
+
if (this._disposed) return;
|
|
272
|
+
this._disposed = true;
|
|
273
|
+
this.stop();
|
|
274
|
+
this.receiver.stop();
|
|
275
|
+
this.emit("disposed");
|
|
276
|
+
this.removeAllListeners();
|
|
277
|
+
}
|
|
278
|
+
_poll() {
|
|
279
|
+
if (this._disposed) return;
|
|
280
|
+
try {
|
|
281
|
+
if (!this.receiver.hasNewFrame()) return;
|
|
282
|
+
const frame = this.receiver.receiveFrame();
|
|
283
|
+
if (!frame) return;
|
|
284
|
+
this.emit("frame", frame);
|
|
285
|
+
const fps = this.fpsCounter.tick();
|
|
286
|
+
if (fps !== null) this.emit("fps", fps);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
289
|
+
this.emit("error", error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
function createTextureReceiver(options) {
|
|
294
|
+
const { senderName, appName, serverUuid, pollIntervalMs = 16 } = options;
|
|
295
|
+
return new TextureReceiverBridgeImpl(new _napolab_texture_bridge_core.TextureReceiver(senderName, appName, serverUuid), pollIntervalMs);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/discovery.ts
|
|
300
|
+
var SenderDiscovery = class extends events.EventEmitter {
|
|
301
|
+
constructor(..._args) {
|
|
302
|
+
super(..._args);
|
|
303
|
+
this._senders = [];
|
|
304
|
+
this._timer = null;
|
|
305
|
+
this._disposed = false;
|
|
306
|
+
}
|
|
307
|
+
get isDisposed() {
|
|
308
|
+
return this._disposed;
|
|
309
|
+
}
|
|
310
|
+
start(intervalMs = 1e3) {
|
|
311
|
+
if (this._disposed || this._timer) return;
|
|
312
|
+
this._timer = setInterval(() => this._refresh(), intervalMs);
|
|
313
|
+
}
|
|
314
|
+
stop() {
|
|
315
|
+
if (this._timer) {
|
|
316
|
+
clearInterval(this._timer);
|
|
317
|
+
this._timer = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
getSenders() {
|
|
321
|
+
return [...this._senders];
|
|
322
|
+
}
|
|
323
|
+
dispose() {
|
|
324
|
+
if (this._disposed) return;
|
|
325
|
+
this._disposed = true;
|
|
326
|
+
this.stop();
|
|
327
|
+
this.removeAllListeners();
|
|
328
|
+
}
|
|
329
|
+
_refresh() {
|
|
330
|
+
if (this._disposed) return;
|
|
331
|
+
try {
|
|
332
|
+
const current = (0, _napolab_texture_bridge_core.listSenders)();
|
|
333
|
+
const prev = this._senders;
|
|
334
|
+
const added = current.filter((c) => !prev.some((p) => this._isSame(c, p)));
|
|
335
|
+
const removed = prev.filter((p) => !current.some((c) => this._isSame(c, p)));
|
|
336
|
+
this._senders = current;
|
|
337
|
+
if (added.length > 0) this.emit("added", added);
|
|
338
|
+
if (removed.length > 0) this.emit("removed", removed);
|
|
339
|
+
if (added.length > 0 || removed.length > 0) this.emit("updated", current);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
342
|
+
this.emit("error", error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
_isSame(a, b) {
|
|
346
|
+
if (a.uuid && b.uuid) return a.uuid === b.uuid;
|
|
347
|
+
return a.name === b.name && a.appName === b.appName;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
//#endregion
|
|
352
|
+
exports.SenderDiscovery = SenderDiscovery;
|
|
353
|
+
exports.createTextureBridge = createTextureBridge;
|
|
354
|
+
exports.createTextureReceiver = createTextureReceiver;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BrowserWindow } from "electron";
|
|
2
|
+
import { ReceivedFrame, SenderInfo } from "@napolab/texture-bridge-core";
|
|
3
|
+
import { EventEmitter } from "events";
|
|
2
4
|
|
|
3
5
|
//#region src/types.d.ts
|
|
4
6
|
/** Options for the preview window */
|
|
@@ -63,4 +65,48 @@ interface TextureBridge {
|
|
|
63
65
|
*/
|
|
64
66
|
declare function createTextureBridge(options: TextureBridgeOptions): Promise<TextureBridge>;
|
|
65
67
|
//#endregion
|
|
66
|
-
|
|
68
|
+
//#region src/receiver.d.ts
|
|
69
|
+
interface TextureReceiverBridgeOptions {
|
|
70
|
+
senderName: string;
|
|
71
|
+
appName?: string;
|
|
72
|
+
serverUuid?: string;
|
|
73
|
+
pollIntervalMs?: number;
|
|
74
|
+
}
|
|
75
|
+
interface ReceiverBridgeEvents {
|
|
76
|
+
frame: [frame: ReceivedFrame];
|
|
77
|
+
fps: [fps: number];
|
|
78
|
+
error: [error: Error];
|
|
79
|
+
disposed: [];
|
|
80
|
+
}
|
|
81
|
+
interface TextureReceiverBridge {
|
|
82
|
+
on<K extends keyof ReceiverBridgeEvents>(event: K, listener: (...args: ReceiverBridgeEvents[K]) => void): this;
|
|
83
|
+
off<K extends keyof ReceiverBridgeEvents>(event: K, listener: (...args: ReceiverBridgeEvents[K]) => void): this;
|
|
84
|
+
once<K extends keyof ReceiverBridgeEvents>(event: K, listener: (...args: ReceiverBridgeEvents[K]) => void): this;
|
|
85
|
+
start(): void;
|
|
86
|
+
stop(): void;
|
|
87
|
+
dispose(): void;
|
|
88
|
+
readonly isDisposed: boolean;
|
|
89
|
+
}
|
|
90
|
+
declare function createTextureReceiver(options: TextureReceiverBridgeOptions): TextureReceiverBridge;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/discovery.d.ts
|
|
93
|
+
interface SenderDiscoveryEvents {
|
|
94
|
+
updated: [senders: SenderInfo[]];
|
|
95
|
+
added: [senders: SenderInfo[]];
|
|
96
|
+
removed: [senders: SenderInfo[]];
|
|
97
|
+
error: [error: Error];
|
|
98
|
+
}
|
|
99
|
+
declare class SenderDiscovery extends EventEmitter {
|
|
100
|
+
private _senders;
|
|
101
|
+
private _timer;
|
|
102
|
+
private _disposed;
|
|
103
|
+
get isDisposed(): boolean;
|
|
104
|
+
start(intervalMs?: number): void;
|
|
105
|
+
stop(): void;
|
|
106
|
+
getSenders(): SenderInfo[];
|
|
107
|
+
dispose(): void;
|
|
108
|
+
private _refresh;
|
|
109
|
+
private _isSame;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { type BridgeEvents, type PreviewOptions, type ReceiverBridgeEvents, SenderDiscovery, type SenderDiscoveryEvents, type TextureBridge, type TextureBridgeOptions, type TextureReceiverBridge, type TextureReceiverBridgeOptions, createTextureBridge, createTextureReceiver };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
1
2
|
import { BrowserWindow } from "electron";
|
|
3
|
+
import { ReceivedFrame, SenderInfo } from "@napolab/texture-bridge-core";
|
|
2
4
|
|
|
3
5
|
//#region src/types.d.ts
|
|
4
6
|
/** Options for the preview window */
|
|
@@ -63,4 +65,48 @@ interface TextureBridge {
|
|
|
63
65
|
*/
|
|
64
66
|
declare function createTextureBridge(options: TextureBridgeOptions): Promise<TextureBridge>;
|
|
65
67
|
//#endregion
|
|
66
|
-
|
|
68
|
+
//#region src/receiver.d.ts
|
|
69
|
+
interface TextureReceiverBridgeOptions {
|
|
70
|
+
senderName: string;
|
|
71
|
+
appName?: string;
|
|
72
|
+
serverUuid?: string;
|
|
73
|
+
pollIntervalMs?: number;
|
|
74
|
+
}
|
|
75
|
+
interface ReceiverBridgeEvents {
|
|
76
|
+
frame: [frame: ReceivedFrame];
|
|
77
|
+
fps: [fps: number];
|
|
78
|
+
error: [error: Error];
|
|
79
|
+
disposed: [];
|
|
80
|
+
}
|
|
81
|
+
interface TextureReceiverBridge {
|
|
82
|
+
on<K extends keyof ReceiverBridgeEvents>(event: K, listener: (...args: ReceiverBridgeEvents[K]) => void): this;
|
|
83
|
+
off<K extends keyof ReceiverBridgeEvents>(event: K, listener: (...args: ReceiverBridgeEvents[K]) => void): this;
|
|
84
|
+
once<K extends keyof ReceiverBridgeEvents>(event: K, listener: (...args: ReceiverBridgeEvents[K]) => void): this;
|
|
85
|
+
start(): void;
|
|
86
|
+
stop(): void;
|
|
87
|
+
dispose(): void;
|
|
88
|
+
readonly isDisposed: boolean;
|
|
89
|
+
}
|
|
90
|
+
declare function createTextureReceiver(options: TextureReceiverBridgeOptions): TextureReceiverBridge;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/discovery.d.ts
|
|
93
|
+
interface SenderDiscoveryEvents {
|
|
94
|
+
updated: [senders: SenderInfo[]];
|
|
95
|
+
added: [senders: SenderInfo[]];
|
|
96
|
+
removed: [senders: SenderInfo[]];
|
|
97
|
+
error: [error: Error];
|
|
98
|
+
}
|
|
99
|
+
declare class SenderDiscovery extends EventEmitter {
|
|
100
|
+
private _senders;
|
|
101
|
+
private _timer;
|
|
102
|
+
private _disposed;
|
|
103
|
+
get isDisposed(): boolean;
|
|
104
|
+
start(intervalMs?: number): void;
|
|
105
|
+
stop(): void;
|
|
106
|
+
getSenders(): SenderInfo[];
|
|
107
|
+
dispose(): void;
|
|
108
|
+
private _refresh;
|
|
109
|
+
private _isSame;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { type BridgeEvents, type PreviewOptions, type ReceiverBridgeEvents, SenderDiscovery, type SenderDiscoveryEvents, type TextureBridge, type TextureBridgeOptions, type TextureReceiverBridge, type TextureReceiverBridgeOptions, createTextureBridge, createTextureReceiver };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
2
|
import { BrowserWindow, app, ipcMain, sharedTexture } from "electron";
|
|
3
|
-
import { TextureSender, sendTextureFromPaintEvent } from "@napolab/texture-bridge-core";
|
|
3
|
+
import { TextureReceiver, TextureSender, listSenders, sendTextureFromPaintEvent } from "@napolab/texture-bridge-core";
|
|
4
4
|
import path from "path";
|
|
5
5
|
|
|
6
6
|
//#region src/preview-manager.ts
|
|
@@ -92,6 +92,11 @@ var FpsCounter = class {
|
|
|
92
92
|
this.count = 0;
|
|
93
93
|
this.lastTime = Date.now();
|
|
94
94
|
}
|
|
95
|
+
/** Reset the counter, discarding any accumulated frames and time. */
|
|
96
|
+
reset() {
|
|
97
|
+
this.count = 0;
|
|
98
|
+
this.lastTime = Date.now();
|
|
99
|
+
}
|
|
95
100
|
/** Call on every frame. Returns FPS when 1 second has elapsed, otherwise null. */
|
|
96
101
|
tick() {
|
|
97
102
|
this.count++;
|
|
@@ -187,7 +192,7 @@ async function createTextureBridge(options) {
|
|
|
187
192
|
const bridge = new TextureBridgeImpl(renderWindow, sender, previewManager, options);
|
|
188
193
|
renderWindow.webContents.on("paint", (event) => {
|
|
189
194
|
const texture = event.texture;
|
|
190
|
-
if (!texture) return;
|
|
195
|
+
if (!texture?.textureInfo) return;
|
|
191
196
|
try {
|
|
192
197
|
sendTextureFromPaintEvent(bridge.sender, texture.textureInfo);
|
|
193
198
|
bridge.previewManager?.sendFrame(texture);
|
|
@@ -209,4 +214,110 @@ async function createTextureBridge(options) {
|
|
|
209
214
|
}
|
|
210
215
|
|
|
211
216
|
//#endregion
|
|
212
|
-
|
|
217
|
+
//#region src/receiver.ts
|
|
218
|
+
var TextureReceiverBridgeImpl = class extends EventEmitter {
|
|
219
|
+
constructor(receiver, pollIntervalMs) {
|
|
220
|
+
super();
|
|
221
|
+
this.fpsCounter = new FpsCounter();
|
|
222
|
+
this._disposed = false;
|
|
223
|
+
this._timer = null;
|
|
224
|
+
this.receiver = receiver;
|
|
225
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
226
|
+
}
|
|
227
|
+
get isDisposed() {
|
|
228
|
+
return this._disposed;
|
|
229
|
+
}
|
|
230
|
+
start() {
|
|
231
|
+
if (this._disposed || this._timer) return;
|
|
232
|
+
this.fpsCounter.reset();
|
|
233
|
+
this._timer = setInterval(() => this._poll(), this.pollIntervalMs);
|
|
234
|
+
}
|
|
235
|
+
stop() {
|
|
236
|
+
if (this._timer) {
|
|
237
|
+
clearInterval(this._timer);
|
|
238
|
+
this._timer = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
dispose() {
|
|
242
|
+
if (this._disposed) return;
|
|
243
|
+
this._disposed = true;
|
|
244
|
+
this.stop();
|
|
245
|
+
this.receiver.stop();
|
|
246
|
+
this.emit("disposed");
|
|
247
|
+
this.removeAllListeners();
|
|
248
|
+
}
|
|
249
|
+
_poll() {
|
|
250
|
+
if (this._disposed) return;
|
|
251
|
+
try {
|
|
252
|
+
if (!this.receiver.hasNewFrame()) return;
|
|
253
|
+
const frame = this.receiver.receiveFrame();
|
|
254
|
+
if (!frame) return;
|
|
255
|
+
this.emit("frame", frame);
|
|
256
|
+
const fps = this.fpsCounter.tick();
|
|
257
|
+
if (fps !== null) this.emit("fps", fps);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
260
|
+
this.emit("error", error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
function createTextureReceiver(options) {
|
|
265
|
+
const { senderName, appName, serverUuid, pollIntervalMs = 16 } = options;
|
|
266
|
+
return new TextureReceiverBridgeImpl(new TextureReceiver(senderName, appName, serverUuid), pollIntervalMs);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/discovery.ts
|
|
271
|
+
var SenderDiscovery = class extends EventEmitter {
|
|
272
|
+
constructor(..._args) {
|
|
273
|
+
super(..._args);
|
|
274
|
+
this._senders = [];
|
|
275
|
+
this._timer = null;
|
|
276
|
+
this._disposed = false;
|
|
277
|
+
}
|
|
278
|
+
get isDisposed() {
|
|
279
|
+
return this._disposed;
|
|
280
|
+
}
|
|
281
|
+
start(intervalMs = 1e3) {
|
|
282
|
+
if (this._disposed || this._timer) return;
|
|
283
|
+
this._timer = setInterval(() => this._refresh(), intervalMs);
|
|
284
|
+
}
|
|
285
|
+
stop() {
|
|
286
|
+
if (this._timer) {
|
|
287
|
+
clearInterval(this._timer);
|
|
288
|
+
this._timer = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
getSenders() {
|
|
292
|
+
return [...this._senders];
|
|
293
|
+
}
|
|
294
|
+
dispose() {
|
|
295
|
+
if (this._disposed) return;
|
|
296
|
+
this._disposed = true;
|
|
297
|
+
this.stop();
|
|
298
|
+
this.removeAllListeners();
|
|
299
|
+
}
|
|
300
|
+
_refresh() {
|
|
301
|
+
if (this._disposed) return;
|
|
302
|
+
try {
|
|
303
|
+
const current = listSenders();
|
|
304
|
+
const prev = this._senders;
|
|
305
|
+
const added = current.filter((c) => !prev.some((p) => this._isSame(c, p)));
|
|
306
|
+
const removed = prev.filter((p) => !current.some((c) => this._isSame(c, p)));
|
|
307
|
+
this._senders = current;
|
|
308
|
+
if (added.length > 0) this.emit("added", added);
|
|
309
|
+
if (removed.length > 0) this.emit("removed", removed);
|
|
310
|
+
if (added.length > 0 || removed.length > 0) this.emit("updated", current);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
313
|
+
this.emit("error", error);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
_isSame(a, b) {
|
|
317
|
+
if (a.uuid && b.uuid) return a.uuid === b.uuid;
|
|
318
|
+
return a.name === b.name && a.appName === b.appName;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
//#endregion
|
|
323
|
+
export { SenderDiscovery, createTextureBridge, createTextureReceiver };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@napolab/texture-bridge-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "High-level factory API for GPU texture bridge (BrowserWindow + Preview + Sender)",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@napolab/texture-bridge-core": "0.
|
|
28
|
+
"@napolab/texture-bridge-core": "0.5.1"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"electron": ">=40.0.0"
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
],
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "tsdown src/index.ts src/client/index.ts src/client/worker-protocol.ts --format cjs,esm --dts && cp -r src/assets dist/assets",
|
|
56
|
+
"test": "vitest run",
|
|
56
57
|
"typecheck": "tsgo --noEmit"
|
|
57
58
|
}
|
|
58
59
|
}
|