@midscene/visualizer 1.7.7-beta-20260429033400.0 → 1.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
1
  import { mouseLoading, mousePointer } from "../../../utils/index.mjs";
2
2
  import { getCenterHighlightBox } from "../../../utils/highlight-element.mjs";
3
- import { deriveFrameState } from "./derive-frame-state.mjs";
3
+ import { deriveFrameState, shouldRenderCursor } from "./derive-frame-state.mjs";
4
4
  import { getPlaybackViewport } from "./playback-layout.mjs";
5
+ import { resolveExportPointerLayout, resolveSpinnerLayout } from "./pointer-layout.mjs";
5
6
  function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
6
7
  try {
7
8
  var info = gen[key](arg);
@@ -28,16 +29,64 @@ function _async_to_generator(fn) {
28
29
  });
29
30
  };
30
31
  }
32
+ function _define_property(obj, key, value) {
33
+ if (key in obj) Object.defineProperty(obj, key, {
34
+ value: value,
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true
38
+ });
39
+ else obj[key] = value;
40
+ return obj;
41
+ }
42
+ function _object_spread(target) {
43
+ for(var i = 1; i < arguments.length; i++){
44
+ var source = null != arguments[i] ? arguments[i] : {};
45
+ var ownKeys = Object.keys(source);
46
+ if ("function" == typeof Object.getOwnPropertySymbols) ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
47
+ return Object.getOwnPropertyDescriptor(source, sym).enumerable;
48
+ }));
49
+ ownKeys.forEach(function(key) {
50
+ _define_property(target, key, source[key]);
51
+ });
52
+ }
53
+ return target;
54
+ }
55
+ function export_branded_video_ownKeys(object, enumerableOnly) {
56
+ var keys = Object.keys(object);
57
+ if (Object.getOwnPropertySymbols) {
58
+ var symbols = Object.getOwnPropertySymbols(object);
59
+ if (enumerableOnly) symbols = symbols.filter(function(sym) {
60
+ return Object.getOwnPropertyDescriptor(object, sym).enumerable;
61
+ });
62
+ keys.push.apply(keys, symbols);
63
+ }
64
+ return keys;
65
+ }
66
+ function _object_spread_props(target, source) {
67
+ source = null != source ? source : {};
68
+ if (Object.getOwnPropertyDescriptors) Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
69
+ else export_branded_video_ownKeys(Object(source)).forEach(function(key) {
70
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
71
+ });
72
+ return target;
73
+ }
31
74
  const W = 960;
32
75
  const H = 540;
33
76
  const POINTER_PHASE = 0.375;
34
77
  const CROSSFADE_FRAMES = 10;
78
+ const EXPORT_STALL_GRACE_MS = 2000;
79
+ const EXPORT_STALL_GRACE_FRAMES = 10;
80
+ let activeExport = false;
35
81
  function clamp(v, lo, hi) {
36
82
  return Math.min(Math.max(v, lo), hi);
37
83
  }
38
84
  function lerp(a, b, t) {
39
85
  return a + (b - a) * t;
40
86
  }
87
+ function isExportRenderStalled(elapsedSinceLastFrameMs, frameDurationMs) {
88
+ return elapsedSinceLastFrameMs > Math.max(EXPORT_STALL_GRACE_MS, frameDurationMs * EXPORT_STALL_GRACE_FRAMES);
89
+ }
41
90
  function resolveExportCamera(prevCamera, camera, imageWidth, progress, autoZoom) {
42
91
  if (!autoZoom) return {
43
92
  camLeft: 0,
@@ -59,60 +108,68 @@ function loadImage(src) {
59
108
  img.src = src;
60
109
  });
61
110
  }
