@hyperframes/engine 0.7.60 → 0.7.61

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.
Files changed (54) hide show
  1. package/README.md +3 -4
  2. package/dist/config.d.ts +45 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +39 -16
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +3 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/services/audioMixer.d.ts.map +1 -1
  11. package/dist/services/audioMixer.js +19 -3
  12. package/dist/services/audioMixer.js.map +1 -1
  13. package/dist/services/browserLeasePool.d.ts +48 -0
  14. package/dist/services/browserLeasePool.d.ts.map +1 -0
  15. package/dist/services/browserLeasePool.js +210 -0
  16. package/dist/services/browserLeasePool.js.map +1 -0
  17. package/dist/services/browserManager.d.ts +6 -7
  18. package/dist/services/browserManager.d.ts.map +1 -1
  19. package/dist/services/browserManager.js +122 -177
  20. package/dist/services/browserManager.js.map +1 -1
  21. package/dist/services/chunkEncoder.d.ts.map +1 -1
  22. package/dist/services/chunkEncoder.js +45 -134
  23. package/dist/services/chunkEncoder.js.map +1 -1
  24. package/dist/services/frameCapture.d.ts +13 -1
  25. package/dist/services/frameCapture.d.ts.map +1 -1
  26. package/dist/services/frameCapture.js +85 -8
  27. package/dist/services/frameCapture.js.map +1 -1
  28. package/dist/services/streamingEncoder.d.ts.map +1 -1
  29. package/dist/services/streamingEncoder.js +23 -58
  30. package/dist/services/streamingEncoder.js.map +1 -1
  31. package/dist/services/videoFrameExtractor.d.ts.map +1 -1
  32. package/dist/services/videoFrameExtractor.js +39 -71
  33. package/dist/services/videoFrameExtractor.js.map +1 -1
  34. package/dist/utils/ffmpegBinaries.d.ts +2 -2
  35. package/dist/utils/ffmpegBinaries.d.ts.map +1 -1
  36. package/dist/utils/ffmpegBinaries.js +8 -82
  37. package/dist/utils/ffmpegBinaries.js.map +1 -1
  38. package/dist/utils/ffprobe.d.ts +3 -1
  39. package/dist/utils/ffprobe.d.ts.map +1 -1
  40. package/dist/utils/ffprobe.js +35 -42
  41. package/dist/utils/ffprobe.js.map +1 -1
  42. package/dist/utils/gpuEncoder.d.ts.map +1 -1
  43. package/dist/utils/gpuEncoder.js +32 -52
  44. package/dist/utils/gpuEncoder.js.map +1 -1
  45. package/dist/utils/managedChildProcess.d.ts +50 -0
  46. package/dist/utils/managedChildProcess.d.ts.map +1 -0
  47. package/dist/utils/managedChildProcess.js +148 -0
  48. package/dist/utils/managedChildProcess.js.map +1 -0
  49. package/dist/utils/processTracker.js +1 -1
  50. package/dist/utils/runFfmpeg.d.ts +3 -0
  51. package/dist/utils/runFfmpeg.d.ts.map +1 -1
  52. package/dist/utils/runFfmpeg.js +16 -50
  53. package/dist/utils/runFfmpeg.js.map +1 -1
  54. package/package.json +3 -2
