@gradio/core 0.17.0 → 0.18.0

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.
@@ -0,0 +1,361 @@
1
+ import type { ToastMessage } from "@gradio/statustracker";
2
+
3
+ let isRecording = false;
4
+ let mediaRecorder: MediaRecorder | null = null;
5
+ let recordedChunks: Blob[] = [];
6
+ let recordingStartTime = 0;
7
+ let animationFrameId: number | null = null;
8
+ let removeSegment: { start?: number; end?: number } = {};
9
+ let root: string;
10
+
11
+ let add_message_callback: (
12
+ title: string,
13
+ message: string,
14
+ type: ToastMessage["type"]
15
+ ) => void;
16
+ let onRecordingStateChange: ((isRecording: boolean) => void) | null = null;
17
+ let zoomEffects: {
18
+ boundingBox: { topLeft: [number, number]; bottomRight: [number, number] };
19
+ start_frame: number;
20
+ duration?: number;
21
+ }[] = [];
22
+
23
+ export function initialize(
24
+ rootPath: string,
25
+ add_new_message: (
26
+ title: string,
27
+ message: string,
28
+ type: ToastMessage["type"]
29
+ ) => void,
30
+ recordingStateCallback?: (isRecording: boolean) => void
31
+ ): void {
32
+ root = rootPath;
33
+ add_message_callback = add_new_message;
34
+ if (recordingStateCallback) {
35
+ onRecordingStateChange = recordingStateCallback;
36
+ }
37
+ }
38
+
39
+ export async function startRecording(): Promise<void> {
40
+ if (isRecording) {
41
+ return;
42
+ }
43
+
44
+ try {
45
+ const originalTitle = document.title;
46
+ document.title = "[Sharing] Gradio Tab";
47
+ const stream = await navigator.mediaDevices.getDisplayMedia({
48
+ video: {
49
+ width: { ideal: 1920 },
50
+ height: { ideal: 1080 },
51
+ frameRate: { ideal: 30 }
52
+ },
53
+ audio: true,
54
+ selfBrowserSurface: "include"
55
+ } as MediaStreamConstraints);
56
+ document.title = originalTitle;
57
+
58
+ const options = {
59
+ videoBitsPerSecond: 5000000
60
+ };
61
+
62
+ mediaRecorder = new MediaRecorder(stream, options);
63
+
64
+ recordedChunks = [];
65
+ removeSegment = {};
66
+
67
+ mediaRecorder.ondataavailable = handleDataAvailable;
68
+ mediaRecorder.onstop = handleStop;
69
+
70
+ mediaRecorder.start(1000);
71
+ isRecording = true;
72
+ if (onRecordingStateChange) {
73
+ onRecordingStateChange(true);
74
+ }
75
+ recordingStartTime = Date.now();
76
+ } catch (error: any) {
77
+ add_message_callback(
78
+ "Recording Error",
79
+ "Failed to start recording: " + error.message,
80
+ "error"
81
+ );
82
+ }
83
+ }
84
+
85
+ export function stopRecording(): void {
86
+ if (!isRecording || !mediaRecorder) {
87
+ return;
88
+ }
89
+
90
+ mediaRecorder.stop();
91
+ isRecording = false;
92
+ if (onRecordingStateChange) {
93
+ onRecordingStateChange(false);
94
+ }
95
+ }
96
+
97
+ export function isCurrentlyRecording(): boolean {
98
+ return isRecording;
99
+ }
100
+
101
+ export function markRemoveSegmentStart(): void {
102
+ if (!isRecording) {
103
+ return;
104
+ }
105
+
106
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
107
+ removeSegment.start = currentTime;
108
+ }
109
+
110
+ export function markRemoveSegmentEnd(): void {
111
+ if (!isRecording || removeSegment.start === undefined) {
112
+ return;
113
+ }
114
+
115
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
116
+ removeSegment.end = currentTime;
117
+ }
118
+
119
+ export function clearRemoveSegment(): void {
120
+ removeSegment = {};
121
+ }
122
+
123
+ export function addZoomEffect(
124
+ is_input: boolean,
125
+ params: {
126
+ boundingBox: {
127
+ topLeft: [number, number];
128
+ bottomRight: [number, number];
129
+ };
130
+ duration?: number;
131
+ }
132
+ ): void {
133
+ if (!isRecording) {
134
+ return;
135
+ }
136
+
137
+ const FPS = 30;
138
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
139
+ const currentFrame = is_input
140
+ ? Math.floor((currentTime - 2) * FPS)
141
+ : Math.floor(currentTime * FPS);
142
+
143
+ if (
144
+ params.boundingBox &&
145
+ params.boundingBox.topLeft &&
146
+ params.boundingBox.bottomRight &&
147
+ params.boundingBox.topLeft.length === 2 &&
148
+ params.boundingBox.bottomRight.length === 2
149
+ ) {
150
+ const newEffectDuration = params.duration || 2.0;
151
+ const newEffectEndFrame =
152
+ currentFrame + Math.floor(newEffectDuration * FPS);
153
+
154
+ const hasOverlap = zoomEffects.some((existingEffect) => {
155
+ const existingEffectEndFrame =
156
+ existingEffect.start_frame +
157
+ Math.floor((existingEffect.duration || 2.0) * FPS);
158
+ return (
159
+ (currentFrame >= existingEffect.start_frame &&
160
+ currentFrame <= existingEffectEndFrame) ||
161
+ (newEffectEndFrame >= existingEffect.start_frame &&
162
+ newEffectEndFrame <= existingEffectEndFrame) ||
163
+ (currentFrame <= existingEffect.start_frame &&
164
+ newEffectEndFrame >= existingEffectEndFrame)
165
+ );
166
+ });
167
+
168
+ if (!hasOverlap) {
169
+ zoomEffects.push({
170
+ boundingBox: params.boundingBox,
171
+ start_frame: currentFrame,
172
+ duration: newEffectDuration
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ export function zoom(
179
+ is_input: boolean,
180
+ elements: number[],
181
+ duration = 2.0
182
+ ): void {
183
+ if (!isRecording) {
184
+ return;
185
+ }
186
+
187
+ try {
188
+ setTimeout(() => {
189
+ if (!elements || elements.length === 0) {
190
+ return;
191
+ }
192
+
193
+ let minLeft = Infinity;
194
+ let minTop = Infinity;
195
+ let maxRight = 0;
196
+ let maxBottom = 0;
197
+ let foundElements = false;
198
+
199
+ for (const elementId of elements) {
200
+ const selector = `#component-${elementId}`;
201
+ const element = document.querySelector(selector);
202
+
203
+ if (element) {
204
+ foundElements = true;
205
+ const rect = element.getBoundingClientRect();
206
+
207
+ minLeft = Math.min(minLeft, rect.left);
208
+ minTop = Math.min(minTop, rect.top);
209
+ maxRight = Math.max(maxRight, rect.right);
210
+ maxBottom = Math.max(maxBottom, rect.bottom);
211
+ }
212
+ }
213
+
214
+ if (!foundElements) {
215
+ return;
216
+ }
217
+
218
+ const viewportWidth = window.innerWidth;
219
+ const viewportHeight = window.innerHeight;
220
+
221
+ const boxWidth = Math.min(maxRight, viewportWidth) - Math.max(0, minLeft);
222
+ const boxHeight =
223
+ Math.min(maxBottom, viewportHeight) - Math.max(0, minTop);
224
+
225
+ const widthPercentage = boxWidth / viewportWidth;
226
+ const heightPercentage = boxHeight / viewportHeight;
227
+
228
+ if (widthPercentage >= 0.8 || heightPercentage >= 0.8) {
229
+ return;
230
+ }
231
+
232
+ const isSafari = /^((?!chrome|android).)*safari/i.test(
233
+ navigator.userAgent
234
+ );
235
+
236
+ let topLeft: [number, number] = [
237
+ Math.max(0, minLeft) / viewportWidth,
238
+ Math.max(0, minTop) / viewportHeight
239
+ ];
240
+
241
+ let bottomRight: [number, number] = [
242
+ Math.min(maxRight, viewportWidth) / viewportWidth,
243
+ Math.min(maxBottom, viewportHeight) / viewportHeight
244
+ ];
245
+
246
+ if (isSafari) {
247
+ topLeft[0] = Math.max(0, topLeft[0] * 0.9);
248
+ bottomRight[0] = Math.min(1, bottomRight[0] * 0.9);
249
+ const width = bottomRight[0] - topLeft[0];
250
+ const center = (topLeft[0] + bottomRight[0]) / 2;
251
+ const newCenter = center * 0.9;
252
+ topLeft[0] = Math.max(0, newCenter - width / 2);
253
+ bottomRight[0] = Math.min(1, newCenter + width / 2);
254
+ }
255
+
256
+ topLeft[0] = Math.max(0, topLeft[0]);
257
+ topLeft[1] = Math.max(0, topLeft[1]);
258
+ bottomRight[0] = Math.min(1, bottomRight[0]);
259
+ bottomRight[1] = Math.min(1, bottomRight[1]);
260
+
261
+ addZoomEffect(is_input, {
262
+ boundingBox: {
263
+ topLeft,
264
+ bottomRight
265
+ },
266
+ duration: duration
267
+ });
268
+ }, 300);
269
+ } catch (error) {
270
+ // pass
271
+ }
272
+ }
273
+
274
+ function handleDataAvailable(event: BlobEvent): void {
275
+ if (event.data.size > 0) {
276
+ recordedChunks.push(event.data);
277
+ }
278
+ }
279
+
280
+ function handleStop(): void {
281
+ isRecording = false;
282
+ if (onRecordingStateChange) {
283
+ onRecordingStateChange(false);
284
+ }
285
+
286
+ const blob = new Blob(recordedChunks, {
287
+ type: "video/mp4"
288
+ });
289
+
290
+ handleRecordingComplete(blob);
291
+
292
+ const screenStream = mediaRecorder?.stream?.getTracks() || [];
293
+ screenStream.forEach((track) => track.stop());
294
+
295
+ if (animationFrameId !== null) {
296
+ cancelAnimationFrame(animationFrameId);
297
+ animationFrameId = null;
298
+ }
299
+ }
300
+
301
+ async function handleRecordingComplete(recordedBlob: Blob): Promise<void> {
302
+ try {
303
+ add_message_callback(
304
+ "Processing video",
305
+ "This may take a few seconds...",
306
+ "info"
307
+ );
308
+
309
+ const formData = new FormData();
310
+ formData.append("video", recordedBlob, "recording.mp4");
311
+
312
+ if (removeSegment.start !== undefined && removeSegment.end !== undefined) {
313
+ formData.append("remove_segment_start", removeSegment.start.toString());
314
+ formData.append("remove_segment_end", removeSegment.end.toString());
315
+ }
316
+
317
+ if (zoomEffects.length > 0) {
318
+ formData.append("zoom_effects", JSON.stringify(zoomEffects));
319
+ }
320
+
321
+ const response = await fetch(root + "/gradio_api/process_recording", {
322
+ method: "POST",
323
+ body: formData
324
+ });
325
+
326
+ if (!response.ok) {
327
+ throw new Error(
328
+ `Server returned ${response.status}: ${response.statusText}`
329
+ );
330
+ }
331
+
332
+ const processedBlob = await response.blob();
333
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
334
+ saveWithDownloadAttribute(processedBlob, defaultFilename);
335
+ zoomEffects = [];
336
+ } catch (error) {
337
+ add_message_callback(
338
+ "Processing Error",
339
+ "Failed to process recording. Saving original version.",
340
+ "warning"
341
+ );
342
+
343
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
344
+ saveWithDownloadAttribute(recordedBlob, defaultFilename);
345
+ }
346
+ }
347
+
348
+ function saveWithDownloadAttribute(blob: Blob, suggestedName: string): void {
349
+ const url = URL.createObjectURL(blob);
350
+ const a = document.createElement("a");
351
+ a.style.display = "none";
352
+ a.href = url;
353
+ a.download = suggestedName;
354
+
355
+ document.body.appendChild(a);
356
+ a.click();
357
+ setTimeout(() => {
358
+ document.body.removeChild(a);
359
+ URL.revokeObjectURL(url);
360
+ }, 100);
361
+ }