62
- function drawInsightOverlays(ctx, insights, cameraTransform, bx, contentY) {
111
+ function projectNativeRectToExportViewport(rect, cameraTransform, viewport) {
112
+ const scaleX = viewport.contentWidth / viewport.imageWidth;
113
+ const scaleY = viewport.contentHeight / viewport.imageHeight;
114
+ return {
115
+ left: viewport.offsetX + (rect.left * scaleX + cameraTransform.tx) * cameraTransform.zoom,
116
+ top: viewport.offsetY + (rect.top * scaleY + cameraTransform.ty) * cameraTransform.zoom,
117
+ width: rect.width * scaleX * cameraTransform.zoom,
118
+ height: rect.height * scaleY * cameraTransform.zoom
119
+ };
120
+ }
121
+ function drawInsightOverlays(ctx, insights, cameraTransform, viewport) {
122
+ ctx.save();
123
+ ctx.beginPath();
124
+ ctx.rect(viewport.offsetX, viewport.offsetY, viewport.contentWidth, viewport.contentHeight);
125
+ ctx.clip();
63
126
  for (const insight of insights)if (!(insight.alpha <= 0)) {
64
127
  ctx.save();
65
128
  ctx.globalAlpha *= insight.alpha;
66
129
  if (insight.highlightElement) {
67
130
  const highlightBox = getCenterHighlightBox(insight.highlightElement);
68
- const rx = bx + (highlightBox.left * cameraTransform.zoom + cameraTransform.tx * cameraTransform.zoom);
69
- const ry = contentY + (highlightBox.top * cameraTransform.zoom + cameraTransform.ty * cameraTransform.zoom);
70
- const highlightWidth = highlightBox.width * cameraTransform.zoom;
71
- const highlightHeight = highlightBox.height * cameraTransform.zoom;
131
+ const projected = projectNativeRectToExportViewport(highlightBox, cameraTransform, viewport);
72
132
  ctx.fillStyle = 'rgba(253, 89, 7, 0.4)';
73
- ctx.fillRect(rx, ry, highlightWidth, highlightHeight);
133
+ ctx.fillRect(projected.left, projected.top, projected.width, projected.height);
74
134
  ctx.strokeStyle = '#fd5907';
75
135
  ctx.lineWidth = 1;
76
- ctx.strokeRect(rx, ry, highlightWidth, highlightHeight);
136
+ ctx.strokeRect(projected.left, projected.top, projected.width, projected.height);
77
137
  ctx.shadowColor = 'rgba(51, 51, 51, 0.4)';
78
138
  ctx.shadowBlur = 2;
79
139
  ctx.shadowOffsetX = 4;
80
140
  ctx.shadowOffsetY = 4;
81
- ctx.strokeRect(rx, ry, highlightWidth, highlightHeight);
141
+ ctx.strokeRect(projected.left, projected.top, projected.width, projected.height);
82
142
  ctx.shadowBlur = 0;
83
143
  ctx.shadowOffsetX = 0;
84
144
  ctx.shadowOffsetY = 0;
85
145
  }
86
146
  if (insight.searchArea) {
87
- const r = insight.searchArea;
88
- const rx = bx + (r.left * cameraTransform.zoom + cameraTransform.tx * cameraTransform.zoom);
89
- const ry = contentY + (r.top * cameraTransform.zoom + cameraTransform.ty * cameraTransform.zoom);
90
- const rw = r.width * cameraTransform.zoom;
91
- const rh = r.height * cameraTransform.zoom;
147
+ const projected = projectNativeRectToExportViewport(insight.searchArea, cameraTransform, viewport);
92
148
  ctx.fillStyle = 'rgba(2, 131, 145, 0.4)';
93
- ctx.fillRect(rx, ry, rw, rh);
149
+ ctx.fillRect(projected.left, projected.top, projected.width, projected.height);
94
150
  ctx.strokeStyle = '#028391';
95
151
  ctx.lineWidth = 1;
96
- ctx.strokeRect(rx, ry, rw, rh);
152
+ ctx.strokeRect(projected.left, projected.top, projected.width, projected.height);
97
153
  }
98
154
  ctx.restore();
99
155
  }
156
+ ctx.restore();
100
157
  }
101
- function drawSpinningPointer(ctx, img, x, y, elapsedMs) {
158
+ function drawSpinningPointer(ctx, img, x, y, layout, elapsedMs) {
102
159
  const progress = (Math.sin(elapsedMs / 500 - Math.PI / 2) + 1) / 2;
103
160
  const rotation = progress * Math.PI * 2;
104
161
  ctx.save();
105
162
  ctx.translate(x, y);
106
163
  ctx.rotate(rotation);
107
- ctx.drawImage(img, -11, -14, 22, 28);
164
+ ctx.drawImage(img, -layout.centerOffsetX, -layout.centerOffsetY, layout.width, layout.height);
108
165
  ctx.restore();
109
166
  }
110
- function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg, autoZoom) {
167
+ function drawSteps(ctx, stepsFrame, frameMap, imgCache, pointerCache, spinnerImg, autoZoom) {
111
168
  const { scriptFrames, imageWidth: baseW, imageHeight: baseH, fps } = frameMap;
112
169
  const st = deriveFrameState(scriptFrames, stepsFrame, baseW, baseH, fps);
113
170
  if (!st.img) return;
114
- const { img, prevImg, imageWidth: imgW, imageHeight: imgH, camera, prevCamera, pointerMoved, imageChanged, rawProgress, frameInScript: fInScript, spinning, spinningElapsedMs, insights } = st;
115
- const pT = pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress;
171
+ const { img, prevImg, imageWidth: imgW, imageHeight: imgH, camera, prevCamera, pointerMoved, imageChanged, rawProgress, frameInScript: fInScript, spinning, spinningElapsedMs, currentPointerImg, pointerVisible, insights } = st;
172
+ const pT = autoZoom ? pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress : 1;
116
173
  const cT = pointerMoved ? rawProgress <= POINTER_PHASE ? 0 : Math.min((rawProgress - POINTER_PHASE) / (1 - POINTER_PHASE), 1) : rawProgress;
117
174
  const { camLeft: camL, camTop: camT2, camWidth: camW } = resolveExportCamera(prevCamera, camera, imgW, cT, autoZoom);
118
175
  const ptrX = lerp(prevCamera.pointerLeft, camera.pointerLeft, pT);
@@ -143,15 +200,42 @@ function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg, a
143
200
  zoom,
144
201
  tx,
145
202
  ty
146
- }, offsetX, offsetY);
203
+ }, {
204
+ offsetX,
205
+ offsetY,
206
+ contentWidth,
207
+ contentHeight,
208
+ imageWidth: imgW,
209
+ imageHeight: imgH
210
+ });
147
211
  const camH = imgH / imgW * camW;
