@gradio/core 0.17.0 → 0.18.1
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 +34 -0
- package/dist/src/Blocks.svelte +74 -4
- package/dist/src/api_docs/ApiDocs.svelte +17 -17
- package/dist/src/api_docs/Settings.svelte +74 -1
- package/dist/src/api_docs/Settings.svelte.d.ts +3 -0
- package/dist/src/api_docs/img/record-stop.svg +1 -0
- package/dist/src/api_docs/img/record.svg +1 -0
- package/dist/src/init.js +7 -3
- package/dist/src/lang/en.json +4 -0
- package/dist/src/screen_recorder.d.ts +16 -0
- package/dist/src/screen_recorder.js +255 -0
- package/package.json +46 -46
- package/src/Blocks.svelte +85 -6
- package/src/api_docs/ApiDocs.svelte +18 -17
- package/src/api_docs/Settings.svelte +77 -1
- package/src/api_docs/img/record-stop.svg +1 -0
- package/src/api_docs/img/record.svg +1 -0
- package/src/init.ts +7 -6
- package/src/lang/en.json +4 -0
- package/src/screen_recorder.ts +361 -0
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
export let space_id: string | null;
|
|
7
7
|
export let pwa_enabled: boolean | undefined;
|
|
8
8
|
import { BaseDropdown as Dropdown } from "@gradio/dropdown";
|
|
9
|
+
import { BaseCheckbox as Checkbox } from "@gradio/checkbox";
|
|
9
10
|
import { language_choices, changeLocale } from "../i18n";
|
|
10
11
|
import { locale, _ } from "svelte-i18n";
|
|
11
12
|
import { setupi18n } from "../i18n";
|
|
13
|
+
import record from "./img/record.svg";
|
|
14
|
+
import { createEventDispatcher } from "svelte";
|
|
12
15
|
|
|
16
|
+
const dispatch = createEventDispatcher();
|
|
13
17
|
if (root === "") {
|
|
14
18
|
root = location.protocol + "//" + location.host + location.pathname;
|
|
15
19
|
}
|
|
@@ -44,6 +48,8 @@
|
|
|
44
48
|
|
|
45
49
|
let current_locale: string;
|
|
46
50
|
let current_theme: "light" | "dark" | "system" = "system";
|
|
51
|
+
export let allow_zoom = true;
|
|
52
|
+
export let allow_video_trim = true;
|
|
47
53
|
|
|
48
54
|
locale.subscribe((value) => {
|
|
49
55
|
if (value) {
|
|
@@ -55,6 +61,15 @@
|
|
|
55
61
|
const new_locale = e.detail;
|
|
56
62
|
changeLocale(new_locale);
|
|
57
63
|
}
|
|
64
|
+
|
|
65
|
+
function handleZoomChange(e: CustomEvent): void {
|
|
66
|
+
allow_zoom = e.detail;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleVideoTrimChange(e: CustomEvent): void {
|
|
70
|
+
allow_video_trim = e.detail;
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
setupi18n();
|
|
59
74
|
</script>
|
|
60
75
|
|
|
@@ -120,6 +135,44 @@
|
|
|
120
135
|
{/if}
|
|
121
136
|
</p>
|
|
122
137
|
</div>
|
|
138
|
+
<div class="banner-wrap">
|
|
139
|
+
<h2>{$_("common.screen_studio")} <span class="beta-tag">beta</span></h2>
|
|
140
|
+
<p class="padded">
|
|
141
|
+
Screen Studio allows you to record your screen and generates a video of your
|
|
142
|
+
app with automatically adding zoom in and zoom out effects as well as
|
|
143
|
+
trimming the video to remove the prediction time.
|
|
144
|
+
<br /><br />
|
|
145
|
+
Start recording by clicking the <i>Start Recording</i> button below and then
|
|
146
|
+
sharing the current browser tab of your Gradio demo. Use your app as you
|
|
147
|
+
would normally to generate a prediction.
|
|
148
|
+
<br />
|
|
149
|
+
Stop recording by clicking the <i>Stop Recording</i> button in the footer of
|
|
150
|
+
the demo.
|
|
151
|
+
<br /><br />
|
|
152
|
+
<Checkbox
|
|
153
|
+
label="Include automatic zoom in/out"
|
|
154
|
+
interactive={true}
|
|
155
|
+
value={allow_zoom}
|
|
156
|
+
on:change={handleZoomChange}
|
|
157
|
+
/>
|
|
158
|
+
<Checkbox
|
|
159
|
+
label="Include automatic video trimming"
|
|
160
|
+
interactive={true}
|
|
161
|
+
value={allow_video_trim}
|
|
162
|
+
on:change={handleVideoTrimChange}
|
|
163
|
+
/>
|
|
164
|
+
</p>
|
|
165
|
+
<button
|
|
166
|
+
class="record-button"
|
|
167
|
+
on:click={() => {
|
|
168
|
+
dispatch("close");
|
|
169
|
+
dispatch("start_recording");
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<img src={record} alt="Start Recording" />
|
|
173
|
+
Start Recording
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
123
176
|
|
|
124
177
|
<style>
|
|
125
178
|
.banner-wrap {
|
|
@@ -150,7 +203,8 @@
|
|
|
150
203
|
margin-left: var(--size-2);
|
|
151
204
|
}
|
|
152
205
|
|
|
153
|
-
.theme-button
|
|
206
|
+
.theme-button,
|
|
207
|
+
.record-button {
|
|
154
208
|
display: flex;
|
|
155
209
|
align-items: center;
|
|
156
210
|
border: 1px solid var(--border-color-primary);
|
|
@@ -162,6 +216,15 @@
|
|
|
162
216
|
cursor: pointer;
|
|
163
217
|
}
|
|
164
218
|
|
|
219
|
+
.record-button img {
|
|
220
|
+
margin-right: var(--size-1);
|
|
221
|
+
margin-left: var(--size-1);
|
|
222
|
+
width: var(--size-3);
|
|
223
|
+
}
|
|
224
|
+
.record-button:hover {
|
|
225
|
+
border-color: red;
|
|
226
|
+
}
|
|
227
|
+
|
|
165
228
|
.current-theme {
|
|
166
229
|
border: 1px solid var(--body-text-color-subdued);
|
|
167
230
|
color: var(--body-text-color);
|
|
@@ -181,4 +244,17 @@
|
|
|
181
244
|
all: unset;
|
|
182
245
|
cursor: pointer;
|
|
183
246
|
}
|
|
247
|
+
|
|
248
|
+
.beta-tag {
|
|
249
|
+
position: relative;
|
|
250
|
+
top: -5px;
|
|
251
|
+
font-size: var(--text-xs);
|
|
252
|
+
background-color: var(--color-accent);
|
|
253
|
+
color: white;
|
|
254
|
+
padding: 2px 6px;
|
|
255
|
+
border-radius: 10px;
|
|
256
|
+
margin-left: 5px;
|
|
257
|
+
font-weight: normal;
|
|
258
|
+
text-transform: uppercase;
|
|
259
|
+
}
|
|
184
260
|
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>record [#982]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-380.000000, -3839.000000)" fill="#FF0000"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M338,3689 C338,3691.209 336.209,3693 334,3693 C331.791,3693 330,3691.209 330,3689 C330,3686.791 331.791,3685 334,3685 C336.209,3685 338,3686.791 338,3689 M334,3697 C329.589,3697 326,3693.411 326,3689 C326,3684.589 329.589,3681 334,3681 C338.411,3681 342,3684.589 342,3689 C342,3693.411 338.411,3697 334,3697 M334,3679 C328.477,3679 324,3683.477 324,3689 C324,3694.523 328.477,3699 334,3699 C339.523,3699 344,3694.523 344,3689 C344,3683.477 339.523,3679 334,3679" id="record-[#982]"> </path> </g> </g> </g> </g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <title>record [#982]</title> <desc>Created with Sketch.</desc> <defs> </defs> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Dribbble-Light-Preview" transform="translate(-380.000000, -3839.000000)" fill="#808080"> <g id="icons" transform="translate(56.000000, 160.000000)"> <path d="M338,3689 C338,3691.209 336.209,3693 334,3693 C331.791,3693 330,3691.209 330,3689 C330,3686.791 331.791,3685 334,3685 C336.209,3685 338,3686.791 338,3689 M334,3697 C329.589,3697 326,3693.411 326,3689 C326,3684.589 329.589,3681 334,3681 C338.411,3681 342,3684.589 342,3689 C342,3693.411 338.411,3697 334,3697 M334,3679 C328.477,3679 324,3683.477 324,3689 C324,3694.523 328.477,3699 334,3699 C339.523,3699 344,3694.523 344,3689 C344,3683.477 339.523,3679 334,3679" id="record-[#982]"> </path> </g> </g> </g> </g></svg>
|
package/src/init.ts
CHANGED
|
@@ -179,13 +179,15 @@ export function create_components(initial_layout: ComponentMeta | undefined): {
|
|
|
179
179
|
root: string;
|
|
180
180
|
dependencies: Dependency[];
|
|
181
181
|
}): void {
|
|
182
|
+
components.forEach((c) => {
|
|
183
|
+
for (const prop in c.props) {
|
|
184
|
+
if (c.props[prop] === null) {
|
|
185
|
+
c.props[prop] = undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
182
189
|
let replacement_components: ComponentMeta[] = [];
|
|
183
190
|
let new_components: ComponentMeta[] = [];
|
|
184
|
-
console.log("old_keys", keys_per_render_id[render_id]);
|
|
185
|
-
console.log(
|
|
186
|
-
"new_keys",
|
|
187
|
-
components.map((c) => c.key)
|
|
188
|
-
);
|
|
189
191
|
components.forEach((c) => {
|
|
190
192
|
if (c.key == null || !keys_per_render_id[render_id]?.includes(c.key)) {
|
|
191
193
|
new_components.push(c);
|
|
@@ -193,7 +195,6 @@ export function create_components(initial_layout: ComponentMeta | undefined): {
|
|
|
193
195
|
replacement_components.push(c);
|
|
194
196
|
}
|
|
195
197
|
});
|
|
196
|
-
console.log(new_components.length, replacement_components.length);
|
|
197
198
|
let _constructor_map = preload_all_components(new_components, root);
|
|
198
199
|
_constructor_map.forEach((v, k) => {
|
|
199
200
|
constructor_map.set(k, v);
|
package/src/lang/en.json
CHANGED
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
"language": "Language",
|
|
70
70
|
"display_theme": "Display Theme",
|
|
71
71
|
"pwa": "Progressive Web App",
|
|
72
|
+
"record": "Record",
|
|
73
|
+
"stop_recording": "Stop Recording",
|
|
74
|
+
"screen_studio": "Screen Studio",
|
|
75
|
+
"share_gradio_tab": "[Sharing] Gradio Tab",
|
|
72
76
|
"run": "Run"
|
|
73
77
|
},
|
|
74
78
|
"dataframe": {
|
|
@@ -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
|
+
}
|