@@ -0,0 +1,210 @@
1
+ function freezeFingerprint(fingerprint) {
2
+ return Object.freeze({
3
+ ...fingerprint,
4
+ args: Object.freeze([...fingerprint.args]),
5
+ });
6
+ }
7
+ function fingerprintKey(fingerprint) {
8
+ return JSON.stringify([
9
+ fingerprint.args,
10
+ fingerprint.executablePath ?? null,
11
+ fingerprint.browserTimeoutMs,
12
+ fingerprint.protocolTimeoutMs,
13
+ fingerprint.requestedCaptureMode,
14
+ ]);
15
+ }
16
+ /** Owns pooled browser generations and hands callers exactly-once leases. */
17
+ export class BrowserLeasePool {
18
+ options;
19
+ available = new Map();
20
+ entries = new Set();
21
+ leasesByBrowser = new Map();
22
+ drainPromise = null;
23
+ constructor(options) {
24
+ this.options = options;
25
+ }
26
+ async acquire(fingerprintInput, pooled) {
27
+ if (this.drainPromise)
28
+ await this.drainPromise;
29
+ const fingerprint = freezeFingerprint(fingerprintInput);
30
+ const entry = this.reserveEntry(fingerprint, pooled);
31
+ return this.issueLease(entry);
32
+ }
33
+ async issueLease(entry) {
34
+ try {
35
+ const result = await entry.launchPromise;
36
+ if (entry.state !== "ready") {
37
+ throw new Error("Browser pool drained during acquisition");
38
+ }
39
+ return this.createLease(entry, result);
40
+ }
41
+ catch (error) {
42
+ entry.refCount = Math.max(0, entry.refCount - 1);
43
+ throw error;
44
+ }
45
+ }
46
+ reserveEntry(fingerprint, pooled) {
47
+ const key = fingerprintKey(fingerprint);
48
+ let entry = pooled ? this.available.get(key) : undefined;
49
+ if (entry?.state === "ready" && !entry.result?.browser.connected) {
50
+ this.requestClose(entry, true);
51
+ entry = undefined;
52
+ }
53
+ if (!entry || entry.state === "closing")
54
+ return this.createEntry(key, fingerprint, pooled);
55
+ entry.refCount += 1;
56
+ return entry;
57
+ }
58
+ /** Preserve legacy release-by-handle only when ownership is unambiguous. */
59
+ async releaseByBrowser(browser) {
60
+ const leases = this.leasesByBrowser.get(browser);
61
+ if (leases?.size === 1) {
62
+ const lease = leases.values().next().value;
63
+ await lease.release();
64
+ return;
65
+ }
66
+ if (leases) {
67
+ throw new Error(`Cannot release a pooled browser by handle while ${leases.size} leases are active; release the owning BrowserLease instead`);
68
+ }
69
+ await this.options.close(browser).catch(() => { });
70
+ }
71
+ /** Preserve the legacy ambiguous-handle no-op without closing another owner's browser. */
72
+ forceReleaseByBrowser(browser) {
73
+ const leases = this.leasesByBrowser.get(browser);
74
+ if (leases?.size === 1) {
75
+ const lease = leases.values().next().value;
76
+ lease.forceRelease();
77
+ return;
78
+ }
79
+ if (leases)
80
+ return;
81
+ this.options.forceClose(browser);
82
+ }
83
+ drain() {
84
+ if (this.drainPromise)
85
+ return this.drainPromise;
86
+ const entries = [...this.entries];
87
+ for (const entry of entries)
88
+ this.requestClose(entry, false);
89
+ this.drainPromise = Promise.all(entries.map((entry) => entry.closePromise)).then(() => {
90
+ this.drainPromise = null;
91
+ });
92
+ return this.drainPromise;
93
+ }
94
+ /** Test-only state reset. Call `drain()` first when entries own real browsers. */
95
+ reset() {
96
+ this.available.clear();
97
+ this.entries.clear();
98
+ this.leasesByBrowser.clear();
99
+ this.drainPromise = null;
100
+ }
101
+ createEntry(key, fingerprint, pooled) {
102
+ const entry = {
103
+ key,
104
+ fingerprint,
105
+ pooled,
106
+ state: "launching",
107
+ refCount: 1,
108
+ closeRequested: false,
109
+ forceCloseRequested: false,
110
+ launchPromise: undefined,
111
+ };
112
+ entry.launchPromise = this.options.launch(fingerprint).then((result) => {
113
+ entry.result = result;
114
+ if (entry.closeRequested) {
115
+ entry.state = "closing";
116
+ }
117
+ else {
118
+ entry.state = "ready";
119
+ }
120
+ return result;
121
+ }, (error) => {
122
+ if (this.available.get(key) === entry)
123
+ this.available.delete(key);
124
+ this.entries.delete(entry);
125
+ throw error;
126
+ });
127
+ this.entries.add(entry);
128
+ if (pooled)
129
+ this.available.set(key, entry);
130
+ return entry;
131
+ }
132
+ createLease(entry, result) {
133
+ let active = true;
134
+ let lease;
135
+ const deactivate = (force) => {
136
+ if (!active)
137
+ return false;
138
+ active = false;
139
+ this.removeLease(result.browser, lease);
140
+ entry.refCount = Math.max(0, entry.refCount - 1);
141
+ if (entry.refCount === 0)
142
+ this.requestClose(entry, force);
143
+ return true;
144
+ };
145
+ lease = {
146
+ ...result,
147
+ fingerprint: entry.fingerprint,
148
+ release: async () => {
149
+ if (!deactivate(false))
150
+ return;
151
+ await entry.closePromise;
152
+ },
153
+ forceRelease: () => {
154
+ if (!deactivate(true) && entry.refCount === 0 && entry.closePromise) {
155
+ this.requestClose(entry, true);
156
+ }
157
+ },
158
+ };
159
+ const leases = this.leasesByBrowser.get(result.browser) ?? new Set();
160
+ leases.add(lease);
161
+ this.leasesByBrowser.set(result.browser, leases);
162
+ return lease;
163
+ }
164
+ requestClose(entry, force) {
165
+ if (force) {
166
+ entry.forceCloseRequested = true;
167
+ entry.forceClose?.();
168
+ }
169
+ if (entry.closePromise)
170
+ return;
171
+ entry.closeRequested = true;
172
+ if (this.available.get(entry.key) === entry)
173
+ this.available.delete(entry.key);
174
+ entry.closePromise = entry.launchPromise
175
+ .then(async (result) => {
176
+ entry.state = "closing";
177
+ if (entry.forceCloseRequested) {
178
+ this.options.forceClose(result.browser);
179
+ }
180
+ else {
181
+ await Promise.race([
182
+ this.options.close(result.browser).catch(() => { }),
183
+ new Promise((resolve) => {
184
+ entry.forceClose = () => {
185
+ entry.forceClose = undefined;
186
+ this.options.forceClose(result.browser);
187
+ resolve();
188
+ };
189
+ }),
190
+ ]);
191
+ }
192
+ })
193
+ .catch(() => { })
194
+ .finally(() => {
195
+ entry.forceClose = undefined;
196
+ this.entries.delete(entry);
197
+ if (entry.result)
198
+ this.leasesByBrowser.delete(entry.result.browser);
199
+ });
200
+ }
201
+ removeLease(browser, lease) {
202
+ const leases = this.leasesByBrowser.get(browser);
203
+ if (!leases)
204
+ return;
205
+ leases.delete(lease);
206
+ if (leases.size === 0)
207
+ this.leasesByBrowser.delete(browser);
208
+ }
209
+ }
210
+ //# sourceMappingURL=browserLeasePool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browserLeasePool.js","sourceRoot":"","sources":["../../src/services/browserLeasePool.ts"],"names":[],"mappings":"AA4CA,SAAS,iBAAiB,CACxB,WAAqC;IAErC,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,WAAW;QACd,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;KAC3C,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,WAA+C;IACrE,OAAO,IAAI,CAAC,SAAS,CAAC;QACpB,WAAW,CAAC,IAAI;QAChB,WAAW,CAAC,cAAc,IAAI,IAAI;QAClC,WAAW,CAAC,gBAAgB;QAC5B,WAAW,CAAC,iBAAiB;QAC7B,WAAW,CAAC,oBAAoB;KACjC,CAAC,CAAC;AACL,CAAC;AAED,6EAA6E;AAC7E,MAAM,OAAO,gBAAgB;IAME;IALZ,SAAS,GAAG,IAAI,GAAG,EAA4B,CAAC;IAChD,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;IACtC,eAAe,GAAG,IAAI,GAAG,EAA8B,CAAC;IACjE,YAAY,GAAyB,IAAI,CAAC;IAElD,YAA6B,OAAgC;QAAhC,YAAO,GAAP,OAAO,CAAyB;IAAG,CAAC;IAEjE,KAAK,CAAC,OAAO,CACX,gBAA0C,EAC1C,MAAe;QAEf,IAAI,IAAI,CAAC,YAAY;YAAE,MAAM,IAAI,CAAC,YAAY,CAAC;QAE/C,MAAM,WAAW,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACrD,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,KAAuB;QAC9C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,aAAa,CAAC;YACzC,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO,EAAE,CAAC;gBAC5B,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;YAC7D,CAAC;YACD,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACzC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;YACjD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAEO,YAAY,CAClB,WAA+C,EAC/C,MAAe;QAEf,MAAM,GAAG,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC;QACxC,IAAI,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzD,IAAI,KAAK,EAAE,KAAK,KAAK,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;YACjE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC/B,KAAK,GAAG,SAAS,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QAC3F,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC;QACpB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,gBAAgB,CAAC,OAAgB;QACrC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjD,IAAI,MAAM,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAqB,CAAC;YAC3D,MAAM,KAAK,CAAC,OAAO,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACb,mDAAmD,MAAM,CAAC,IAAI,6DAA6D,CAC5H,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,0FAA0F;IAC1F,qBAAqB,CAAC,OAAgB;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjD,IAAI,MAAM,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAqB,CAAC;YAC3D,KAAK,CAAC,YAAY,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QACD,IAAI,MAAM;YAAE,OAAO;QACnB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC;QAChD,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;QAClC,KAAK,MAAM,KAAK,IAAI,OAAO;YAAE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC7D,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACpF,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,kFAAkF;IAClF,KAAK;QACH,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAC3B,CAAC;IAEO,WAAW,CACjB,GAAW,EACX,WAA+C,EAC/C,MAAe;QAEf,MAAM,KAAK,GAAqB;YAC9B,GAAG;YACH,WAAW;YACX,MAAM;YACN,KAAK,EAAE,WAAW;YAClB,QAAQ,EAAE,CAAC;YACX,cAAc,EAAE,KAAK;YACrB,mBAAmB,EAAE,KAAK;YAC1B,aAAa,EAAE,SAAoD;SACpE,CAAC;QACF,KAAK,CAAC,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CACzD,CAAC,MAAM,EAAE,EAAE;YACT,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;YACtB,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;gBACzB,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC;YACxB,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YACjB,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK;gBAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAClE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC3B,MAAM,KAAK,CAAC;QACd,CAAC,CACF,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,MAAM;YAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3C,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,WAAW,CAAC,KAAuB,EAAE,MAA2B;QACtE,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,IAAI,KAAmB,CAAC;QACxB,MAAM,UAAU,GAAG,CAAC,KAAc,EAAW,EAAE;YAC7C,IAAI,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAC;YAC1B,MAAM,GAAG,KAAK,CAAC;YACf,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACxC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC;YACjD,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC;gBAAE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAC1D,OAAO,IAAI,CAAC;QACd,CAAC,CAAC;QACF,KAAK,GAAG;YACN,GAAG,MAAM;YACT,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;oBAAE,OAAO;gBAC/B,MAAM,KAAK,CAAC,YAAY,CAAC;YAC3B,CAAC;YACD,YAAY,EAAE,GAAG,EAAE;gBACjB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;oBACpE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;SACF,CAAC;QACF,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,EAAgB,CAAC;QACnF,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACjD,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,YAAY,CAAC,KAAuB,EAAE,KAAc;QAC1D,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,CAAC,mBAAmB,GAAG,IAAI,CAAC;YACjC,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC;QACvB,CAAC;QACD,IAAI,KAAK,CAAC,YAAY;YAAE,OAAO;QAC/B,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC;QAC5B,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,KAAK;YAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9E,KAAK,CAAC,YAAY,GAAG,KAAK,CAAC,aAAa;aACrC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YACrB,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC;YACxB,IAAI,KAAK,CAAC,mBAAmB,EAAE,CAAC;gBAC9B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAC1C,CAAC;iBAAM,CAAC;gBACN,MAAM,OAAO,CAAC,IAAI,CAAC;oBACjB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;oBAClD,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;wBAC5B,KAAK,CAAC,UAAU,GAAG,GAAG,EAAE;4BACtB,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC;4BAC7B,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;4BACxC,OAAO,EAAE,CAAC;wBACZ,CAAC,CAAC;oBACJ,CAAC,CAAC;iBACH,CAAC,CAAC;YACL,CAAC;QACH,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;aACf,OAAO,CAAC,GAAG,EAAE;YACZ,KAAK,CAAC,UAAU,GAAG,SAAS,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC3B,IAAI,KAAK,CAAC,MAAM;gBAAE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,WAAW,CAAC,OAAgB,EAAE,KAAmB;QACvD,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACjD,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACrB,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC;YAAE,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -6,15 +6,14 @@
6
6
  */