148
212
  const sX = offsetX + (ptrX - camL) / camW * contentWidth;
149
213
  const sY = offsetY + (ptrY - camT2) / camH * contentHeight;
150
- const hasPtrData = Math.abs(camera.pointerLeft - Math.round(imgW / 2)) > 1 || Math.abs(camera.pointerTop - Math.round(imgH / 2)) > 1 || Math.abs(prevCamera.pointerLeft - Math.round(imgW / 2)) > 1 || Math.abs(prevCamera.pointerTop - Math.round(imgH / 2)) > 1;
151
- if (spinning && spinnerImg) drawSpinningPointer(ctx, spinnerImg, sX, sY, spinningElapsedMs);
152
- if (!spinning && hasPtrData && cursorImg) ctx.drawImage(cursorImg, sX - 3, sY - 2, 22, 28);
214
+ const pointerLayout = resolveExportPointerLayout(imgW, contentWidth);
215
+ const spinnerLayout = resolveSpinnerLayout(pointerLayout);
216
+ var _pointerCache_get;
217
+ const cursorImg = null != (_pointerCache_get = pointerCache.get(currentPointerImg)) ? _pointerCache_get : pointerCache.get(mousePointer);
218
+ const showCursor = shouldRenderCursor(pointerVisible, camera, prevCamera, imgW, imgH);
219
+ if (spinning && spinnerImg) drawSpinningPointer(ctx, spinnerImg, sX, sY, _object_spread_props(_object_spread({}, pointerLayout), {
220
+ width: spinnerLayout.size,
221
+ height: spinnerLayout.size,
222
+ centerOffsetX: spinnerLayout.centerOffset,
223
+ centerOffsetY: spinnerLayout.centerOffset
224
+ }), spinningElapsedMs);
225
+ if (!spinning && showCursor && cursorImg) ctx.drawImage(cursorImg, sX - pointerLayout.hotspotX, sY - pointerLayout.hotspotY, pointerLayout.width, pointerLayout.height);
153
226
  }
154
227
  function exportBrandedVideo(frameMap, options, onProgress) {
228
+ return _async_to_generator(function*() {
229
+ if (activeExport) throw new Error('Video export is already in progress');
230
+ activeExport = true;
231
+ try {
232
+ yield runExportBrandedVideo(frameMap, options, onProgress);
233
+ } finally{
234
+ activeExport = false;
235
+ }
236
+ })();
237
+ }
238
+ function runExportBrandedVideo(frameMap, options, onProgress) {
155
239
  return _async_to_generator(function*() {
156
240
  const { totalDurationInFrames: total, fps } = frameMap;
157
241
  var _options_autoZoom;
@@ -166,11 +250,19 @@ function exportBrandedVideo(frameMap, options, onProgress) {
166
250
  imgCache.set(src, (yield loadImage(src)));
167
251
  } catch (e) {}
168
252
  })()));
169
- let cursorImg = null;
253
+ const pointerSrcs = new Set([
254
+ mousePointer
255
+ ]);
256
+ for (const sf of frameMap.scriptFrames)if (sf.pointerImg) pointerSrcs.add(sf.pointerImg);
257
+ const pointerCache = new Map();
258
+ yield Promise.all([
259
+ ...pointerSrcs
260
+ ].map((src)=>_async_to_generator(function*() {
261
+ try {
262
+ pointerCache.set(src, (yield loadImage(src)));
263
+ } catch (e) {}
264
+ })()));
170
265
  let spinnerImg = null;
