@jam.dev/recording-links 0.2.0 → 0.3.0-electron.5
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/README.md +348 -0
- package/lib/electron.d.ts +193 -0
- package/lib/electron.js +1 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -183,6 +183,354 @@ type RecorderSingleton = {
|
|
|
183
183
|
};
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
## Electron
|
|
187
|
+
|
|
188
|
+
Jam Recording Links can be integrated into your Electron app to enable seamless
|
|
189
|
+
bug reporting with screen recordings.
|
|
190
|
+
|
|
191
|
+
### 1. Initialize Jam Electron (Required)
|
|
192
|
+
|
|
193
|
+
Initialize the SDK in your main process before `app.whenReady()`:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { app, BrowserWindow } from "electron";
|
|
197
|
+
import * as jam from "@jam.dev/recording-links/electron";
|
|
198
|
+
|
|
199
|
+
jam.initialize({
|
|
200
|
+
// Optional: Customize the session if using non-default or multiple sessions.
|
|
201
|
+
// A Recorder can only capture logs from windows belonging to the same session.
|
|
202
|
+
// Defaults to session.defaultSession if not provided.
|
|
203
|
+
// defaultSession: session.defaultSession,
|
|
204
|
+
|
|
205
|
+
// Optional: Customize the display media request handler.
|
|
206
|
+
// By default, Jam installs a handler that auto-selects capture sources.
|
|
207
|
+
// Set to null if you want to handle display media requests yourself.
|
|
208
|
+
// defaultDisplayMediaRequestHandler: null,
|
|
209
|
+
|
|
210
|
+
openRecorderWindow(session) {
|
|
211
|
+
return new BrowserWindow({
|
|
212
|
+
width: 1000,
|
|
213
|
+
height: 700,
|
|
214
|
+
webPreferences: {
|
|
215
|
+
// Required to allow loading Jam's remote recorder scripts from *.jam.dev.
|
|
216
|
+
// Alternative: Use session.webRequest.onHeadersReceived() to set CSP headers
|
|
217
|
+
// allowing *.jam.dev for default-src, script-src, and worker-src.
|
|
218
|
+
allowRunningInsecureContent: true,
|
|
219
|
+
nodeIntegration: false,
|
|
220
|
+
contextIsolation: true,
|
|
221
|
+
sandbox: true,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
async loadRecorderPage(win, data) {
|
|
226
|
+
// Load your app with jam-* query parameters
|
|
227
|
+
return win.loadURL(`https://my-app.com${data.search}`);
|
|
228
|
+
// or: return win.loadFile("index.html", data);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**What this does:**
|
|
234
|
+
- Installs display media request handler for screen capture on the default session
|
|
235
|
+
- Configures window creation and content loading for recorder
|
|
236
|
+
- Manages recorder window lifecycle per session
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### 2. Handle Incoming Jam Links (Recommended)
|
|
241
|
+
|
|
242
|
+
**Register your app's protocol** (if not already done):
|
|
243
|
+
|
|
244
|
+
Add to your `package.json` electron-builder config:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"build": {
|
|
249
|
+
"protocols": {
|
|
250
|
+
"name": "my-app",
|
|
251
|
+
"schemes": ["myapp"]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Wire up protocol handlers** to open Jam recordings:
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { app } from "electron";
|
|
261
|
+
import * as jam from "@jam.dev/recording-links/electron";
|
|
262
|
+
|
|
263
|
+
// macOS: Handle protocol URLs
|
|
264
|
+
app.on("open-url", (event, url) => {
|
|
265
|
+
event.preventDefault();
|
|
266
|
+
const [cleanUrl, recorderWindow] = jam.openUrl(url);
|
|
267
|
+
|
|
268
|
+
if (recorderWindow) {
|
|
269
|
+
recorderWindow.focus();
|
|
270
|
+
}
|
|
271
|
+
// cleanUrl has jam-* params removed for your main window
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// Windows/Linux: Handle second instance
|
|
275
|
+
// Note: Remember to implement app.requestSingleInstanceLock() in your app
|
|
276
|
+
app.on("second-instance", (event, commandLine) => {
|
|
277
|
+
const url = commandLine.find((arg) => arg.startsWith("myapp://"));
|
|
278
|
+
if (url) {
|
|
279
|
+
const [cleanUrl, recorderWindow] = jam.openUrl(url);
|
|
280
|
+
if (recorderWindow) {
|
|
281
|
+
recorderWindow.focus();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Result:** Links like `myapp://open?jam-recording=abc123` will launch your app and open the recorder.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### 3. Enable In-App Recording (Optional)
|
|
292
|
+
|
|
293
|
+
Add a menu item to trigger recordings from within your app:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
import { Menu } from "electron";
|
|
297
|
+
import * as jam from "@jam.dev/recording-links/electron";
|
|
298
|
+
|
|
299
|
+
const template = [
|
|
300
|
+
{
|
|
301
|
+
label: "Help",
|
|
302
|
+
submenu: [
|
|
303
|
+
{
|
|
304
|
+
label: "Report a Bug",
|
|
305
|
+
click: () => {
|
|
306
|
+
jam.openRecorder("your-recording-id-here");
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Test:** Click "Report a Bug" → Recorder window opens and screen capture starts.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### API Reference
|
|
321
|
+
|
|
322
|
+
#### `initialize(config)`
|
|
323
|
+
|
|
324
|
+
Initializes the Jam SDK for Electron. Must be called before using other Jam functions.
|
|
325
|
+
|
|
326
|
+
**Parameters:**
|
|
327
|
+
|
|
328
|
+
- `config.defaultSession?: Session` - The session to install display media handler on. Defaults to `session.defaultSession`. If using multiple sessions, install handlers on each session.
|
|
329
|
+
|
|
330
|
+
- `config.defaultDisplayMediaRequestHandler?: DisplayMediaRequestHandler | null` - The display media request handler to install. In an upcoming version, this will introduce a "screen or window picker" UI to the Recorder window. Right now, it uses the OS picker on Mac, and default-selects the first window it sees in Windows + Linux. Set to `null` to handle display media requests yourself (see advanced example below).
|
|
331
|
+
|
|
332
|
+
- `config.openRecorderWindow(session: Session): BrowserWindow` **(required)** - Function that creates and returns a BrowserWindow for the recorder. Called once per session when opening a recorder.
|
|
333
|
+
|
|
334
|
+
- `config.loadRecorderPage(win: BrowserWindow, data: IJamData): Promise<void>` **(required)** - Function that loads your app with Jam recording parameters into the recorder window.
|
|
335
|
+
|
|
336
|
+
**Advanced Example with Custom Display Media Handler:**
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { BrowserWindow, desktopCapturer, session, webContents } from "electron";
|
|
340
|
+
import * as jam from "@jam.dev/recording-links/electron";
|
|
341
|
+
|
|
342
|
+
jam.initialize({
|
|
343
|
+
defaultDisplayMediaRequestHandler: null, // Don't install default handler
|
|
344
|
+
// ... other config
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Install custom handler per session
|
|
348
|
+
session.defaultSession.setDisplayMediaRequestHandler(
|
|
349
|
+
async (request, callback) => {
|
|
350
|
+
const frame = request.frame;
|
|
351
|
+
const contents = frame ? webContents.fromFrame(frame) : null;
|
|
352
|
+
const win = contents ? BrowserWindow.fromWebContents(contents) : null;
|
|
353
|
+
|
|
354
|
+
if (jam.isJamRecorder(win)) {
|
|
355
|
+
const sources = await desktopCapturer.getSources({
|
|
356
|
+
types: ["screen", "window"],
|
|
357
|
+
});
|
|
358
|
+
jam.handleDisplayMediaRequest(sources, callback, win);
|
|
359
|
+
} else {
|
|
360
|
+
// Your app's own display media handling
|
|
361
|
+
callback({});
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{ useSystemPicker: true },
|
|
365
|
+
);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `openRecorder(init, session?)`
|
|
369
|
+
|
|
370
|
+
Opens a Jam recorder window. If a recorder window is already open for the session, focuses it and loads the new recording.
|
|
371
|
+
|
|
372
|
+
**Parameters:**
|
|
373
|
+
|
|
374
|
+
- `init: string | { recordingId: string; jamTitle?: string } | URLSearchParams` - Recording ID, data object, or URL parameters
|
|
375
|
+
- `session?: Session` - Optional session (defaults to `defaultSession` from initialize)
|
|
376
|
+
|
|
377
|
+
**Returns:** `BrowserWindow` - The recorder window (returns immediately, page loads asynchronously)
|
|
378
|
+
|
|
379
|
+
**Examples:**
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// Open by recording ID
|
|
383
|
+
jam.openRecorder("abc123");
|
|
384
|
+
|
|
385
|
+
// Open with title
|
|
386
|
+
jam.openRecorder({ recordingId: "abc123", jamTitle: "Bug Report" });
|
|
387
|
+
|
|
388
|
+
// Open from URL parameters
|
|
389
|
+
const params = new URLSearchParams("jam-recording=abc123&jam-title=Bug");
|
|
390
|
+
jam.openRecorder(params);
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
#### `openUrl(url, session?)`
|
|
394
|
+
|
|
395
|
+
Parses a URL, extracts Jam recording parameters, and opens the recorder if parameters are found. Returns the cleaned URL (with Jam params removed) and the recorder window if opened.
|
|
396
|
+
|
|
397
|
+
**Parameters:**
|
|
398
|
+
|
|
399
|
+
- `url: string | URL` - URL to parse (e.g., from protocol handler)
|
|
400
|
+
- `session?: Session` - Optional session (defaults to `defaultSession` from initialize)
|
|
401
|
+
|
|
402
|
+
**Returns:** `[string, BrowserWindow | null]` - Tuple of cleaned URL and recorder window (or null)
|
|
403
|
+
|
|
404
|
+
**Example:**
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
app.on("open-url", (event, url) => {
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
const [cleanUrl, recorderWindow] = jam.openUrl(url);
|
|
410
|
+
|
|
411
|
+
if (recorderWindow) {
|
|
412
|
+
recorderWindow.focus();
|
|
413
|
+
} else {
|
|
414
|
+
// Open your main window with cleanUrl
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### `isJamRecorder(win)`
|
|
420
|
+
|
|
421
|
+
Checks if a BrowserWindow is a Jam recorder window.
|
|
422
|
+
|
|
423
|
+
**Parameters:**
|
|
424
|
+
|
|
425
|
+
- `win: BrowserWindow | null` - Window to check
|
|
426
|
+
|
|
427
|
+
**Returns:** `boolean` - True if window is a Jam recorder
|
|
428
|
+
|
|
429
|
+
**Example:**
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { BrowserWindow } from "electron";
|
|
433
|
+
import * as jam from "@jam.dev/recording-links/electron";
|
|
434
|
+
|
|
435
|
+
const win = BrowserWindow.getFocusedWindow();
|
|
436
|
+
if (jam.isJamRecorder(win)) {
|
|
437
|
+
console.log("This is a Jam recorder window");
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### `handleDisplayMediaRequest(sources, callback, requester?)`
|
|
442
|
+
|
|
443
|
+
Helper function for custom display media request handlers. Filters sources (excludes recorder window and DevTools), selects the best source, and calls the callback.
|
|
444
|
+
|
|
445
|
+
In an upcoming version, this function will implement a handshake with the Recorder window to display a "screen or window picker" UI. Right now, it auto-selects the first app window (or first screen if no windows are available).
|
|
446
|
+
|
|
447
|
+
**Parameters:**
|
|
448
|
+
|
|
449
|
+
- `sources: DesktopCapturerSource[]` - Sources from `desktopCapturer.getSources()`
|
|
450
|
+
- `callback: DisplayMediaRequestHandlerCallback` - Callback from display media request handler
|
|
451
|
+
- `requester?: BrowserWindow | null` - The requesting window (to exclude from sources)
|
|
452
|
+
|
|
453
|
+
**Example:**
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { BrowserWindow, desktopCapturer, session, webContents } from "electron";
|
|
457
|
+
import * as jam from "@jam.dev/recording-links/electron";
|
|
458
|
+
|
|
459
|
+
session.defaultSession.setDisplayMediaRequestHandler(
|
|
460
|
+
async (request, callback) => {
|
|
461
|
+
const sources = await desktopCapturer.getSources({
|
|
462
|
+
types: ["screen", "window"],
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const frame = request.frame;
|
|
466
|
+
const win = frame
|
|
467
|
+
? BrowserWindow.fromWebContents(webContents.fromFrame(frame))
|
|
468
|
+
: null;
|
|
469
|
+
|
|
470
|
+
jam.handleDisplayMediaRequest(sources, callback, win);
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
#### `IJamData` Interface
|
|
476
|
+
|
|
477
|
+
Data structure representing Jam recording parameters.
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
interface IJamData {
|
|
481
|
+
recordingId: string; // Jam recording ID
|
|
482
|
+
jamTitle: string | undefined | null; // Optional recording title
|
|
483
|
+
state: string | undefined | null; // Optional JWT state
|
|
484
|
+
searchParams: URLSearchParams; // jam-* query parameters
|
|
485
|
+
search: string; // Query string with '?' prefix (or empty)
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## What Jam Recording Links Enables for Your Electron App
|
|
492
|
+
|
|
493
|
+
Users can report bugs with one click:
|
|
494
|
+
|
|
495
|
+
- From your app menu → Instant screen recording starts
|
|
496
|
+
- From Jam Recording Links (via Slack, email) → App launches and recording begins
|
|
497
|
+
- Zero configuration of screen capture or window management
|
|
498
|
+
|
|
499
|
+
## What the SDK Provides
|
|
500
|
+
|
|
501
|
+
### Window Architecture
|
|
502
|
+
|
|
503
|
+
- Creates separate recorder window when `jam-*` query parameters detected
|
|
504
|
+
- Maintains main window alongside recorder (multi-window pattern)
|
|
505
|
+
- Auto-focuses or reuses existing recorder window if already open
|
|
506
|
+
- Prevents recorder window from closing while recording
|
|
507
|
+
|
|
508
|
+
### Screen Capture
|
|
509
|
+
|
|
510
|
+
- Filters capture sources to exclude recorder window (prevents recursive capture)
|
|
511
|
+
- Auto-excludes DevTools windows from source list
|
|
512
|
+
- Configures `desktopCapturer` with appropriate permissions
|
|
513
|
+
- TEMPORARY: Implements source selection strategy: prefer app windows, then screens
|
|
514
|
+
- WILL BE REPLACED with user source picker in future release
|
|
515
|
+
|
|
516
|
+
### Protocol Integration
|
|
517
|
+
|
|
518
|
+
- Parses `yourapp://open?jam-recording=ID` format URLs
|
|
519
|
+
- Routes protocol requests to dual-window opener
|
|
520
|
+
- Handles cross-platform differences (macOS `open-url` vs Windows/Linux `second-instance`)
|
|
521
|
+
- Validates and extracts `jam-*` query parameters
|
|
522
|
+
|
|
523
|
+
### Developer Configuration
|
|
524
|
+
|
|
525
|
+
**You provide:**
|
|
526
|
+
|
|
527
|
+
- **Protocol scheme**: Your app's custom URL scheme (e.g., "notion", "slack")
|
|
528
|
+
- **Web URLs**: Dev server (e.g., `http://localhost:3000`) and production path
|
|
529
|
+
- **Recording IDs**: Which Jam recording to open for bug reports (coming soon: dynamic values via API)
|
|
530
|
+
- **Trigger UI**: Where to place menu items or buttons in your app
|
|
531
|
+
|
|
532
|
+
**SDK handles everything else.**
|
|
533
|
+
|
|
186
534
|
## Contributing
|
|
187
535
|
|
|
188
536
|
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, build commands, and release process.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { BrowserWindow, type DesktopCapturerSource, type DisplayMediaRequestHandlerHandlerRequest, type Session, type WebFrameMain } from "electron";
|
|
2
|
+
type DisplayMediaRequestHandler = (request: DisplayMediaRequestHandlerHandlerRequest, callback: DisplayMediaRequestHandlerCallback) => void | Promise<void>;
|
|
3
|
+
type DisplayMediaRequestHandlerCallback = (streams: {
|
|
4
|
+
video?: WebFrameMain | {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
} | undefined;
|
|
8
|
+
audio?: "loopback" | "loopbackWithMute" | WebFrameMain | undefined;
|
|
9
|
+
enableLocalEcho?: boolean | undefined;
|
|
10
|
+
}) => void | Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Initialize the Jam SDK for Electron.
|
|
13
|
+
*
|
|
14
|
+
* @example Basic setup
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { app, BrowserWindow } from 'electron';
|
|
17
|
+
* import * as jam from '@jam.dev/recording-links/electron';
|
|
18
|
+
*
|
|
19
|
+
* jam.initialize({
|
|
20
|
+
* openRecorderWindow(session) {
|
|
21
|
+
* return new BrowserWindow({
|
|
22
|
+
* width: 1000,
|
|
23
|
+
* height: 700,
|
|
24
|
+
* webPreferences: {
|
|
25
|
+
* allowRunningInsecureContent: true,
|
|
26
|
+
* nodeIntegration: false,
|
|
27
|
+
* contextIsolation: true,
|
|
28
|
+
* sandbox: true,
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* },
|
|
32
|
+
* async loadRecorderPage(win, data) {
|
|
33
|
+
* await win.loadURL(`https://my-app.com${data.searchParams.toString()}`);
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example Multi-session app with custom display media handler
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { session, desktopCapturer, BrowserWindow } from 'electron';
|
|
41
|
+
* import * as jam from '@jam.dev/recording-links/electron';
|
|
42
|
+
*
|
|
43
|
+
* jam.initialize({
|
|
44
|
+
* defaultDisplayMediaRequestHandler: null, // Use custom handler per session
|
|
45
|
+
* openRecorderWindow(ses) {
|
|
46
|
+
* const win = new BrowserWindow({
|
|
47
|
+
* width: 1000,
|
|
48
|
+
* height: 700,
|
|
49
|
+
* webPreferences: {
|
|
50
|
+
* allowRunningInsecureContent: true,
|
|
51
|
+
* nodeIntegration: false,
|
|
52
|
+
* contextIsolation: true,
|
|
53
|
+
* sandbox: true,
|
|
54
|
+
* },
|
|
55
|
+
* });
|
|
56
|
+
* win.on('closed', () => console.log('Recorder closed'));
|
|
57
|
+
* return win;
|
|
58
|
+
* },
|
|
59
|
+
* async loadRecorderPage(win, data) {
|
|
60
|
+
* const recordingId = data.searchParams.get('jam-recording');
|
|
61
|
+
* await win.loadURL(`https://my-app.com/recorder?id=${recordingId}`);
|
|
62
|
+
* },
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Install handler on a specific session
|
|
66
|
+
* const mySession = session.fromPartition('persist:my-session');
|
|
67
|
+
* mySession.setDisplayMediaRequestHandler(async (request, callback) => {
|
|
68
|
+
* const frame = request.frame;
|
|
69
|
+
* const win = frame ? BrowserWindow.fromWebContents(frame.webContents) : null;
|
|
70
|
+
*
|
|
71
|
+
* if (jam.isJamRecorder(win)) {
|
|
72
|
+
* const sources = await desktopCapturer.getSources({ types: ['screen', 'window'] });
|
|
73
|
+
* jam.handleDisplayMediaRequest(sources, callback, win);
|
|
74
|
+
* } else {
|
|
75
|
+
* // Your app's default behavior
|
|
76
|
+
* callback({});
|
|
77
|
+
* }
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function initialize(config: {
|
|
82
|
+
/**
|
|
83
|
+
* The session upon which we will install Jam's display media request handler.
|
|
84
|
+
* If your app uses multiple sessions, please install a handler on each session
|
|
85
|
+
* for which Jam is installed; then in your handler, call
|
|
86
|
+
* `jam.handleDisplayMediaRequest(request, callback)`.
|
|
87
|
+
*
|
|
88
|
+
* @default session.defaultSession
|
|
89
|
+
*/
|
|
90
|
+
defaultSession?: Session | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Jam.js for Electron requires setting a display media request handler,
|
|
93
|
+
* which defines the app's implementation for
|
|
94
|
+
* `navigator.mediaDevices.getDisplayMedia(...)` calls.
|
|
95
|
+
*
|
|
96
|
+
* By default, we use:
|
|
97
|
+
*
|
|
98
|
+
* ```
|
|
99
|
+
* defaultSession.setDisplayMediaRequestHandler(
|
|
100
|
+
* (request, callback) => {
|
|
101
|
+
* if (request.frame.url === "https://jam.dev") {
|
|
102
|
+
* jam.handleDisplayMediaRequest(sources, callback);
|
|
103
|
+
* }
|
|
104
|
+
* }),
|
|
105
|
+
* { useSystemPicker: true },
|
|
106
|
+
* );
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* If your app manages multiple sessions, or sets its own display media
|
|
110
|
+
* request handler on the session, set this to `null` and either:
|
|
111
|
+
*
|
|
112
|
+
* - Select the default `source` and call the `callback` yourself; or
|
|
113
|
+
* - Call `jam.handleDisplayMediaRequest(sources, callback)` in your handler
|
|
114
|
+
* when the request originates from a Jam-originating frame
|
|
115
|
+
*/
|
|
116
|
+
defaultDisplayMediaRequestHandler?: DisplayMediaRequestHandler | null;
|
|
117
|
+
/**
|
|
118
|
+
* A function that takes one argument (a `Session` object) and opens a
|
|
119
|
+
* BrowserWindow we will use for recording.
|
|
120
|
+
*
|
|
121
|
+
* The window will be cached for reuse; when calling `loadRecorder`, Jam will
|
|
122
|
+
* only call this function if a previously-opened Recorder window for this
|
|
123
|
+
* session has been closed.
|
|
124
|
+
*/
|
|
125
|
+
openRecorderWindow(ses: Session): BrowserWindow;
|
|
126
|
+
/**
|
|
127
|
+
* A function that takes two arguments—a `BrowserWindow` (opened by
|
|
128
|
+
* `openRecorderWindow`) and an `IJamData` object used to route the window
|
|
129
|
+
* to the proper recorder configuration.
|
|
130
|
+
*/
|
|
131
|
+
loadRecorderPage(win: BrowserWindow, data: IJamData): Promise<void>;
|
|
132
|
+
}): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Opens a Jam recorder window for the given recording ID or data.
|
|
135
|
+
*
|
|
136
|
+
* @param init - Recording ID string, data object, or URLSearchParams
|
|
137
|
+
* @param ses - Optional session (defaults to defaultSession from initialize)
|
|
138
|
+
* @returns The recorder BrowserWindow
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* import * as jam from '@jam.dev/recording-links/electron';
|
|
143
|
+
*
|
|
144
|
+
* // Open by recording ID
|
|
145
|
+
* jam.openRecorder('abc123');
|
|
146
|
+
*
|
|
147
|
+
* // Open with title
|
|
148
|
+
* jam.openRecorder({ recordingId: 'abc123', title: 'Bug Report' });
|
|
149
|
+
*
|
|
150
|
+
* // Open with URLSearchParams (from protocol handler)
|
|
151
|
+
* const params = new URLSearchParams('jam-recording=abc123&jam-title=Bug+Report');
|
|
152
|
+
* jam.openRecorder(params);
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function openRecorder(init: string | JamData | ConstructorParameters<typeof JamData>[0], ses?: Session): BrowserWindow;
|
|
156
|
+
/**
|
|
157
|
+
* Syntactic sugar for opening the recorder from a URL.
|
|
158
|
+
* Returns a tuple of the un-jammed URL and the recorder window, if opened.
|
|
159
|
+
*/
|
|
160
|
+
export declare function openUrl(url: string | URL, ses?: Session): [string, BrowserWindow | null];
|
|
161
|
+
/**
|
|
162
|
+
* Data structure representing Jam recording parameters.
|
|
163
|
+
*/
|
|
164
|
+
export interface IJamData {
|
|
165
|
+
/** Jam recording ID */
|
|
166
|
+
readonly recordingId: string;
|
|
167
|
+
/** Optional recording title */
|
|
168
|
+
readonly title: string | undefined | null;
|
|
169
|
+
/**
|
|
170
|
+
* URLSearchParams containing jam-* query parameters.
|
|
171
|
+
* Follows the same convention as URL.searchParams.
|
|
172
|
+
*/
|
|
173
|
+
readonly searchParams: URLSearchParams;
|
|
174
|
+
/**
|
|
175
|
+
* Query string with leading '?' (or empty string if no params).
|
|
176
|
+
* Follows the same convention as URL.search - includes the '?' prefix when params exist.
|
|
177
|
+
*/
|
|
178
|
+
readonly search: string;
|
|
179
|
+
}
|
|
180
|
+
declare class JamData implements IJamData {
|
|
181
|
+
readonly recordingId: string;
|
|
182
|
+
readonly title: string | undefined | null;
|
|
183
|
+
get searchParams(): URLSearchParams;
|
|
184
|
+
get search(): string;
|
|
185
|
+
constructor(init: URLSearchParams | {
|
|
186
|
+
recordingId: string;
|
|
187
|
+
title?: string | null;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
export declare function isJamRecorder(win: BrowserWindow | null): boolean;
|
|
191
|
+
export declare function handleDisplayMediaRequest(sources: DesktopCapturerSource[], callback: DisplayMediaRequestHandlerCallback, requester?: BrowserWindow | null): void;
|
|
192
|
+
export {};
|
|
193
|
+
//# sourceMappingURL=electron.d.ts.map
|
package/lib/electron.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{app as e,session as r,webContents as t,BrowserWindow as n,desktopCapturer as o}from"electron";const s={defaultSession:null,windows:new Map,openRecorderWindow(){throw new Error("Not initialized")},loadRecorderPage(){throw new Error("Not initialized")}},i=(e,r)=>{const s=e.frame,i=s?t.fromFrame(s):null,a=i?n.fromWebContents(i):null;u(a)?o.getSources({types:["screen","window"]}).then(e=>f(e,r,a)).catch(e=>{r({})}):r({})};async function a(t){if(!e.isReady())return e.whenReady().then(()=>a(t));const{defaultSession:n=r.defaultSession,defaultDisplayMediaRequestHandler:o=i}=t;s.defaultSession=n,s.openRecorderWindow=t.openRecorderWindow,s.loadRecorderPage=t.loadRecorderPage,o&&n.setDisplayMediaRequestHandler(o,{useSystemPicker:!0})}function d(e,r){const t=r??s.defaultSession;if(null===t)throw new Error("Cannot open recorder: no `session` found or provided");let n=s.windows.get(t);n||(n=s.openRecorderWindow(t),s.windows.set(t,n),n.on("closed",()=>s.windows.delete(t)));const o="string"==typeof e?new l({recordingId:e}):e instanceof l?e:new l(e);return s.loadRecorderPage(n,o).catch(e=>{}),n.isMinimized()&&n.restore(),n.focus(),n}function c(e,r){const t="string"==typeof e?new URL(e):e,n=new URLSearchParams;for(const[e,r]of t.searchParams.entries())e.startsWith("jam-")&&(n.set(e,r),t.searchParams.delete(e));return[t.href,n.has("jam-recording")?d(new l(n),r):null]}class l{get searchParams(){const e=new URLSearchParams;return e.set("jam-recording",this.recordingId),this.title&&e.set("jam-title",this.title),e}get search(){const e=this.searchParams.toString();return e?`?${e}`:""}constructor(e){if(e instanceof URLSearchParams){const r=e.get("jam-recording");if(!r)throw new Error("Missing jam-recording parameter");this.recordingId=r,this.title=e.get("jam-title")}else this.recordingId=e.recordingId,this.title=e.title||null}}function u(e){for(const r of s.windows.values())if(r===e)return!0;return!1}function f(e,r,t){const n=t?.getMediaSourceId(),o=e.filter(e=>!e.name.includes("DevTools")&&e.id!==n),s=o.find(e=>e.id.startsWith("window:")),i=o.find(e=>e.id.startsWith("screen:")),a=s||i||o[0];r(a?{video:a,audio:"loopback"}:{})}export{f as handleDisplayMediaRequest,a as initialize,u as isJamRecorder,d as openRecorder,c as openUrl};//# sourceMappingURL=electron.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jam.dev/recording-links",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-electron.5",
|
|
4
4
|
"description": "Capture bug reports from your users with the Jam recording links SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jam",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"./lib/sdk": {
|
|
30
30
|
"types": "./lib/sdk.d.ts",
|
|
31
31
|
"import": "./lib/sdk.js"
|
|
32
|
+
},
|
|
33
|
+
"./electron": {
|
|
34
|
+
"types": "./lib/electron.d.ts",
|
|
35
|
+
"import": "./lib/electron.js"
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
38
|
"files": [
|
|
@@ -63,5 +67,8 @@
|
|
|
63
67
|
"tslib": "^2.8.1",
|
|
64
68
|
"typescript": "^5.8.2",
|
|
65
69
|
"vitest": "^2.1.8"
|
|
70
|
+
},
|
|
71
|
+
"optionalDependencies": {
|
|
72
|
+
"electron": "^39.2.3"
|
|
66
73
|
}
|
|
67
74
|
}
|