7
7
  import type { Browser, PuppeteerNode } from "puppeteer-core";
8
8
  import { type EngineConfig } from "../config.js";
9
- export type CaptureMode = "beginframe" | "screenshot" | "drawelement";
10
- export interface AcquiredBrowser {
11
- browser: Browser;
12
- captureMode: CaptureMode;
13
- }
9
+ import { type BrowserLease, type CaptureMode } from "./browserLeasePool.js";
10
+ export { BrowserLeasePool } from "./browserLeasePool.js";
11
+ export type { BrowserLaunchFingerprint, BrowserLease, BrowserPoolState, CaptureMode, } from "./browserLeasePool.js";
12
+ export type AcquiredBrowser = BrowserLease;
14
13
  /**
15
14
  * Resolve chrome-headless-shell binary for deterministic BeginFrame rendering.
16
15
  * Checks config.chromePath, then PRODUCER_HEADLESS_SHELL_PATH env var,
17
- * then scans Puppeteer's managed cache at ~/.cache/puppeteer/chrome-headless-shell/.
16
+ * then the CLI browser override, HyperFrames' managed cache, and Puppeteer's cache.
18
17
  */
19
18
  export declare function resolveHeadlessShellPath(config?: Partial<Pick<EngineConfig, "chromePath">>): string | undefined;