171
- try {
172
- cursorImg = yield loadImage(mousePointer);
173
- } catch (e) {}
174
266
  try {
175
267
  spinnerImg = yield loadImage(mouseLoading);
176
268
  } catch (e) {}
@@ -187,8 +279,41 @@ function exportBrandedVideo(frameMap, options, onProgress) {
187
279
  if (e.data.size > 0) chunks.push(e.data);
188
280
  };
189
281
  return new Promise((resolve, reject)=>{
190
- recorder.onerror = ()=>reject(new Error('MediaRecorder error'));
282
+ let stoppedByError = null;
283
+ let settled = false;
284
+ let nextFrame = 0;
285
+ let nextFrameDueAt = performance.now();
286
+ let lastFrameAt = nextFrameDueAt;
287
+ let stopTimer = null;
288
+ let renderTimer = null;
289
+ const frameDuration = 1000 / fps;
290
+ const cleanup = ()=>{
291
+ if (stopTimer) clearTimeout(stopTimer);
292
+ if (renderTimer) clearTimeout(renderTimer);
293
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
294
+ stream.getTracks().forEach((track)=>track.stop());
295
+ };
296
+ const finishWithError = (error)=>{
297
+ if (settled || stoppedByError) return;
298
+ stoppedByError = error;
299
+ if ('inactive' !== recorder.state) recorder.stop();
300
+ else {
301
+ cleanup();
302
+ settled = true;
303
+ reject(error);
304
+ }
305
+ };
306
+ const handleVisibilityChange = ()=>{
307
+ if (document.hidden) finishWithError(new Error('Video export was interrupted because the report tab was hidden'));
308
+ };
309
+ recorder.onerror = ()=>{
310
+ finishWithError(new Error('MediaRecorder error'));
311
+ };
191
312
  recorder.onstop = ()=>{
313
+ cleanup();
314
+ if (settled) return;
315
+ settled = true;
316
+ if (stoppedByError) return void reject(stoppedByError);
192
317
  if (0 === chunks.length) return void reject(new Error('No video data'));
193
318
  const blob = new Blob(chunks, {
194
319
  type: 'video/webm'
@@ -198,28 +323,38 @@ function exportBrandedVideo(frameMap, options, onProgress) {
198
323
  a.href = url;
199
324
  a.download = 'midscene_replay.webm';
200
325
  a.click();
201
- stream.getTracks().forEach((track)=>track.stop());
202
326
  setTimeout(()=>URL.revokeObjectURL(url), 1000);
203
327
  resolve();
204
328
  };
329
+ document.addEventListener('visibilitychange', handleVisibilityChange);
205
330
  recorder.start();
206
- const frameDuration = 1000 / fps;
207
- const startTime = performance.now();
208
- let lastFrame = -1;
209
- const tick = ()=>{
210
- const elapsed = performance.now() - startTime;
211
- const targetFrame = Math.min(Math.floor(elapsed / frameDuration), total - 1);
212
- if (targetFrame > lastFrame) {
213
- lastFrame = targetFrame;
214
- ctx.clearRect(0, 0, W, H);
215
- drawSteps(ctx, targetFrame, frameMap, imgCache, cursorImg, spinnerImg, autoZoom);
216
- null == onProgress || onProgress((targetFrame + 1) / total);
217
- }
218
- if (targetFrame < total - 1) requestAnimationFrame(tick);
219
- else setTimeout(()=>recorder.stop(), 2 * frameDuration);
331
+ const scheduleNextFrame = ()=>{
332
+ const delay = Math.max(0, nextFrameDueAt - performance.now());
333
+ renderTimer = setTimeout(()=>{
334
+ requestAnimationFrame(renderFrame);
335
+ }, delay);
336
+ };
337
+ const renderFrame = (timestamp)=>{
338
+ if (settled || 'inactive' === recorder.state) return;
339
+ if (nextFrame > 0 && isExportRenderStalled(timestamp - lastFrameAt, frameDuration)) return void finishWithError(new Error('Video export was interrupted because rendering stalled'));
340
+ lastFrameAt = timestamp;
341
+ ctx.clearRect(0, 0, W, H);
342
+ drawSteps(ctx, nextFrame, frameMap, imgCache, pointerCache, spinnerImg, autoZoom);
343
+ null == onProgress || onProgress((nextFrame + 1) / total);
344
+ nextFrame += 1;
345
+ if (nextFrame < total) {
346
+ nextFrameDueAt += frameDuration;
347
+ scheduleNextFrame();
348
+ } else stopTimer = setTimeout(()=>{
349
+ if ('inactive' !== recorder.state) recorder.stop();
350
+ }, 2 * frameDuration);
220
351
  };
221
- requestAnimationFrame(tick);
352
+ requestAnimationFrame((timestamp)=>{
353
+ lastFrameAt = timestamp;
354
+ nextFrameDueAt = timestamp;
355
+ renderFrame(timestamp);
356
+ });
222
357
  });
223
358
  })();
224
359
  }
225
- export { exportBrandedVideo, resolveExportCamera };
360
+ export { exportBrandedVideo, isExportRenderStalled, projectNativeRectToExportViewport, resolveExportCamera };
@@ -0,0 +1,36 @@
1
+ const POINTER_REFERENCE_IMAGE_WIDTH = 1920;
2
+ const POINTER_WIDTH = 44;
3
+ const POINTER_HEIGHT = 56;
4
+ const POINTER_HOTSPOT_X = 6;
5
+ const POINTER_HOTSPOT_Y = 4;
6
+ function assertPositiveFinite(value, name) {
7
+ if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive finite number`);
8
+ }
9
+ function buildPointerLayout(scale) {
10
+ return {
11
+ scale,
12
+ width: POINTER_WIDTH * scale,
13
+ height: POINTER_HEIGHT * scale,
14
+ hotspotX: POINTER_HOTSPOT_X * scale,
15
+ hotspotY: POINTER_HOTSPOT_Y * scale,
16
+ centerOffsetX: POINTER_WIDTH * scale / 2,
17
+ centerOffsetY: POINTER_HEIGHT * scale / 2
18
+ };
19
+ }
20
+ function resolvePointerLayout(imageWidth) {
21
+ assertPositiveFinite(imageWidth, 'imageWidth');
22
+ return buildPointerLayout(Math.max(1, Math.sqrt(imageWidth / POINTER_REFERENCE_IMAGE_WIDTH)));
23
+ }
24
+ function resolveExportPointerLayout(imageWidth, contentWidth) {
25
+ assertPositiveFinite(contentWidth, 'contentWidth');
26
+ const liveLayout = resolvePointerLayout(imageWidth);
27
+ return buildPointerLayout(liveLayout.scale * (contentWidth / imageWidth));
28
+ }
29
+ function resolveSpinnerLayout(pointerLayout) {
30
+ const size = pointerLayout.height;
31
+ return {
32
+ size,
33
+ centerOffset: size / 2
34
+ };
35
+ }
36
+ export { POINTER_HEIGHT, POINTER_HOTSPOT_X, POINTER_HOTSPOT_Y, POINTER_WIDTH, resolveExportPointerLayout, resolvePointerLayout, resolveSpinnerLayout };
@@ -198,9 +198,11 @@
198
198
 
199
199
  .player-container .player-custom-controls {
200
200
  flex-direction: row;
201
+ justify-content: center;
201
202
  align-items: center;
202
- gap: 8px;
203
- margin-right: 8px;
203
+ gap: 2px;
204
+ min-width: 58px;
205
+ margin-left: 4px;
204
206
  display: flex;
205
207
  }
206
208
 
@@ -208,6 +210,23 @@
208
210
  color: #fff;
209
211
  }
210
212
 
213
+ .player-container.player-container-empty {
214
+ justify-content: center;
215
+ align-items: center;
216
+ }
217
+
218
+ .player-container.player-container-empty .player-empty-state {
219
+ color: #6b7280;
220
+ flex-direction: column;
221
+ align-items: center;
222
+ gap: 12px;
223
+ display: flex;
224
+ }
225
+
226
+ .player-container.player-container-empty .player-empty-state .player-empty-text {
227
+ font-size: 14px;
228
+ }
229
+
211
230
  .chapter-tooltip .ant-tooltip-inner {
212
231
  -webkit-backdrop-filter: blur(8px);
213
232
  backdrop-filter: blur(8px);
@@ -250,6 +269,12 @@
250
269
  margin: 4px 0;
251
270
  }
252
271
 
272
+ .player-export-label {
273
+ font-variant-numeric: tabular-nums;
274
+ font-feature-settings: "tnum";
275
+ font-size: 14px;
276
+ }
277
+
253
278
  .player-speed-option:hover, .player-settings-item:hover {
254
279
  background: rgba(0, 0, 0, .04);
255
280
  }