mesurer 0.2.2 → 0.2.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/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/dist/chunk-DBVWPZ7G.js +272 -0
- package/dist/index.cjs +17896 -15784
- package/dist/index.js +17100 -15422
- package/dist/screen-recording-gif-J4EOWLWH.js +155 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# mesurer
|
|
2
2
|
|
|
3
|
+
## 0.2.4
|
|
4
|
+
|
|
5
|
+
- add CSS and JavaScript motion inspection with playback, scrubbing, and previews
|
|
6
|
+
- improve iframe, nested-element, keyboard, comments, settings, and extension behavior
|
|
7
|
+
|
|
8
|
+
## 0.2.3
|
|
9
|
+
|
|
10
|
+
- add GIF export and capability-gated MP4 export
|
|
11
|
+
- improve extension recording previews, shortcuts, menu stacking, and theme support
|
|
12
|
+
|
|
3
13
|
## 0.2.2
|
|
4
14
|
|
|
5
15
|
- improve screen recording stability across browser resizing, first-frame capture, color picker usage, and export duration
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Props are the defaults. Saved settings override them; **Use defaults** restores
|
|
|
122
122
|
- **X-ray mode** - Reveal element structure without changing the page
|
|
123
123
|
- **Sample color** - Sample rendered colors and copy values in your chosen format
|
|
124
124
|
- **Screenshot** - Drag a region of the visible tab and copy or download it. In a React integration without the extension, the browser may show a tab-sharing prompt.
|
|
125
|
-
- **Video recording** - Drag a region of the visible tab, record up to 60 seconds, trim the result, and export it as WebM or MP4 when supported. The Chrome extension records the active tab without a second sharing prompt.
|
|
125
|
+
- **Video recording** - Drag a region of the visible tab, record up to 60 seconds, trim the result, and export it as WebM, GIF, or MP4 when supported. The Chrome extension records the active tab without a second sharing prompt.
|
|
126
126
|
- **Distance overlays** - Hold Alt for quick spacing checks
|
|
127
127
|
- **Undo/redo** - Command history for guides, measurements, arrows, pen strokes, and text annotations
|
|
128
128
|
- **Comments** - Pin threads to live elements, add replies, move targets, search threads, and copy structured feedback for agents
|
|
@@ -133,7 +133,7 @@ Props are the defaults. Saved settings override them; **Use defaults** restores
|
|
|
133
133
|
|
|
134
134
|
- React 18+
|
|
135
135
|
- Chromium-based browser. The native screen color picker requires Chromium on Windows or macOS; Linux Chromium builds may not expose the EyeDropper API.
|
|
136
|
-
- Video recording uses the Chrome extension's tab-capture integration for the smoothest active-tab workflow. Without the extension, browser capture permissions and supported export formats depend on the browser.
|
|
136
|
+
- Video recording uses the Chrome extension's tab-capture integration for the smoothest active-tab workflow. Without the extension, browser capture permissions and supported export formats depend on the browser. MP4 requires browser WebCodecs support.
|
|
137
137
|
|
|
138
138
|
Settings are stored separately from workspace state. The default adapter uses `localStorage`; integrations can provide a `persistence` adapter such as the browser extension's `chrome.storage.local` implementation.
|
|
139
139
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// core/screen-recording-webm.ts
|
|
2
|
+
var EBML_ID = 440786851;
|
|
3
|
+
var SEGMENT_ID = 408125543;
|
|
4
|
+
var INFO_ID = 357149030;
|
|
5
|
+
var TIMECODE_SCALE_ID = 2807729;
|
|
6
|
+
var DURATION_ID = 17545;
|
|
7
|
+
var CLUSTER_ID = 524531317;
|
|
8
|
+
var MASTER_IDS = /* @__PURE__ */ new Set([EBML_ID, SEGMENT_ID, INFO_ID]);
|
|
9
|
+
var readElementId = (bytes, offset) => {
|
|
10
|
+
const first = bytes[offset];
|
|
11
|
+
if (first === void 0 || first === 0) return null;
|
|
12
|
+
let length = 1;
|
|
13
|
+
let mask = 128;
|
|
14
|
+
while (length <= 4 && (first & mask) === 0) {
|
|
15
|
+
mask >>= 1;
|
|
16
|
+
length += 1;
|
|
17
|
+
}
|
|
18
|
+
if (offset + length > bytes.length) return null;
|
|
19
|
+
let id = 0;
|
|
20
|
+
for (let index = 0; index < length; index += 1) id = id * 256 + bytes[offset + index];
|
|
21
|
+
return { length, id };
|
|
22
|
+
};
|
|
23
|
+
var readElementSize = (bytes, offset) => {
|
|
24
|
+
const first = bytes[offset];
|
|
25
|
+
if (first === void 0 || first === 0) return null;
|
|
26
|
+
let length = 1;
|
|
27
|
+
let mask = 128;
|
|
28
|
+
while (length <= 8 && (first & mask) === 0) {
|
|
29
|
+
mask >>= 1;
|
|
30
|
+
length += 1;
|
|
31
|
+
}
|
|
32
|
+
if (offset + length > bytes.length) return null;
|
|
33
|
+
const marker = mask - 1;
|
|
34
|
+
let unknown = (first & marker) === marker;
|
|
35
|
+
let value = first & marker;
|
|
36
|
+
for (let index = 1; index < length; index += 1) {
|
|
37
|
+
const byte = bytes[offset + index];
|
|
38
|
+
if (byte !== 255) unknown = false;
|
|
39
|
+
value = value * 256 + byte;
|
|
40
|
+
}
|
|
41
|
+
return { length, value, unknown };
|
|
42
|
+
};
|
|
43
|
+
var readUint = (bytes, offset, size) => {
|
|
44
|
+
let value = 0;
|
|
45
|
+
for (let index = 0; index < size; index += 1) value = value * 256 + bytes[offset + index];
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
function writeWebmDuration(bytes, durationSeconds) {
|
|
49
|
+
if (!(durationSeconds > 0)) return false;
|
|
50
|
+
let timecodeScale = 1e6;
|
|
51
|
+
let durationOffset = -1;
|
|
52
|
+
let durationSize = 0;
|
|
53
|
+
const walk = (start, end, inInfo) => {
|
|
54
|
+
let cursor = start;
|
|
55
|
+
while (cursor + 2 < end && durationOffset < 0) {
|
|
56
|
+
const id = readElementId(bytes, cursor);
|
|
57
|
+
if (!id) return;
|
|
58
|
+
cursor += id.length;
|
|
59
|
+
const size = readElementSize(bytes, cursor);
|
|
60
|
+
if (!size) return;
|
|
61
|
+
cursor += size.length;
|
|
62
|
+
const dataStart = cursor;
|
|
63
|
+
if (id.id === CLUSTER_ID) return;
|
|
64
|
+
if (id.id === TIMECODE_SCALE_ID && !size.unknown && size.value > 0 && size.value <= 8) {
|
|
65
|
+
timecodeScale = readUint(bytes, dataStart, size.value);
|
|
66
|
+
}
|
|
67
|
+
if (id.id === DURATION_ID && inInfo && !size.unknown && (size.value === 4 || size.value === 8)) {
|
|
68
|
+
durationOffset = dataStart;
|
|
69
|
+
durationSize = size.value;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const dataEnd = size.unknown ? end : dataStart + size.value;
|
|
73
|
+
if (size.unknown || MASTER_IDS.has(id.id)) {
|
|
74
|
+
walk(dataStart, dataEnd, inInfo || id.id === INFO_ID);
|
|
75
|
+
if (durationOffset >= 0) return;
|
|
76
|
+
}
|
|
77
|
+
if (size.unknown) return;
|
|
78
|
+
cursor = dataEnd;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
walk(0, bytes.length, false);
|
|
82
|
+
if (durationOffset < 0 || timecodeScale <= 0) return false;
|
|
83
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
84
|
+
const current = durationSize === 8 ? view.getFloat64(durationOffset) : view.getFloat32(durationOffset);
|
|
85
|
+
const next = durationSeconds * (1e9 / timecodeScale);
|
|
86
|
+
if (Number.isFinite(current) && current >= next - 1e9 / timecodeScale * 0.05) return false;
|
|
87
|
+
if (durationSize === 8) view.setFloat64(durationOffset, next);
|
|
88
|
+
else view.setFloat32(durationOffset, next);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
async function correctWebmDuration(blob, durationSeconds) {
|
|
92
|
+
if (!(durationSeconds > 0) || !blob.type.includes("webm")) return blob;
|
|
93
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
94
|
+
if (!writeWebmDuration(bytes, durationSeconds)) return blob;
|
|
95
|
+
return new Blob([bytes], { type: blob.type || "video/webm" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// core/screen-recording-export.ts
|
|
99
|
+
function resolveRecordingDuration(mediaDuration, elapsedSeconds) {
|
|
100
|
+
const media = Number.isFinite(mediaDuration) && mediaDuration > 0 ? mediaDuration : 0;
|
|
101
|
+
const elapsed = Number.isFinite(elapsedSeconds) && elapsedSeconds > 0 ? elapsedSeconds : 0;
|
|
102
|
+
if (media <= 0 && elapsed <= 0) return 0.1;
|
|
103
|
+
if (media <= 0) return Math.max(elapsed, 0.1);
|
|
104
|
+
if (elapsed <= 0) return media;
|
|
105
|
+
if (media < elapsed * 0.9) return elapsed;
|
|
106
|
+
return Math.min(media, elapsed);
|
|
107
|
+
}
|
|
108
|
+
function isFullClipExport(startTime, endTime, duration, tolerance = 0.05) {
|
|
109
|
+
return startTime <= tolerance && endTime >= duration - tolerance;
|
|
110
|
+
}
|
|
111
|
+
async function readBlobVideoDuration(blob, ownerDocument) {
|
|
112
|
+
const url = URL.createObjectURL(blob);
|
|
113
|
+
const video = ownerDocument.createElement("video");
|
|
114
|
+
video.preload = "metadata";
|
|
115
|
+
video.muted = true;
|
|
116
|
+
try {
|
|
117
|
+
return await new Promise((resolve, reject) => {
|
|
118
|
+
video.onloadedmetadata = () => {
|
|
119
|
+
resolve(video.duration);
|
|
120
|
+
};
|
|
121
|
+
video.onerror = () => reject(new Error("Could not read recording"));
|
|
122
|
+
video.src = url;
|
|
123
|
+
});
|
|
124
|
+
} finally {
|
|
125
|
+
URL.revokeObjectURL(url);
|
|
126
|
+
video.removeAttribute("src");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function seekVideoElement(video, time) {
|
|
130
|
+
const target = Math.max(0, time);
|
|
131
|
+
if (target <= 0 && video.readyState >= 1) {
|
|
132
|
+
video.currentTime = 0;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
const onSeeked = () => resolve();
|
|
137
|
+
const onError = () => reject(new Error("Could not seek recording"));
|
|
138
|
+
video.addEventListener("seeked", onSeeked, { once: true });
|
|
139
|
+
video.addEventListener("error", onError, { once: true });
|
|
140
|
+
if (video.readyState < 1) {
|
|
141
|
+
video.addEventListener(
|
|
142
|
+
"loadedmetadata",
|
|
143
|
+
() => {
|
|
144
|
+
video.currentTime = target;
|
|
145
|
+
},
|
|
146
|
+
{ once: true }
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
video.currentTime = target;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function requestDisplayMediaStream(ownerWindow) {
|
|
154
|
+
const fallback = {
|
|
155
|
+
audio: false,
|
|
156
|
+
video: true
|
|
157
|
+
};
|
|
158
|
+
const chromium = {
|
|
159
|
+
audio: false,
|
|
160
|
+
video: { displaySurface: "browser" },
|
|
161
|
+
preferCurrentTab: true,
|
|
162
|
+
selfBrowserSurface: "include"
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
return await ownerWindow.navigator.mediaDevices.getDisplayMedia(chromium);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
168
|
+
return await ownerWindow.navigator.mediaDevices.getDisplayMedia(fallback);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async function reencodeVideoClip(ownerDocument, ownerWindow, sourceUrl, startTime, endTime, scale, mimeType) {
|
|
172
|
+
const source = ownerDocument.createElement("video");
|
|
173
|
+
source.muted = true;
|
|
174
|
+
source.playsInline = true;
|
|
175
|
+
source.src = sourceUrl;
|
|
176
|
+
source.preload = "auto";
|
|
177
|
+
await new Promise((resolve, reject) => {
|
|
178
|
+
const ready = () => source.videoWidth > 0 && source.videoHeight > 0;
|
|
179
|
+
const finish = () => {
|
|
180
|
+
if (!ready()) return;
|
|
181
|
+
resolve();
|
|
182
|
+
};
|
|
183
|
+
source.onloadedmetadata = finish;
|
|
184
|
+
source.onloadeddata = finish;
|
|
185
|
+
source.onresize = finish;
|
|
186
|
+
source.onerror = () => reject(new Error("Could not trim recording"));
|
|
187
|
+
if (ready()) resolve();
|
|
188
|
+
});
|
|
189
|
+
await seekVideoElement(source, startTime);
|
|
190
|
+
if (source.videoWidth <= 0 || source.videoHeight <= 0) {
|
|
191
|
+
await new Promise((resolve) => {
|
|
192
|
+
if (typeof source.requestVideoFrameCallback === "function") {
|
|
193
|
+
source.requestVideoFrameCallback(() => resolve());
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
ownerWindow.requestAnimationFrame(() => resolve());
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const canvas = ownerDocument.createElement("canvas");
|
|
200
|
+
canvas.width = Math.max(1, Math.round(source.videoWidth * scale));
|
|
201
|
+
canvas.height = Math.max(1, Math.round(source.videoHeight * scale));
|
|
202
|
+
const context = canvas.getContext("2d");
|
|
203
|
+
if (!context) throw new Error("Could not trim recording");
|
|
204
|
+
const stream = canvas.captureStream(30);
|
|
205
|
+
const captureTrack = stream.getVideoTracks()[0];
|
|
206
|
+
const requestFrame = captureTrack && "requestFrame" in captureTrack ? captureTrack.requestFrame : void 0;
|
|
207
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : void 0);
|
|
208
|
+
const chunks = [];
|
|
209
|
+
return await new Promise((resolve, reject) => {
|
|
210
|
+
let stopped = false;
|
|
211
|
+
const onTimeUpdate = () => {
|
|
212
|
+
if (source.currentTime >= endTime) finish();
|
|
213
|
+
};
|
|
214
|
+
const finish = () => {
|
|
215
|
+
if (stopped) return;
|
|
216
|
+
stopped = true;
|
|
217
|
+
source.removeEventListener("timeupdate", onTimeUpdate);
|
|
218
|
+
source.pause();
|
|
219
|
+
try {
|
|
220
|
+
recorder.stop();
|
|
221
|
+
} catch {
|
|
222
|
+
reject(new Error("Could not trim recording"));
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const drawFrame = () => {
|
|
226
|
+
if (stopped) return;
|
|
227
|
+
context.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
228
|
+
requestFrame?.call(captureTrack);
|
|
229
|
+
if (source.currentTime >= endTime || source.ended) {
|
|
230
|
+
finish();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (typeof source.requestVideoFrameCallback === "function") {
|
|
234
|
+
source.requestVideoFrameCallback(() => drawFrame());
|
|
235
|
+
} else {
|
|
236
|
+
ownerWindow.requestAnimationFrame(drawFrame);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
recorder.ondataavailable = (event) => {
|
|
240
|
+
if (event.data.size > 0) chunks.push(event.data);
|
|
241
|
+
};
|
|
242
|
+
recorder.onstop = () => {
|
|
243
|
+
const recorded = new Blob(chunks, { type: recorder.mimeType || mimeType || "video/webm" });
|
|
244
|
+
void correctWebmDuration(recorded, Math.max(0.1, endTime - startTime)).then(resolve, reject);
|
|
245
|
+
};
|
|
246
|
+
recorder.onerror = () => reject(new Error("Could not trim recording"));
|
|
247
|
+
source.addEventListener("timeupdate", onTimeUpdate);
|
|
248
|
+
recorder.start(100);
|
|
249
|
+
void source.play().then(
|
|
250
|
+
() => drawFrame(),
|
|
251
|
+
() => {
|
|
252
|
+
source.removeEventListener("timeupdate", onTimeUpdate);
|
|
253
|
+
reject(new Error("Could not trim recording"));
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function encodeGifClip(ownerDocument, sourceUrl, startTime, endTime, scale) {
|
|
259
|
+
const encoder = await import("./screen-recording-gif-J4EOWLWH.js");
|
|
260
|
+
return encoder.encodeGifClip(ownerDocument, sourceUrl, startTime, endTime, scale);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export {
|
|
264
|
+
correctWebmDuration,
|
|
265
|
+
resolveRecordingDuration,
|
|
266
|
+
isFullClipExport,
|
|
267
|
+
readBlobVideoDuration,
|
|
268
|
+
seekVideoElement,
|
|
269
|
+
requestDisplayMediaStream,
|
|
270
|
+
reencodeVideoClip,
|
|
271
|
+
encodeGifClip
|
|
272
|
+
};
|