20
19
  export declare const ENABLE_BROWSER_POOL: boolean;
@@ -41,7 +40,7 @@ export declare function resolveBrowserGpuMode(mode: EngineConfig["browserGpuMode
41
40
  platform?: NodeJS.Platform;
42
41
  }): Promise<"software" | "hardware">;
43
42
  export declare function acquireBrowser(chromeArgs: string[], config?: Partial<Pick<EngineConfig, "browserTimeout" | "protocolTimeout" | "enableBrowserPool" | "chromePath" | "forceScreenshot">>): Promise<AcquiredBrowser>;
44
- export declare function releaseBrowser(browser: Browser, config?: Partial<Pick<EngineConfig, "enableBrowserPool">>): Promise<void>;
43
+ export declare function releaseBrowser(browser: Browser, _config?: Partial<Pick<EngineConfig, "enableBrowserPool">>): Promise<void>;
45
44
  export declare function forceReleaseBrowser(browser: Browser): void;
46
45
  /**
47
46
  * Forcefully close the pooled browser if one exists, regardless of refCount.
@@ -1 +1 @@
1
- {"version":3,"file":"browserManager.d.ts","sourceRoot":"","sources":["../../src/services/browserManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK7D,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAyFjE,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,YAAY,GAAG,aAAa,CAAC;AAEtE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,GACjD,MAAM,GAAG,SAAS,CAiCpB;AAQD,eAAO,MAAM,mBAAmB,SAAmC,CAAC;AA2EpE,gDAAgD;AAChD,wBAAgB,qCAAqC,IAAI,IAAI,CAE5D;AAkED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,CAAC,gBAAgB,CAAC,EACpC,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACvB,GACL,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC,CAMlC;AA4BD,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,CACd,IAAI,CACF,YAAY,EACZ,gBAAgB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,YAAY,GAAG,iBAAiB,CAC9F,CACF,GACA,OAAO,CAAC,eAAe,CAAC,CAsD1B;AAsED,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8B1D;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CActD;AAED,uCAAuC;AACvC,wBAAgB,yBAAyB,IAAI,IAAI,CAKhD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI,CAE3E;AAyCD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,gBAAgB,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GACnF,MAAM,EAAE,CA0FV"}
1
+ {"version":3,"file":"browserManager.d.ts","sourceRoot":"","sources":["../../src/services/browserManager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAK7D,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,wBAAwB,EACxB,YAAY,EACZ,gBAAgB,EAChB,WAAW,GACZ,MAAM,uBAAuB,CAAC;AAqF/B,MAAM,MAAM,eAAe,GAAG,YAAY,CAAC;AA4C3C;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,GACjD,MAAM,GAAG,SAAS,CA8BpB;AAGD,eAAO,MAAM,mBAAmB,SAAmC,CAAC;AA2EpE,gDAAgD;AAChD,wBAAgB,qCAAqC,IAAI,IAAI,CAE5D;AAkED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,CAAC,gBAAgB,CAAC,EACpC,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CACvB,GACL,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC,CAMlC;AAsCD,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAAE,EACpB,MAAM,CAAC,EAAE,OAAO,CACd,IAAI,CACF,YAAY,EACZ,gBAAgB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,YAAY,GAAG,iBAAiB,CAC9F,CACF,GACA,OAAO,CAAC,eAAe,CAAC,CAG1B;AAgFD,wBAAsB,cAAc,CAClC,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,mBAAmB,CAAC,CAAC,GACzD,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE1D;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEtD;AAED,uCAAuC;AACvC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED,iFAAiF;AACjF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI,CAE3E;AAyCD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC5B;AAKD,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,EAC/B,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,gBAAgB,GAAG,YAAY,GAAG,YAAY,CAAC,CAAC,GACnF,MAAM,EAAE,CA0FV"}
@@ -10,6 +10,8 @@ import { join } from "path";
10
10
  import { homedir } from "os";
11
11
  import { DEFAULT_CONFIG } from "../config.js";
12
12
  import { getSystemTotalMb, LOW_MEMORY_TOTAL_MB_THRESHOLD } from "./systemMemory.js";
13
+ import { BrowserLeasePool, } from "./browserLeasePool.js";
14
+ export { BrowserLeasePool } from "./browserLeasePool.js";
13
15
  let _puppeteer;
14
16
  function isSoftwareWebGlRenderer(rendererInfo) {
15
17
  const renderer = rendererInfo.trim().toLowerCase();
@@ -76,28 +78,33 @@ async function probeHardwareWebGlInfo(ppt, options) {
76
78
  await probeBrowser?.close().catch(() => { });
77
79
  }
78
80
  }
79
- /**
80
- * Resolve chrome-headless-shell binary for deterministic BeginFrame rendering.
81
- * Checks config.chromePath, then PRODUCER_HEADLESS_SHELL_PATH env var,
82
- * then scans Puppeteer's managed cache at ~/.cache/puppeteer/chrome-headless-shell/.
83
- */
84
- export function resolveHeadlessShellPath(config) {
85
- if (config?.chromePath) {
86
- return config.chromePath;
87
- }
88
- if (process.env.PRODUCER_HEADLESS_SHELL_PATH) {
89
- const envPath = process.env.PRODUCER_HEADLESS_SHELL_PATH;
90
- if (!existsSync(envPath)) {
91
- throw new Error(`[BrowserManager] Chrome binary not found at PRODUCER_HEADLESS_SHELL_PATH="${envPath}". ` +
92
- "Run `hyperframes browser ensure` to re-download.");
81
+ function compareBrowserVersionsDescending(left, right) {
82
+ const parse = (value) => {
83
+ const version = value.slice(value.indexOf("-") + 1);
84
+ const segments = [];
85
+ for (const segment of version.split(".")) {
86
+ const parsed = Number.parseInt(segment, 10);
87
+ if (!Number.isFinite(parsed))
88
+ break;
89
+ segments.push(parsed);
93
90
  }
94
- return envPath;
95
- }
96
- const baseDir = join(homedir(), ".cache", "puppeteer", "chrome-headless-shell");
91
+ return segments;
92
+ };
93
+ const leftSegments = parse(left);
94
+ const rightSegments = parse(right);
95
+ const length = Math.max(leftSegments.length, rightSegments.length);
96
+ for (let index = 0; index < length; index += 1) {
97
+ const difference = (rightSegments[index] ?? 0) - (leftSegments[index] ?? 0);
98
+ if (difference !== 0)
99
+ return difference;
100
+ }
101
+ return 0;
102
+ }
103
+ function findCachedHeadlessShell(baseDir) {
97
104
  if (!existsSync(baseDir))
98
105
  return undefined;
99
106
  try {
100
- const versions = readdirSync(baseDir).sort().reverse(); // newest first
107
+ const versions = readdirSync(baseDir).sort(compareBrowserVersionsDescending);
101
108
  for (const version of versions) {
102
109
  const candidates = [
103
110
  join(baseDir, version, "chrome-headless-shell-linux64", "chrome-headless-shell"),
@@ -112,14 +119,38 @@ export function resolveHeadlessShellPath(config) {
112
119
  }
113
120
  }
114
121
  catch {
115
- // ignore
122
+ // Ignore unreadable cache directories and continue browser discovery.
116
123
  }
117
124
  return undefined;
118
125
  }
119
- let pooledBrowser = null;
120
- let pooledBrowserRefCount = 0;
121
- let pooledCaptureMode = "screenshot";
122
- let _pooledBrowserLaunchPromise = null;
126
+ /**
127
+ * Resolve chrome-headless-shell binary for deterministic BeginFrame rendering.
128
+ * Checks config.chromePath, then PRODUCER_HEADLESS_SHELL_PATH env var,
129
+ * then the CLI browser override, HyperFrames' managed cache, and Puppeteer's cache.
130
+ */
131
+ export function resolveHeadlessShellPath(config) {
132
+ if (config?.chromePath) {
133
+ return config.chromePath;
134
+ }
135
+ if (process.env.PRODUCER_HEADLESS_SHELL_PATH) {
136
+ const envPath = process.env.PRODUCER_HEADLESS_SHELL_PATH;
137
+ if (!existsSync(envPath)) {
138
+ throw new Error(`[BrowserManager] Chrome binary not found at PRODUCER_HEADLESS_SHELL_PATH="${envPath}". ` +
139
+ "Run `hyperframes browser ensure` to re-download.");
140
+ }
141
+ return envPath;
142
+ }
143
+ if (process.env.HYPERFRAMES_BROWSER_PATH) {
144
+ const envPath = process.env.HYPERFRAMES_BROWSER_PATH;
145
+ if (!existsSync(envPath)) {
146
+ throw new Error(`[BrowserManager] Chrome binary not found at HYPERFRAMES_BROWSER_PATH="${envPath}". ` +
147
+ "Run `hyperframes browser ensure` to re-download.");
148
+ }
149
+ return envPath;
150
+ }
151
+ const home = homedir();
152
+ return (findCachedHeadlessShell(join(home, ".cache", "hyperframes", "chrome", "chrome-headless-shell")) ?? findCachedHeadlessShell(join(home, ".cache", "puppeteer", "chrome-headless-shell")));
153
+ }
123
154
  // Preserve the producer-era export so re-export shims keep the same public API.
124
155
  export const ENABLE_BROWSER_POOL = DEFAULT_CONFIG.enableBrowserPool;
125
156
  // Flags only meaningful when Chrome's compositor is driven by
@@ -284,153 +315,71 @@ export function resolveBrowserGpuMode(mode, options = {}) {
284
315
  function logResolvedBrowserGpuMode(resolved, reason) {
285
316
  console.error(`[hyperframes] browserGpuMode auto → ${resolved} (${reason})`);
286
317
  }
287
- /**
288
- * Resolve the capture mode the caller expects, WITHOUT launching a browser.
289
- * Used to validate pool compatibility before returning a cached instance.
290
- */
291
- function resolveRequestedCaptureMode(config) {
292
- const headlessShell = resolveHeadlessShellPath(config);
293
- // BeginFrame requires chrome-headless-shell AND Linux — crashes on
294
- // macOS/Windows (crbug.com/40656275).
295
- const isLinux = process.platform === "linux";
296
- const forceScreenshot = config?.forceScreenshot ?? DEFAULT_CONFIG.forceScreenshot;
297
- if (headlessShell && isLinux && !forceScreenshot)
298
- return "beginframe";
299
- return "screenshot";
318
+ function createBrowserLaunchFingerprint(chromeArgs, config) {
319
+ const launchConfig = {
320
+ browserTimeout: DEFAULT_CONFIG.browserTimeout,
321
+ protocolTimeout: DEFAULT_CONFIG.protocolTimeout,
322
+ forceScreenshot: DEFAULT_CONFIG.forceScreenshot,
323
+ ...config,
324
+ };
325
+ const headlessShell = resolveHeadlessShellPath(launchConfig);
326
+ const requestedCaptureMode = headlessShell && process.platform === "linux" && !launchConfig.forceScreenshot
327
+ ? "beginframe"
328
+ : "screenshot";
329
+ return {
330
+ args: chromeArgs,
331
+ executablePath: headlessShell,
332
+ browserTimeoutMs: launchConfig.browserTimeout,
333
+ protocolTimeoutMs: launchConfig.protocolTimeout,
334
+ requestedCaptureMode,
335
+ };
300
336
  }
301
337
  export async function acquireBrowser(chromeArgs, config) {
302
338
  const enablePool = config?.enableBrowserPool ?? DEFAULT_CONFIG.enableBrowserPool;
303
- if (enablePool && pooledBrowser) {
304
- if (!pooledBrowser.connected) {
305
- pooledBrowser = null;
306
- pooledBrowserRefCount = 0;
307
- _pooledBrowserLaunchPromise = null;
308
- }
309
- else {
310
- // Validate mode compatibility: a caller that needs screenshot mode
311
- // (forceScreenshot, alpha output, BeginFrame timeout retry) must not
312
- // receive a beginframe browser — the BeginFrame-only flags make the
313
- // compositor wait for frames the screenshot path never sends.
314
- const requestedMode = resolveRequestedCaptureMode(config);
315
- if (pooledCaptureMode === requestedMode) {
316
- pooledBrowserRefCount += 1;
317
- return { browser: pooledBrowser, captureMode: pooledCaptureMode };
318
- }
319
- // Mode mismatch — skip pool, launch a dedicated browser for this caller.
320
- // Don't evict the pooled browser: other sessions may still hold refs.
321
- }
322
- }
323
- // Dedup concurrent launches: when the pool is enabled and multiple callers
324
- // (e.g. parallel workers via Promise.all) race into acquireBrowser before
325
- // the first launch completes, they would all see pooledBrowser === null and
326
- // each spawn a separate Chrome. Cache the in-flight launch Promise so the
327
- // second+ callers await the same one instead of launching again.
328
- if (enablePool && _pooledBrowserLaunchPromise) {
329
- const result = await _pooledBrowserLaunchPromise;
330
- const requestedMode = resolveRequestedCaptureMode(config);
331
- if (result.captureMode === requestedMode) {
332
- pooledBrowserRefCount += 1;
333
- return result;
334
- }
335
- // Mode mismatch with pending launch — launch a dedicated browser.
336
- }
337
- const launchPromise = launchBrowser(chromeArgs, config);
338
- if (enablePool && !pooledBrowser && !_pooledBrowserLaunchPromise) {
339
- _pooledBrowserLaunchPromise = launchPromise;
340
- try {
341
- const result = await launchPromise;
342
- pooledBrowser = result.browser;
343
- pooledBrowserRefCount = 1;
344
- pooledCaptureMode = result.captureMode;
345
- return result;
346
- }
347
- finally {
348
- _pooledBrowserLaunchPromise = null;
349
- }
350
- }
351
- return launchPromise;
339
+ return browserLeasePool.acquire(createBrowserLaunchFingerprint(chromeArgs, config), enablePool);
352
340
  }
353
341
  // fallow-ignore-next-line complexity
354
- async function launchBrowser(chromeArgs, config) {
355
- // Config chromePath overrides env var / auto-detection.
356
- const headlessShell = resolveHeadlessShellPath(config);
357
- // BeginFrame requires chrome-headless-shell AND Linux (crashes on
358
- // macOS/Windows — crbug.com/40656275).
359
- const isLinux = process.platform === "linux";
360
- const forceScreenshot = config?.forceScreenshot ?? DEFAULT_CONFIG.forceScreenshot;
361
- let captureMode;
362
- let executablePath;
363
- if (headlessShell && isLinux && !forceScreenshot) {
364
- captureMode = "beginframe";
365
- executablePath = headlessShell;
366
- }
367
- else {
368
- // Screenshot mode with renderSeek: works on all platforms.
369
- captureMode = "screenshot";
370
- executablePath = headlessShell ?? undefined;
371
- }
342
+ async function launchBrowser(fingerprint) {
372
343
  const ppt = await getPuppeteer();
373
- const browserTimeout = config?.browserTimeout ?? DEFAULT_CONFIG.browserTimeout;
374
- const protocolTimeout = config?.protocolTimeout ?? DEFAULT_CONFIG.protocolTimeout;
375
- let browser = await ppt.launch({
376
- headless: true,
377
- args: chromeArgs,
378
- defaultViewport: null,
379
- executablePath,
380
- timeout: browserTimeout,
381
- protocolTimeout,
382
- });
383
- const browserVersion = await browser.version().catch(() => "unknown");
384
- const gpuFlags = chromeArgs.filter((a) => a.startsWith("--use-gl=") || a.startsWith("--use-angle="));
385
- console.log(`[BrowserManager] Browser launched (${browserVersion}, ${captureMode}, gl=${gpuFlags.join(" ") || "default"}, headlessShell=${!!headlessShell}, platform=${process.platform})`);
386
- if (captureMode === "beginframe") {
387
- const supported = await probeBeginFrameSupport(browser).catch(() => true);
388
- if (!supported) {
389
- await browser.close().catch(() => { });
390
- console.warn("[BrowserManager] HeadlessExperimental.beginFrame unavailable in this Chromium build; falling back to screenshot mode.");
391
- captureMode = "screenshot";
392
- browser = await ppt.launch({
393
- headless: true,
394
- args: stripBeginFrameFlags(chromeArgs),
395
- defaultViewport: null,
396
- executablePath,
397
- timeout: browserTimeout,
398
- protocolTimeout,
399
- });
344
+ let captureMode = fingerprint.requestedCaptureMode;
345
+ let browser;
346
+ try {
347
+ browser = await ppt.launch({
348
+ headless: true,
349
+ args: [...fingerprint.args],
350
+ defaultViewport: null,
351
+ executablePath: fingerprint.executablePath,
352
+ timeout: fingerprint.browserTimeoutMs,
353
+ protocolTimeout: fingerprint.protocolTimeoutMs,
354
+ });
355
+ const browserVersion = await browser.version().catch(() => "unknown");
356
+ const gpuFlags = fingerprint.args.filter((a) => a.startsWith("--use-gl=") || a.startsWith("--use-angle="));
357
+ console.log(`[BrowserManager] Browser launched (${browserVersion}, ${captureMode}, gl=${gpuFlags.join(" ") || "default"}, headlessShell=${!!fingerprint.executablePath}, platform=${process.platform})`);
358
+ if (captureMode === "beginframe") {
359
+ const supported = await probeBeginFrameSupport(browser).catch(() => true);
360
+ if (!supported) {
361
+ await browser.close().catch(() => { });
362
+ browser = undefined;
363
+ console.warn("[BrowserManager] HeadlessExperimental.beginFrame unavailable in this Chromium build; falling back to screenshot mode.");
364
+ captureMode = "screenshot";
365
+ browser = await ppt.launch({
366
+ headless: true,
367
+ args: stripBeginFrameFlags([...fingerprint.args]),
368
+ defaultViewport: null,
369
+ executablePath: fingerprint.executablePath,
370
+ timeout: fingerprint.browserTimeoutMs,
371
+ protocolTimeout: fingerprint.protocolTimeoutMs,
372
+ });
373
+ }
400
374
  }
375
+ return { browser, captureMode };
401
376
  }
402
- return { browser, captureMode };
403
- }
404
- export async function releaseBrowser(browser, config) {
405
- const enablePool = config?.enableBrowserPool ?? DEFAULT_CONFIG.enableBrowserPool;
406
- if (!enablePool) {
407
- await browser.close().catch(() => { });
408
- return;
409
- }
410
- if (pooledBrowser && pooledBrowser === browser) {
411
- pooledBrowserRefCount = Math.max(0, pooledBrowserRefCount - 1);
412
- if (pooledBrowserRefCount === 0) {
413
- await browser.close().catch(() => { });
414
- pooledBrowser = null;
415
- _pooledBrowserLaunchPromise = null;
416
- }
417
- return;
377
+ catch (error) {
378
+ await browser?.close().catch(() => { });
379
+ throw error;
418
380
  }
419
- await browser.close().catch(() => { });
420
381
  }
421
- export function forceReleaseBrowser(browser) {
422
- if (pooledBrowser && pooledBrowser === browser) {
423
- // If other sessions still hold refs, just drop ours — don't kill the
424
- // shared Chrome out from under them. The browser will be cleaned up when
425
- // the last session releases or drainBrowserPool is called.
426
- if (pooledBrowserRefCount > 1) {
427
- pooledBrowserRefCount -= 1;
428
- return;
429
- }
430
- pooledBrowserRefCount = 0;
431
- pooledBrowser = null;
432
- _pooledBrowserLaunchPromise = null;
433
- }
382
+ function forceCloseBrowserProcess(browser) {
434
383
  const proc = browser.process?.();
435
384
  if (proc && !proc.killed) {
436
385
  try {
@@ -447,32 +396,28 @@ export function forceReleaseBrowser(browser) {
447
396
  // Best-effort cleanup.
448
397
  }
449
398
  }
399
+ const browserLeasePool = new BrowserLeasePool({
400
+ launch: launchBrowser,
401
+ close: async (browser) => browser.close(),
402
+ forceClose: forceCloseBrowserProcess,
403
+ });
404
+ export async function releaseBrowser(browser, _config) {
405
+ await browserLeasePool.releaseByBrowser(browser);
406
+ }
407
+ export function forceReleaseBrowser(browser) {
408
+ browserLeasePool.forceReleaseByBrowser(browser);
409
+ }
450
410
  /**
451
411
  * Forcefully close the pooled browser if one exists, regardless of refCount.
452
412
  * Used for explicit cleanup at process exit or between independent render jobs
453
413
  * that should not share browser state.
454
414
  */
455
415
  export async function drainBrowserPool() {
456
- // Await any in-flight launch first — otherwise the launch resolves after we
457
- // drain and produces a browser that nobody references (orphan).
458
- const pending = _pooledBrowserLaunchPromise;
459
- _pooledBrowserLaunchPromise = null;
460
- if (pending) {
461
- await pending.then((r) => r.browser.close()).catch(() => { });
462
- }
463
- if (pooledBrowser) {
464
- const browser = pooledBrowser;
465
- pooledBrowser = null;
466
- pooledBrowserRefCount = 0;
467
- await browser.close().catch(() => { });
468
- }
416
+ await browserLeasePool.drain();
469
417
  }
470
418
  /** Test-only: reset all pool state. */
471
419
  export function _resetBrowserPoolForTests() {
472
- pooledBrowser = null;
473
- pooledBrowserRefCount = 0;
474
- pooledCaptureMode = "screenshot";
475
- _pooledBrowserLaunchPromise = null;
420
+ browserLeasePool.reset();
476
421
  }
477
422
  /** Test-only: inject a mock PuppeteerNode so tests bypass the dynamic import. */
478
423
  export function _setPuppeteerForTests(mock) {