@dynamicu/chromedebug-mcp 2.7.1 → 2.7.4
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/CLAUDE.md +18 -0
- package/README.md +226 -16
- package/chrome-extension/background.js +569 -64
- package/chrome-extension/browser-recording-manager.js +34 -0
- package/chrome-extension/content.js +438 -32
- package/chrome-extension/firebase-config.public-sw.js +1 -1
- package/chrome-extension/firebase-config.public.js +1 -1
- package/chrome-extension/frame-capture.js +31 -10
- package/chrome-extension/image-processor.js +193 -0
- package/chrome-extension/manifest.free.json +1 -1
- package/chrome-extension/options.html +2 -2
- package/chrome-extension/options.js +4 -4
- package/chrome-extension/popup.html +82 -4
- package/chrome-extension/popup.js +1106 -38
- package/chrome-extension/pro/frame-editor.html +259 -6
- package/chrome-extension/pro/frame-editor.js +959 -10
- package/chrome-extension/pro/video-exporter.js +917 -0
- package/chrome-extension/pro/video-player.js +545 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/scripts/webpack.config.free.cjs +6 -0
- package/scripts/webpack.config.pro.cjs +6 -0
- package/src/chrome-controller.js +6 -6
- package/src/database.js +226 -39
- package/src/http-server.js +55 -11
- package/src/validation/schemas.js +20 -5
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayer - Canvas-based frame playback for Chrome Debug PRO
|
|
3
|
+
* Part of the video playback/export feature
|
|
4
|
+
*/
|
|
5
|
+
class VideoPlayer {
|
|
6
|
+
constructor(container, options = {}) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.canvas = null;
|
|
9
|
+
this.ctx = null;
|
|
10
|
+
|
|
11
|
+
// Frame data (passed from FrameEditor, not reloaded)
|
|
12
|
+
this.frames = [];
|
|
13
|
+
this.imageCache = new Map(); // frameIndex -> HTMLImageElement
|
|
14
|
+
|
|
15
|
+
// Playback state
|
|
16
|
+
this.currentFrameIndex = 0;
|
|
17
|
+
this.isPlaying = false;
|
|
18
|
+
this.playbackSpeed = 1.0;
|
|
19
|
+
this.animationId = null;
|
|
20
|
+
this.lastRenderTime = 0;
|
|
21
|
+
this.playbackStartTime = 0;
|
|
22
|
+
this.playbackStartFrame = 0;
|
|
23
|
+
|
|
24
|
+
// Display options
|
|
25
|
+
this.showLogs = options.showLogs !== false;
|
|
26
|
+
this.logPosition = options.logPosition || 'bottom';
|
|
27
|
+
this.showClickIndicators = true;
|
|
28
|
+
this.showMouseCursor = true;
|
|
29
|
+
|
|
30
|
+
// Interaction data
|
|
31
|
+
this.interactions = [];
|
|
32
|
+
this.originalWidth = null;
|
|
33
|
+
this.originalHeight = null;
|
|
34
|
+
|
|
35
|
+
// Preloading config
|
|
36
|
+
this.preloadBehind = 10;
|
|
37
|
+
this.preloadAhead = 40;
|
|
38
|
+
this.maxCacheSize = 50;
|
|
39
|
+
|
|
40
|
+
// Cached frame data for efficient overlay updates between frames
|
|
41
|
+
this.cachedFrameData = null;
|
|
42
|
+
this.cachedFrameIndex = -1;
|
|
43
|
+
|
|
44
|
+
// Viewport dimensions from recording (for accurate coordinate scaling)
|
|
45
|
+
this.recordedViewportWidth = null;
|
|
46
|
+
this.recordedViewportHeight = null;
|
|
47
|
+
|
|
48
|
+
// Callbacks
|
|
49
|
+
this.onFrameChange = options.onFrameChange || null;
|
|
50
|
+
this.onPlayStateChange = options.onPlayStateChange || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Initialize with frame data
|
|
54
|
+
async initialize(frames) {
|
|
55
|
+
this.frames = frames;
|
|
56
|
+
this.createCanvas();
|
|
57
|
+
await this.preloadFramesAround(0);
|
|
58
|
+
this.renderFrame(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createCanvas() {
|
|
62
|
+
this.canvas = document.createElement('canvas');
|
|
63
|
+
this.canvas.className = 'video-player-canvas';
|
|
64
|
+
// Use willReadFrequently for better performance with getImageData calls during playback
|
|
65
|
+
this.ctx = this.canvas.getContext('2d', { willReadFrequently: true });
|
|
66
|
+
this.container.appendChild(this.canvas);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Image loading with cache
|
|
70
|
+
async loadImage(frameIndex) {
|
|
71
|
+
if (this.imageCache.has(frameIndex)) {
|
|
72
|
+
return this.imageCache.get(frameIndex);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const frame = this.frames[frameIndex];
|
|
76
|
+
if (!frame || !frame.imageData) return null;
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const img = new Image();
|
|
80
|
+
img.onload = () => {
|
|
81
|
+
this.addToCache(frameIndex, img);
|
|
82
|
+
resolve(img);
|
|
83
|
+
};
|
|
84
|
+
img.onerror = reject;
|
|
85
|
+
img.src = frame.imageData.startsWith('data:')
|
|
86
|
+
? frame.imageData
|
|
87
|
+
: `data:image/jpeg;base64,${frame.imageData}`;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Cache management with LRU eviction
|
|
92
|
+
addToCache(frameIndex, image) {
|
|
93
|
+
if (this.imageCache.size >= this.maxCacheSize) {
|
|
94
|
+
// Remove frames furthest from current position
|
|
95
|
+
const entries = [...this.imageCache.keys()];
|
|
96
|
+
entries.sort((a, b) =>
|
|
97
|
+
Math.abs(a - this.currentFrameIndex) - Math.abs(b - this.currentFrameIndex)
|
|
98
|
+
);
|
|
99
|
+
// Remove furthest 10
|
|
100
|
+
for (let i = entries.length - 1; i >= this.maxCacheSize - 10; i--) {
|
|
101
|
+
this.imageCache.delete(entries[i]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
this.imageCache.set(frameIndex, image);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Preload frames around current position
|
|
108
|
+
async preloadFramesAround(centerIndex) {
|
|
109
|
+
const start = Math.max(0, centerIndex - this.preloadBehind);
|
|
110
|
+
const end = Math.min(this.frames.length - 1, centerIndex + this.preloadAhead);
|
|
111
|
+
|
|
112
|
+
const loadPromises = [];
|
|
113
|
+
for (let i = start; i <= end; i++) {
|
|
114
|
+
if (!this.imageCache.has(i)) {
|
|
115
|
+
loadPromises.push(this.loadImage(i).catch(() => null));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await Promise.all(loadPromises);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Render a specific frame
|
|
123
|
+
async renderFrame(frameIndex) {
|
|
124
|
+
if (frameIndex < 0 || frameIndex >= this.frames.length) return;
|
|
125
|
+
|
|
126
|
+
const img = await this.loadImage(frameIndex);
|
|
127
|
+
if (!img) return;
|
|
128
|
+
|
|
129
|
+
// Set canvas size to match image's intrinsic dimensions (not CSS-styled dimensions)
|
|
130
|
+
// Use naturalWidth/naturalHeight to get the actual image pixel dimensions
|
|
131
|
+
const imgWidth = img.naturalWidth || img.width;
|
|
132
|
+
const imgHeight = img.naturalHeight || img.height;
|
|
133
|
+
|
|
134
|
+
if (this.canvas.width !== imgWidth || this.canvas.height !== imgHeight) {
|
|
135
|
+
this.canvas.width = imgWidth;
|
|
136
|
+
this.canvas.height = imgHeight;
|
|
137
|
+
this.originalWidth = imgWidth;
|
|
138
|
+
this.originalHeight = imgHeight;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Clear and draw
|
|
142
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
143
|
+
this.ctx.drawImage(img, 0, 0);
|
|
144
|
+
|
|
145
|
+
// Cache the frame image data (before overlays) for efficient cursor updates
|
|
146
|
+
this.cachedFrameData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
147
|
+
this.cachedFrameIndex = frameIndex;
|
|
148
|
+
|
|
149
|
+
// Draw overlays (clicks and cursor)
|
|
150
|
+
const frame = this.frames[frameIndex];
|
|
151
|
+
if (frame) {
|
|
152
|
+
this.drawOverlays(frame);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.currentFrameIndex = frameIndex;
|
|
156
|
+
|
|
157
|
+
// Callback
|
|
158
|
+
if (this.onFrameChange) {
|
|
159
|
+
this.onFrameChange(frameIndex, this.frames[frameIndex]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Playback control
|
|
164
|
+
play() {
|
|
165
|
+
if (this.isPlaying) return;
|
|
166
|
+
if (this.currentFrameIndex >= this.frames.length - 1) {
|
|
167
|
+
this.seek(0); // Restart from beginning if at end
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.isPlaying = true;
|
|
171
|
+
this.playbackStartTime = performance.now();
|
|
172
|
+
this.playbackStartFrame = this.currentFrameIndex;
|
|
173
|
+
this.lastRenderTime = performance.now();
|
|
174
|
+
|
|
175
|
+
if (this.onPlayStateChange) this.onPlayStateChange(true);
|
|
176
|
+
|
|
177
|
+
this.playbackLoop();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
pause() {
|
|
181
|
+
this.isPlaying = false;
|
|
182
|
+
if (this.animationId) {
|
|
183
|
+
cancelAnimationFrame(this.animationId);
|
|
184
|
+
this.animationId = null;
|
|
185
|
+
}
|
|
186
|
+
if (this.onPlayStateChange) this.onPlayStateChange(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
playbackLoop() {
|
|
190
|
+
if (!this.isPlaying) return;
|
|
191
|
+
|
|
192
|
+
const now = performance.now();
|
|
193
|
+
const currentFrame = this.frames[this.currentFrameIndex];
|
|
194
|
+
const nextFrame = this.frames[this.currentFrameIndex + 1];
|
|
195
|
+
|
|
196
|
+
if (!nextFrame) {
|
|
197
|
+
this.pause();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Calculate time to next frame based on recording timestamps
|
|
202
|
+
const frameDuration = (nextFrame.timestamp - currentFrame.timestamp) / this.playbackSpeed;
|
|
203
|
+
|
|
204
|
+
if (now - this.lastRenderTime >= frameDuration) {
|
|
205
|
+
// Time to advance to next frame
|
|
206
|
+
this.currentFrameIndex++;
|
|
207
|
+
this.renderFrame(this.currentFrameIndex);
|
|
208
|
+
this.lastRenderTime = now;
|
|
209
|
+
|
|
210
|
+
// Preload ahead during playback
|
|
211
|
+
if (this.currentFrameIndex % 10 === 0) {
|
|
212
|
+
this.preloadFramesAround(this.currentFrameIndex);
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// Between frames - interpolate cursor position for smooth movement
|
|
216
|
+
// Calculate the interpolated timestamp based on elapsed time since last frame
|
|
217
|
+
const elapsedSinceFrame = (now - this.lastRenderTime) * this.playbackSpeed;
|
|
218
|
+
const interpolatedTimestamp = currentFrame.timestamp + elapsedSinceFrame;
|
|
219
|
+
|
|
220
|
+
// Only update overlays if we have cached frame data and mouse tracking data
|
|
221
|
+
if (this.cachedFrameData && this.cachedFrameIndex === this.currentFrameIndex &&
|
|
222
|
+
this.showMouseCursor && this.interactions.some(i => i.type === 'mousemove')) {
|
|
223
|
+
this.updateOverlaysAtTimestamp(interpolatedTimestamp);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.animationId = requestAnimationFrame(() => this.playbackLoop());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update overlays at a specific timestamp (for smooth cursor interpolation)
|
|
231
|
+
updateOverlaysAtTimestamp(timestamp) {
|
|
232
|
+
if (!this.cachedFrameData) return;
|
|
233
|
+
|
|
234
|
+
// Restore the cached frame image
|
|
235
|
+
this.ctx.putImageData(this.cachedFrameData, 0, 0);
|
|
236
|
+
|
|
237
|
+
// Draw overlays at the interpolated timestamp
|
|
238
|
+
if (this.showClickIndicators) {
|
|
239
|
+
const clicks = this.interactions.filter(i =>
|
|
240
|
+
i.type === 'click' && i.x && i.y &&
|
|
241
|
+
Math.abs(i.timestamp - timestamp) <= 500
|
|
242
|
+
);
|
|
243
|
+
clicks.forEach(click => {
|
|
244
|
+
const age = Math.abs(timestamp - click.timestamp);
|
|
245
|
+
this.drawClickRipple(click, age); // Pass full click object for per-interaction scaling
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (this.showMouseCursor) {
|
|
250
|
+
const mousePos = this.getMousePositionAtTimestamp(timestamp);
|
|
251
|
+
if (mousePos) {
|
|
252
|
+
this.drawMouseCursor(mousePos); // Pass full position object for viewport scaling
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Seek to specific frame
|
|
258
|
+
async seek(frameIndex) {
|
|
259
|
+
const targetFrame = Math.max(0, Math.min(frameIndex, this.frames.length - 1));
|
|
260
|
+
await this.preloadFramesAround(targetFrame);
|
|
261
|
+
await this.renderFrame(targetFrame);
|
|
262
|
+
this.lastRenderTime = performance.now();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Navigation
|
|
266
|
+
nextFrame() {
|
|
267
|
+
if (this.currentFrameIndex < this.frames.length - 1) {
|
|
268
|
+
this.seek(this.currentFrameIndex + 1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
prevFrame() {
|
|
273
|
+
if (this.currentFrameIndex > 0) {
|
|
274
|
+
this.seek(this.currentFrameIndex - 1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Speed control
|
|
279
|
+
setSpeed(speed) {
|
|
280
|
+
this.playbackSpeed = speed;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Get playback progress
|
|
284
|
+
getProgress() {
|
|
285
|
+
if (this.frames.length === 0) return 0;
|
|
286
|
+
return this.currentFrameIndex / (this.frames.length - 1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Get duration
|
|
290
|
+
getDuration() {
|
|
291
|
+
if (this.frames.length < 2) return 0;
|
|
292
|
+
return this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Get current time
|
|
296
|
+
getCurrentTime() {
|
|
297
|
+
if (this.frames.length === 0) return 0;
|
|
298
|
+
return this.frames[this.currentFrameIndex].timestamp - this.frames[0].timestamp;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Load interactions data and extract viewport dimensions
|
|
302
|
+
loadInteractions(interactions) {
|
|
303
|
+
this.interactions = interactions || [];
|
|
304
|
+
|
|
305
|
+
// Extract viewport dimensions from interaction data (mousemove and click events contain this)
|
|
306
|
+
// This is crucial for accurate coordinate scaling
|
|
307
|
+
const interactionWithViewport = this.interactions.find(i =>
|
|
308
|
+
(i.type === 'mousemove' || i.type === 'click') && i.viewportWidth && i.viewportHeight
|
|
309
|
+
);
|
|
310
|
+
if (interactionWithViewport) {
|
|
311
|
+
this.recordedViewportWidth = interactionWithViewport.viewportWidth;
|
|
312
|
+
this.recordedViewportHeight = interactionWithViewport.viewportHeight;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Get the scale factors for coordinate conversion
|
|
317
|
+
getScaleFactors() {
|
|
318
|
+
// Use recorded viewport dimensions if available, otherwise fall back to image dimensions
|
|
319
|
+
const refWidth = this.recordedViewportWidth || this.originalWidth || this.canvas.width;
|
|
320
|
+
const refHeight = this.recordedViewportHeight || this.originalHeight || this.canvas.height;
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
x: this.canvas.width / refWidth,
|
|
324
|
+
y: this.canvas.height / refHeight
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Get scale factors for a specific interaction (uses per-interaction viewport if available)
|
|
329
|
+
// This handles cases where viewport dimensions change during recording (e.g., browser resize)
|
|
330
|
+
getScaleFactorsForInteraction(interaction) {
|
|
331
|
+
// Prefer interaction-specific viewport dimensions, fall back to global recorded values
|
|
332
|
+
const refWidth = interaction.viewportWidth || this.recordedViewportWidth || this.originalWidth || this.canvas.width;
|
|
333
|
+
const refHeight = interaction.viewportHeight || this.recordedViewportHeight || this.originalHeight || this.canvas.height;
|
|
334
|
+
const dpr = interaction.devicePixelRatio || 1;
|
|
335
|
+
|
|
336
|
+
// CRITICAL FIX (per Gemini analysis):
|
|
337
|
+
//
|
|
338
|
+
// The problem: Click coordinates (clientX/Y) are in CSS pixels, but chrome.tabs.captureVisibleTab
|
|
339
|
+
// and getDisplayMedia capture at PHYSICAL pixel resolution (CSS pixels × devicePixelRatio).
|
|
340
|
+
//
|
|
341
|
+
// On a 2x Retina display with 1440px CSS viewport:
|
|
342
|
+
// - Physical pixels captured: 1440 × 2 = 2880
|
|
343
|
+
// - Screenshot (after constraints): e.g., 640px (scaled down from 2880)
|
|
344
|
+
// - WRONG scale: 640 / 1440 = 0.444 (too large!)
|
|
345
|
+
// - CORRECT scale: 640 / (1440 × 2) = 0.222
|
|
346
|
+
//
|
|
347
|
+
// The fix: Divide by devicePixelRatio to convert CSS viewport to physical pixel space
|
|
348
|
+
// before calculating the scale factor.
|
|
349
|
+
|
|
350
|
+
// Convert viewport dimensions to physical pixel space (what the screenshot was captured from)
|
|
351
|
+
const physicalRefWidth = refWidth * dpr;
|
|
352
|
+
const physicalRefHeight = refHeight * dpr;
|
|
353
|
+
|
|
354
|
+
// Calculate scale factors (canvas pixels / physical viewport pixels)
|
|
355
|
+
let scaleX = this.canvas.width / physicalRefWidth;
|
|
356
|
+
let scaleY = this.canvas.height / physicalRefHeight;
|
|
357
|
+
|
|
358
|
+
// Diagnostic logging for debugging click alignment issues
|
|
359
|
+
console.log('[VideoPlayer] Scale factors:', {
|
|
360
|
+
canvasWidth: this.canvas.width,
|
|
361
|
+
canvasHeight: this.canvas.height,
|
|
362
|
+
refWidth,
|
|
363
|
+
refHeight,
|
|
364
|
+
dpr,
|
|
365
|
+
physicalRefWidth,
|
|
366
|
+
physicalRefHeight,
|
|
367
|
+
scaleX,
|
|
368
|
+
scaleY,
|
|
369
|
+
interactionX: interaction.x,
|
|
370
|
+
interactionY: interaction.y,
|
|
371
|
+
calculatedCanvasX: interaction.x * scaleX,
|
|
372
|
+
calculatedCanvasY: interaction.y * scaleY
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return { x: scaleX, y: scaleY };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Get mouse position at timestamp (interpolated)
|
|
379
|
+
// Returns full position object with viewport dimensions for accurate scaling
|
|
380
|
+
getMousePositionAtTimestamp(timestamp) {
|
|
381
|
+
const mouseMoves = this.interactions.filter(i => i.type === 'mousemove');
|
|
382
|
+
if (mouseMoves.length === 0) return null;
|
|
383
|
+
|
|
384
|
+
let before = null;
|
|
385
|
+
let after = null;
|
|
386
|
+
|
|
387
|
+
for (const move of mouseMoves) {
|
|
388
|
+
if (move.timestamp <= timestamp) {
|
|
389
|
+
before = move;
|
|
390
|
+
} else if (!after && move.timestamp > timestamp) {
|
|
391
|
+
after = move;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!before) return after ? {
|
|
397
|
+
x: after.x,
|
|
398
|
+
y: after.y,
|
|
399
|
+
viewportWidth: after.viewportWidth,
|
|
400
|
+
viewportHeight: after.viewportHeight,
|
|
401
|
+
devicePixelRatio: after.devicePixelRatio
|
|
402
|
+
} : null;
|
|
403
|
+
|
|
404
|
+
if (!after) return {
|
|
405
|
+
x: before.x,
|
|
406
|
+
y: before.y,
|
|
407
|
+
viewportWidth: before.viewportWidth,
|
|
408
|
+
viewportHeight: before.viewportHeight,
|
|
409
|
+
devicePixelRatio: before.devicePixelRatio
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// Interpolate position, use 'before' viewport for scaling
|
|
413
|
+
const t = (timestamp - before.timestamp) / (after.timestamp - before.timestamp);
|
|
414
|
+
return {
|
|
415
|
+
x: before.x + (after.x - before.x) * t,
|
|
416
|
+
y: before.y + (after.y - before.y) * t,
|
|
417
|
+
viewportWidth: before.viewportWidth,
|
|
418
|
+
viewportHeight: before.viewportHeight,
|
|
419
|
+
devicePixelRatio: before.devicePixelRatio
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Draw click ripple effect - DISABLED
|
|
424
|
+
// The complex scaling calculation was causing click indicators to appear in wrong positions.
|
|
425
|
+
// Click visualization is now handled by drawMouseCursor which shows a click circle when
|
|
426
|
+
// there's a click at the current timestamp (using the working mousemove coordinate system).
|
|
427
|
+
drawClickRipple(click, age, maxAge = 500) {
|
|
428
|
+
// Intentionally empty - click indicators now shown at cursor position
|
|
429
|
+
// See drawMouseCursor() for click visualization
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Draw mouse cursor with optional click indicator
|
|
433
|
+
// Accepts position object with viewport dimensions for per-interaction scaling
|
|
434
|
+
// When isClicking is true, draws a larger click circle around the cursor
|
|
435
|
+
drawMouseCursor(posObj, isClicking = false, clickAge = 0) {
|
|
436
|
+
// Use per-interaction viewport scaling for accurate positioning
|
|
437
|
+
const scale = this.getScaleFactorsForInteraction(posObj);
|
|
438
|
+
const cx = posObj.x * scale.x;
|
|
439
|
+
const cy = posObj.y * scale.y;
|
|
440
|
+
|
|
441
|
+
this.ctx.save();
|
|
442
|
+
this.ctx.translate(cx, cy);
|
|
443
|
+
|
|
444
|
+
// Draw click indicator circle (bigger, more visible) when clicking
|
|
445
|
+
if (isClicking) {
|
|
446
|
+
const maxAge = 500;
|
|
447
|
+
const progress = Math.min(clickAge / maxAge, 1);
|
|
448
|
+
const alpha = 1 - progress;
|
|
449
|
+
const radius = 15 + (25 * progress); // Starts at 15, expands to 40
|
|
450
|
+
|
|
451
|
+
// Outer expanding ring
|
|
452
|
+
this.ctx.beginPath();
|
|
453
|
+
this.ctx.arc(0, 0, radius, 0, Math.PI * 2);
|
|
454
|
+
this.ctx.strokeStyle = `rgba(76, 175, 80, ${alpha})`; // Green color
|
|
455
|
+
this.ctx.lineWidth = 3;
|
|
456
|
+
this.ctx.stroke();
|
|
457
|
+
|
|
458
|
+
// Inner solid circle
|
|
459
|
+
this.ctx.beginPath();
|
|
460
|
+
this.ctx.arc(0, 0, 8, 0, Math.PI * 2);
|
|
461
|
+
this.ctx.fillStyle = `rgba(76, 175, 80, ${alpha * 0.6})`;
|
|
462
|
+
this.ctx.fill();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Draw cursor arrow
|
|
466
|
+
this.ctx.fillStyle = 'white';
|
|
467
|
+
this.ctx.strokeStyle = 'black';
|
|
468
|
+
this.ctx.lineWidth = 1;
|
|
469
|
+
|
|
470
|
+
this.ctx.beginPath();
|
|
471
|
+
this.ctx.moveTo(0, 0);
|
|
472
|
+
this.ctx.lineTo(0, 18);
|
|
473
|
+
this.ctx.lineTo(4, 14);
|
|
474
|
+
this.ctx.lineTo(8, 22);
|
|
475
|
+
this.ctx.lineTo(11, 21);
|
|
476
|
+
this.ctx.lineTo(7, 13);
|
|
477
|
+
this.ctx.lineTo(13, 13);
|
|
478
|
+
this.ctx.closePath();
|
|
479
|
+
|
|
480
|
+
this.ctx.fill();
|
|
481
|
+
this.ctx.stroke();
|
|
482
|
+
this.ctx.restore();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Draw all overlays
|
|
486
|
+
drawOverlays(frame) {
|
|
487
|
+
const timestamp = frame.timestamp;
|
|
488
|
+
|
|
489
|
+
// Find if there's an active click near this timestamp
|
|
490
|
+
let activeClick = null;
|
|
491
|
+
let clickAge = 0;
|
|
492
|
+
if (this.showClickIndicators) {
|
|
493
|
+
const clicks = this.interactions.filter(i =>
|
|
494
|
+
i.type === 'click' && i.x && i.y &&
|
|
495
|
+
Math.abs(i.timestamp - timestamp) <= 500
|
|
496
|
+
);
|
|
497
|
+
if (clicks.length > 0) {
|
|
498
|
+
// Find the closest click
|
|
499
|
+
activeClick = clicks.reduce((closest, click) => {
|
|
500
|
+
const age = Math.abs(timestamp - click.timestamp);
|
|
501
|
+
const closestAge = Math.abs(timestamp - closest.timestamp);
|
|
502
|
+
return age < closestAge ? click : closest;
|
|
503
|
+
});
|
|
504
|
+
clickAge = Math.abs(timestamp - activeClick.timestamp);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Draw mouse cursor with click indicator if there's an active click
|
|
509
|
+
if (this.showMouseCursor) {
|
|
510
|
+
const mousePos = this.getMousePositionAtTimestamp(timestamp);
|
|
511
|
+
if (mousePos) {
|
|
512
|
+
const isClicking = activeClick !== null;
|
|
513
|
+
this.drawMouseCursor(mousePos, isClicking, clickAge);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Setters for overlay options
|
|
519
|
+
setShowClickIndicators(show) {
|
|
520
|
+
this.showClickIndicators = show;
|
|
521
|
+
this.renderFrame(this.currentFrameIndex);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
setShowMouseCursor(show) {
|
|
525
|
+
this.showMouseCursor = show;
|
|
526
|
+
this.renderFrame(this.currentFrameIndex);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Cleanup - CRITICAL for memory management
|
|
530
|
+
destroy() {
|
|
531
|
+
this.pause();
|
|
532
|
+
this.imageCache.clear();
|
|
533
|
+
if (this.canvas && this.canvas.parentNode) {
|
|
534
|
+
this.canvas.parentNode.removeChild(this.canvas);
|
|
535
|
+
}
|
|
536
|
+
this.canvas = null;
|
|
537
|
+
this.ctx = null;
|
|
538
|
+
this.frames = [];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Export for use in frame-editor.js
|
|
543
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
544
|
+
module.exports = VideoPlayer;
|
|
545
|
+
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamicu/chromedebug-mcp",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.4",
|
|
4
4
|
"description": "ChromeDebug MCP - MCP server that provides full control over a Chrome browser instance for debugging and automation with AI assistants like Claude Code",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
package/scripts/postinstall.js
CHANGED
|
@@ -46,6 +46,6 @@ console.log('ℹ️ No manual server startup needed!');
|
|
|
46
46
|
console.log(' Claude Code automatically starts both servers when it runs chromedebug-mcp\n');
|
|
47
47
|
|
|
48
48
|
console.log('💎 Want unlimited recordings?');
|
|
49
|
-
console.log(' Upgrade to Pro: https://chromedebug.com/buy/996773cb-682b-430f-b9e3-9ce2130bd967\n');
|
|
49
|
+
console.log(' Upgrade to Pro: https://chromedebug.com/checkout/buy/996773cb-682b-430f-b9e3-9ce2130bd967\n');
|
|
50
50
|
|
|
51
51
|
console.log('📚 Full documentation: https://github.com/dynamicupgrade/ChromeDebug#readme\n');
|
|
@@ -70,6 +70,12 @@ module.exports = {
|
|
|
70
70
|
// Copy Pro features needed for frame editor
|
|
71
71
|
{ from: 'chrome-extension/pro/jszip.min.js', to: 'pro/jszip.min.js' },
|
|
72
72
|
|
|
73
|
+
// Video player feature files (needed for frame editor styling)
|
|
74
|
+
{ from: 'chrome-extension/pro/video-player.js', to: 'pro/video-player.js' },
|
|
75
|
+
{ from: 'chrome-extension/pro/video-exporter.js', to: 'pro/video-exporter.js' },
|
|
76
|
+
{ from: 'chrome-extension/pro/video-player.css', to: 'pro/video-player.css' },
|
|
77
|
+
{ from: 'chrome-extension/pro/lib/mp4-muxer.min.js', to: 'pro/lib/mp4-muxer.min.js' },
|
|
78
|
+
|
|
73
79
|
// Copy README
|
|
74
80
|
{ from: 'chrome-extension/README.md', to: 'README.md' }
|
|
75
81
|
]
|
|
@@ -78,6 +78,12 @@ module.exports = {
|
|
|
78
78
|
{ from: 'chrome-extension/pro/enhanced-capture.js', to: 'pro/enhanced-capture.js' },
|
|
79
79
|
{ from: 'chrome-extension/pro/jszip.min.js', to: 'pro/jszip.min.js' },
|
|
80
80
|
|
|
81
|
+
// Video player feature files
|
|
82
|
+
{ from: 'chrome-extension/pro/video-player.js', to: 'pro/video-player.js' },
|
|
83
|
+
{ from: 'chrome-extension/pro/video-exporter.js', to: 'pro/video-exporter.js' },
|
|
84
|
+
{ from: 'chrome-extension/pro/video-player.css', to: 'pro/video-player.css' },
|
|
85
|
+
{ from: 'chrome-extension/pro/lib/mp4-muxer.min.js', to: 'pro/lib/mp4-muxer.min.js' },
|
|
86
|
+
|
|
81
87
|
// Copy README
|
|
82
88
|
{ from: 'chrome-extension/README.md', to: 'README.md' }
|
|
83
89
|
]
|
package/src/chrome-controller.js
CHANGED
|
@@ -1978,12 +1978,12 @@ export class ChromeController {
|
|
|
1978
1978
|
}
|
|
1979
1979
|
|
|
1980
1980
|
// Workflow recording methods
|
|
1981
|
-
async storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name = null, screenshotSettings = null, functionTraces = null) {
|
|
1981
|
+
async storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name = null, screenshotSettings = null, functionTraces = null, recordingMode = 'workflow') {
|
|
1982
1982
|
try {
|
|
1983
1983
|
const { database } = await import('./database.js');
|
|
1984
1984
|
|
|
1985
1985
|
// Store the workflow recording
|
|
1986
|
-
const workflowId = database.storeWorkflowRecording(sessionId, url, title, includeLogs, name, screenshotSettings);
|
|
1986
|
+
const workflowId = database.storeWorkflowRecording(sessionId, url, title, includeLogs, name, screenshotSettings, recordingMode);
|
|
1987
1987
|
|
|
1988
1988
|
// Merge function traces into actions if provided
|
|
1989
1989
|
let allActions = [...actions];
|
|
@@ -2035,10 +2035,10 @@ export class ChromeController {
|
|
|
2035
2035
|
}
|
|
2036
2036
|
}
|
|
2037
2037
|
|
|
2038
|
-
async listWorkflowRecordings() {
|
|
2038
|
+
async listWorkflowRecordings(recordingModeFilter = null) {
|
|
2039
2039
|
try {
|
|
2040
2040
|
const { database } = await import('./database.js');
|
|
2041
|
-
const recordings = database.listWorkflowRecordings();
|
|
2041
|
+
const recordings = database.listWorkflowRecordings(recordingModeFilter);
|
|
2042
2042
|
return { recordings };
|
|
2043
2043
|
} catch (error) {
|
|
2044
2044
|
logger.error('Error listing workflow recordings:', error);
|
|
@@ -2213,10 +2213,10 @@ export class ChromeController {
|
|
|
2213
2213
|
}
|
|
2214
2214
|
}
|
|
2215
2215
|
|
|
2216
|
-
async storeFrameBatch(sessionId, frames, sessionName = null) {
|
|
2216
|
+
async storeFrameBatch(sessionId, frames, sessionName = null, isVideoMode = false) {
|
|
2217
2217
|
try {
|
|
2218
2218
|
const { database } = await import('./database.js');
|
|
2219
|
-
const result = await database.storeFrameBatch(sessionId, frames, sessionName);
|
|
2219
|
+
const result = await database.storeFrameBatch(sessionId, frames, sessionName, isVideoMode);
|
|
2220
2220
|
|
|
2221
2221
|
// After storing frames, attempt to associate any deferred logs
|
|
2222
2222
|
if (result && result.id) {
|