@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.
@@ -1,66 +1,71 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <title>Preview (GPU Zero-Copy)</title>
6
- <style>
7
- * { margin: 0; padding: 0; }
8
- body {
9
- background: #111;
10
- display: flex;
11
- flex-direction: column;
12
- align-items: center;
13
- justify-content: center;
14
- height: 100vh;
15
- font-family: -apple-system, BlinkMacSystemFont, sans-serif;
16
- }
17
- #info {
18
- position: absolute;
19
- top: 10px;
20
- left: 10px;
21
- color: #0f0;
22
- font-family: 'SF Mono', Menlo, monospace;
23
- font-size: 12px;
24
- background: rgba(0, 0, 0, 0.7);
25
- padding: 8px 12px;
26
- border-radius: 4px;
27
- z-index: 100;
28
- }
29
- canvas {
30
- max-width: 100%;
31
- max-height: calc(100vh - 20px);
32
- object-fit: contain;
33
- background: #000;
34
- }
35
- .error { color: #f44; }
36
- </style>
37
- </head>
38
- <body>
39
- <div id="info">Initializing WebGPU...</div>
40
- <canvas id="preview"></canvas>
41
-
42
- <script type="module">
43
- const params = new URLSearchParams(location.search);
44
- const W = parseInt(params.get('w'), 10) || 1920;
45
- const H = parseInt(params.get('h'), 10) || 1080;
46
-
47
- const canvas = document.getElementById('preview');
48
- canvas.width = W;
49
- canvas.height = H;
50
-
51
- const infoEl = document.getElementById('info');
52
-
53
- let device = null;
54
- let context = null;
55
- let pipeline = null;
56
- let sampler = null;
57
- let bindGroupLayout = null;
58
-
59
- let frameCount = 0;
60
- let lastTime = performance.now();
61
- let fps = 0;
62
-
63
- const shaderCode = `
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
- async function initWebGPU() {
98
- if (!navigator.gpu) {
99
- throw new Error('WebGPU not supported');
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
- bindGroupLayout = device.createBindGroupLayout({
121
- entries: [
122
- { binding: 0, visibility: GPUShaderStage.FRAGMENT, externalTexture: {} },
123
- { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
124
- ],
125
- });
107
+ const adapter = await navigator.gpu.requestAdapter();
108
+ if (!adapter) {
109
+ throw new Error("No GPU adapter found");
110
+ }
126
111
 
127
- pipeline = device.createRenderPipeline({
128
- layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
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
- infoEl.textContent = 'WebGPU ready, waiting for frames...';
139
- }
115
+ const format = navigator.gpu.getPreferredCanvasFormat();
116
+ context.configure({ device, format, alphaMode: "opaque" });
140
117
 
141
- function renderFrame(videoFrame) {
142
- if (!device || !pipeline || !videoFrame) return;
118
+ const shaderModule = device.createShaderModule({ code: shaderCode });
143
119
 
144
- try {
145
- const externalTexture = device.importExternalTexture({ source: videoFrame });
120
+ sampler = device.createSampler({
121
+ magFilter: "linear",
122
+ minFilter: "linear",
123
+ });
146
124
 
147
- const bindGroup = device.createBindGroup({
148
- layout: bindGroupLayout,
125
+ bindGroupLayout = device.createBindGroupLayout({
149
126
  entries: [
150
- { binding: 0, resource: externalTexture },
151
- { binding: 1, resource: sampler },
127
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, externalTexture: {} },
128
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: {} },
152
129
  ],
153
130
  });
154
131
 
155
- const commandEncoder = device.createCommandEncoder();
156
- const textureView = context.getCurrentTexture().createView();
157
-
158
- const renderPass = commandEncoder.beginRenderPass({
159
- colorAttachments: [{
160
- view: textureView,
161
- clearValue: { r: 0, g: 0, b: 0, a: 1 },
162
- loadOp: 'clear',
163
- storeOp: 'store',
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
- renderPass.setPipeline(pipeline);
168
- renderPass.setBindGroup(0, bindGroup);
169
- renderPass.draw(3);
170
- renderPass.end();
143
+ infoEl.textContent = "WebGPU ready, waiting for frames...";
144
+ }
171
145
 
172
- device.queue.submit([commandEncoder.finish()]);
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
- frameCount++;
175
- const now = performance.now();
176
- if (now - lastTime >= 1000) {
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
- window.electronAPI.previewReady();
204
- } catch (err) {
205
- console.error('[preview] init error:', err);
206
- infoEl.className = 'error';
207
- infoEl.textContent = `WebGPU Error: ${err.message}`;
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
- main();
212
- </script>
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
- exports.createTextureBridge = createTextureBridge;
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
- export { type BridgeEvents, type PreviewOptions, type TextureBridge, type TextureBridgeOptions, createTextureBridge };
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
- export { type BridgeEvents, type PreviewOptions, type TextureBridge, type TextureBridgeOptions, createTextureBridge };
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
- export { createTextureBridge };
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.4.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.4.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
  }