@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # @gradio/core
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Features
6
+
7
+ - [#11224](https://github.com/gradio-app/gradio/pull/11224) [`834e92c`](https://github.com/gradio-app/gradio/commit/834e92c187f200665c78c344f0b38f5adede807b) - Fix re-rendering with key when setting a value to None. Thanks @aliabid94!
8
+ - [#10832](https://github.com/gradio-app/gradio/pull/10832) [`d457438`](https://github.com/gradio-app/gradio/commit/d4574381bdd12709183c3affe740fada82b8baea) - Screen recording. Thanks @dawoodkhan82!
9
+
3
10
  ## 0.17.0
4
11
 
5
12
  ### Features
@@ -1,6 +1,7 @@
1
1
  <script>import { tick, onMount } from "svelte";
2
2
  import { _ } from "svelte-i18n";
3
3
  import { Client } from "@gradio/client";
4
+ import { writable } from "svelte/store";
4
5
  import { setupi18n } from "./i18n";
5
6
  import { ApiDocs, ApiRecorder, Settings } from "./api_docs/";
6
7
  import { Toast } from "@gradio/statustracker";
@@ -9,7 +10,9 @@ import { prefix_css } from "./css";
9
10
  import logo from "./images/logo.svg";
10
11
  import api_logo from "./api_docs/img/api-logo.svg";
11
12
  import settings_logo from "./api_docs/img/settings-logo.svg";
13
+ import record_stop from "./api_docs/img/record-stop.svg";
12
14
  import { create_components, AsyncFunction } from "./init";
15
+ import * as screen_recorder from "./screen_recorder";
13
16
  export let root;
14
17
  export let components;
15
18
  export let layout;
@@ -74,6 +77,8 @@ export let search_params;
74
77
  let api_docs_visible = search_params.get("view") === "api" && show_api;
75
78
  let settings_visible = search_params.get("view") === "settings";
76
79
  let api_recorder_visible = search_params.get("view") === "api-recorder" && show_api;
80
+ let allow_zoom = true;
81
+ let allow_video_trim = true;
77
82
  function set_api_docs_visible(visible) {
78
83
  api_recorder_visible = false;
79
84
  api_docs_visible = visible;
@@ -99,6 +104,17 @@ let api_calls = [];
99
104
  export let render_complete = false;
100
105
  async function handle_update(data, fn_index) {
101
106
  const dep = dependencies.find((dep2) => dep2.id === fn_index);
107
+ const input_type = components.find(
108
+ (comp) => comp.id === dep?.inputs[0]
109
+ )?.type;
110
+ if (allow_zoom && dep && input_type !== "dataset") {
111
+ if (dep && dep.inputs && dep.inputs.length > 0 && $is_screen_recording) {
112
+ screen_recorder.zoom(true, dep.inputs, 1);
113
+ }
114
+ if (dep && dep.outputs && dep.outputs.length > 0 && $is_screen_recording) {
115
+ screen_recorder.zoom(false, dep.outputs, 2);
116
+ }
117
+ }
102
118
  if (!dep) {
103
119
  return;
104
120
  }
@@ -285,6 +301,9 @@ async function trigger_api_call(dep_index, trigger_id = null, event_data = null)
285
301
  }
286
302
  }
287
303
  async function make_prediction(payload2, streaming = false) {
304
+ if (allow_video_trim) {
305
+ screen_recorder.markRemoveSegmentStart();
306
+ }
288
307
  if (api_recorder_visible) {
289
308
  api_calls = [...api_calls, JSON.parse(JSON.stringify(payload2))];
290
309
  }
@@ -472,6 +491,9 @@ async function trigger_api_call(dep_index, trigger_id = null, event_data = null)
472
491
  });
473
492
  }
474
493
  }
494
+ if (allow_video_trim) {
495
+ screen_recorder.markRemoveSegmentEnd();
496
+ }
475
497
  }
476
498
  }
477
499
  function trigger_share(title2, description) {
@@ -612,6 +634,7 @@ function set_status(statuses) {
612
634
  function isCustomEvent(event) {
613
635
  return "detail" in event;
614
636
  }
637
+ let is_screen_recording = writable(false);
615
638
  onMount(() => {
616
639
  document.addEventListener("visibilitychange", function() {
617
640
  if (document.visibilityState === "hidden") {
@@ -621,7 +644,23 @@ onMount(() => {
621
644
  is_mobile_device = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
622
645
  navigator.userAgent
623
646
  );
647
+ screen_recorder.initialize(
648
+ root,
649
+ (title2, message, type) => {
650
+ add_new_message(title2, message, type);
651
+ },
652
+ (isRecording) => {
653
+ $is_screen_recording = isRecording;
654
+ }
655
+ );
624
656
  });
657
+ function screen_recording() {
658
+ if ($is_screen_recording) {
659
+ screen_recorder.stopRecording();
660
+ } else {
661
+ screen_recorder.startRecording();
662
+ }
663
+ }
625
664
  </script>
626
665
 
627
666
  <svelte:head>
@@ -677,6 +716,17 @@ onMount(() => {
677
716
  {$_("common.built_with_gradio")}
678
717
  <img src={logo} alt={$_("common.logo")} />
679
718
  </a>
719
+ <div class="divider" class:hidden={!$is_screen_recording}>·</div>
720
+ <button
721
+ class:hidden={!$is_screen_recording}
722
+ on:click={() => {
723
+ screen_recording();
724
+ }}
725
+ class="record"
726
+ >
727
+ {$_("common.stop_recording")}
728
+ <img src={record_stop} alt={$_("common.stop_recording")} />
729
+ </button>
680
730
  <div class="divider">·</div>
681
731
  <button
682
732
  on:click={() => {
@@ -749,9 +799,14 @@ onMount(() => {
749
799
  />
750
800
  <div class="api-docs-wrap">
751
801
  <Settings
802
+ bind:allow_zoom
803
+ bind:allow_video_trim
752
804
  on:close={(event) => {
753
805
  set_settings_visible(false);
754
806
  }}
807
+ on:start_recording={(event) => {
808
+ screen_recording();
809
+ }}
755
810
  pwa_enabled={app.config.pwa}
756
811
  {root}
757
812
  {space_id}
@@ -791,7 +846,8 @@ onMount(() => {
791
846
  }
792
847
 
793
848
  .show-api,
794
- .settings {
849
+ .settings,
850
+ .record {
795
851
  display: flex;
796
852
  align-items: center;
797
853
  }
@@ -811,13 +867,20 @@ onMount(() => {
811
867
  width: var(--size-4);
812
868
  }
813
869
 
870
+ .record img {
871
+ margin-right: var(--size-1);
872
+ margin-left: var(--size-1);
873
+ width: var(--size-3);
874
+ }
875
+
814
876
  .built-with {
815
877
  display: flex;
816
878
  align-items: center;
817
879
  }
818
880
 
819
881
  .built-with:hover,
820
- .settings:hover {
882
+ .settings:hover,
883
+ .record:hover {
821
884
  color: var(--body-text-color);
822
885
  }
823
886
 
@@ -888,4 +951,8 @@ onMount(() => {
888
951
  .show-api:hover {
889
952
  color: var(--body-text-color);
890
953
  }
954
+
955
+ .hidden {
956
+ display: none;
957
+ }
891
958
  </style>
@@ -4,9 +4,13 @@ export let root;
4
4
  export let space_id;
5
5
  export let pwa_enabled;
6
6
  import { BaseDropdown as Dropdown } from "@gradio/dropdown";
7
+ import { BaseCheckbox as Checkbox } from "@gradio/checkbox";
7
8
  import { language_choices, changeLocale } from "../i18n";
8
9
  import { locale, _ } from "svelte-i18n";
9
10
  import { setupi18n } from "../i18n";
11
+ import record from "./img/record.svg";
12
+ import { createEventDispatcher } from "svelte";
13
+ const dispatch = createEventDispatcher();
10
14
  if (root === "") {
11
15
  root = location.protocol + "//" + location.host + location.pathname;
12
16
  }
@@ -38,6 +42,8 @@ onMount(() => {
38
42
  });
39
43
  let current_locale;
40
44
  let current_theme = "system";
45
+ export let allow_zoom = true;
46
+ export let allow_video_trim = true;
41
47
  locale.subscribe((value) => {
42
48
  if (value) {
43
49
  current_locale = value;
@@ -47,6 +53,12 @@ function handleLanguageChange(e) {
47
53
  const new_locale = e.detail;
48
54
  changeLocale(new_locale);
49
55
  }
56
+ function handleZoomChange(e) {
57
+ allow_zoom = e.detail;
58
+ }
59
+ function handleVideoTrimChange(e) {
60
+ allow_video_trim = e.detail;
61
+ }
50
62
  setupi18n();
51
63
  </script>
52
64
 
@@ -112,6 +124,44 @@ setupi18n();
112
124
  {/if}
113
125
  </p>
114
126
  </div>
127
+ <div class="banner-wrap">
128
+ <h2>{$_("common.screen_studio")} <span class="beta-tag">beta</span></h2>
129
+ <p class="padded">
130
+ Screen Studio allows you to record your screen and generates a video of your
131
+ app with automatically adding zoom in and zoom out effects as well as
132
+ trimming the video to remove the prediction time.
133
+ <br /><br />
134
+ Start recording by clicking the <i>Start Recording</i> button below and then
135
+ sharing the current browser tab of your Gradio demo. Use your app as you
136
+ would normally to generate a prediction.
137
+ <br />
138
+ Stop recording by clicking the <i>Stop Recording</i> button in the footer of
139
+ the demo.
140
+ <br /><br />
141
+ <Checkbox
142
+ label="Include automatic zoom in/out"
143
+ interactive={true}
144
+ value={allow_zoom}
145
+ on:change={handleZoomChange}
146
+ />
147
+ <Checkbox
148
+ label="Include automatic video trimming"
149
+ interactive={true}
150
+ value={allow_video_trim}
151
+ on:change={handleVideoTrimChange}
152
+ />
153
+ </p>
154
+ <button
155
+ class="record-button"
156
+ on:click={() => {
157
+ dispatch("close");
158
+ dispatch("start_recording");
159
+ }}
160
+ >
161
+ <img src={record} alt="Start Recording" />
162
+ Start Recording
163
+ </button>
164
+ </div>
115
165
 
116
166
  <style>
117
167
  .banner-wrap {
@@ -142,7 +192,8 @@ setupi18n();
142
192
  margin-left: var(--size-2);
143
193
  }
144
194
 
145
- .theme-button {
195
+ .theme-button,
196
+ .record-button {
146
197
  display: flex;
147
198
  align-items: center;
148
199
  border: 1px solid var(--border-color-primary);
@@ -154,6 +205,15 @@ setupi18n();
154
205
  cursor: pointer;
155
206
  }
156
207
 
208
+ .record-button img {
209
+ margin-right: var(--size-1);
210
+ margin-left: var(--size-1);
211
+ width: var(--size-3);
212
+ }
213
+ .record-button:hover {
214
+ border-color: red;
215
+ }
216
+
157
217
  .current-theme {
158
218
  border: 1px solid var(--body-text-color-subdued);
159
219
  color: var(--body-text-color);
@@ -173,4 +233,17 @@ setupi18n();
173
233
  all: unset;
174
234
  cursor: pointer;
175
235
  }
236
+
237
+ .beta-tag {
238
+ position: relative;
239
+ top: -5px;
240
+ font-size: var(--text-xs);
241
+ background-color: var(--color-accent);
242
+ color: white;
243
+ padding: 2px 6px;
244
+ border-radius: 10px;
245
+ margin-left: 5px;
246
+ font-weight: normal;
247
+ text-transform: uppercase;
248
+ }
176
249
  </style>
@@ -4,9 +4,12 @@ declare const __propDef: {
4
4
  root: string;
5
5
  space_id: string | null;
6
6
  pwa_enabled: boolean | undefined;
7
+ allow_zoom?: boolean | undefined;
8
+ allow_video_trim?: boolean | undefined;
7
9
  };
8
10
  events: {
9
11
  close: CustomEvent<any>;
12
+ start_recording: CustomEvent<any>;
10
13
  } & {
11
14
  [evt: string]: CustomEvent<any>;
12
15
  };
@@ -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/dist/src/init.js CHANGED
@@ -80,10 +80,15 @@ export function create_components(initial_layout) {
80
80
  * Rerender the layout when the config has been modified to attach new components
81
81
  */
82
82
  function rerender_layout({ render_id, components, layout, root, dependencies }) {
83
+ components.forEach((c) => {
84
+ for (const prop in c.props) {
85
+ if (c.props[prop] === null) {
86
+ c.props[prop] = undefined;
87
+ }
88
+ }
89
+ });
83
90
  let replacement_components = [];
84
91
  let new_components = [];
85
- console.log("old_keys", keys_per_render_id[render_id]);
86
- console.log("new_keys", components.map((c) => c.key));
87
92
  components.forEach((c) => {
88
93
  if (c.key == null || !keys_per_render_id[render_id]?.includes(c.key)) {
89
94
  new_components.push(c);
@@ -92,7 +97,6 @@ export function create_components(initial_layout) {
92
97
  replacement_components.push(c);
93
98
  }
94
99
  });
95
- console.log(new_components.length, replacement_components.length);
96
100
  let _constructor_map = preload_all_components(new_components, root);
97
101
  _constructor_map.forEach((v, k) => {
98
102
  constructor_map.set(k, v);
@@ -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,16 @@
1
+ import type { ToastMessage } from "@gradio/statustracker";
2
+ export declare function initialize(rootPath: string, add_new_message: (title: string, message: string, type: ToastMessage["type"]) => void, recordingStateCallback?: (isRecording: boolean) => void): void;
3
+ export declare function startRecording(): Promise<void>;
4
+ export declare function stopRecording(): void;
5
+ export declare function isCurrentlyRecording(): boolean;
6
+ export declare function markRemoveSegmentStart(): void;
7
+ export declare function markRemoveSegmentEnd(): void;
8
+ export declare function clearRemoveSegment(): void;
9
+ export declare function addZoomEffect(is_input: boolean, params: {
10
+ boundingBox: {
11
+ topLeft: [number, number];
12
+ bottomRight: [number, number];
13
+ };
14
+ duration?: number;
15
+ }): void;
16
+ export declare function zoom(is_input: boolean, elements: number[], duration?: number): void;
@@ -0,0 +1,255 @@
1
+ let isRecording = false;
2
+ let mediaRecorder = null;
3
+ let recordedChunks = [];
4
+ let recordingStartTime = 0;
5
+ let animationFrameId = null;
6
+ let removeSegment = {};
7
+ let root;
8
+ let add_message_callback;
9
+ let onRecordingStateChange = null;
10
+ let zoomEffects = [];
11
+ export function initialize(rootPath, add_new_message, recordingStateCallback) {
12
+ root = rootPath;
13
+ add_message_callback = add_new_message;
14
+ if (recordingStateCallback) {
15
+ onRecordingStateChange = recordingStateCallback;
16
+ }
17
+ }
18
+ export async function startRecording() {
19
+ if (isRecording) {
20
+ return;
21
+ }
22
+ try {
23
+ const originalTitle = document.title;
24
+ document.title = "[Sharing] Gradio Tab";
25
+ const stream = await navigator.mediaDevices.getDisplayMedia({
26
+ video: {
27
+ width: { ideal: 1920 },
28
+ height: { ideal: 1080 },
29
+ frameRate: { ideal: 30 }
30
+ },
31
+ audio: true,
32
+ selfBrowserSurface: "include"
33
+ });
34
+ document.title = originalTitle;
35
+ const options = {
36
+ videoBitsPerSecond: 5000000
37
+ };
38
+ mediaRecorder = new MediaRecorder(stream, options);
39
+ recordedChunks = [];
40
+ removeSegment = {};
41
+ mediaRecorder.ondataavailable = handleDataAvailable;
42
+ mediaRecorder.onstop = handleStop;
43
+ mediaRecorder.start(1000);
44
+ isRecording = true;
45
+ if (onRecordingStateChange) {
46
+ onRecordingStateChange(true);
47
+ }
48
+ recordingStartTime = Date.now();
49
+ }
50
+ catch (error) {
51
+ add_message_callback("Recording Error", "Failed to start recording: " + error.message, "error");
52
+ }
53
+ }
54
+ export function stopRecording() {
55
+ if (!isRecording || !mediaRecorder) {
56
+ return;
57
+ }
58
+ mediaRecorder.stop();
59
+ isRecording = false;
60
+ if (onRecordingStateChange) {
61
+ onRecordingStateChange(false);
62
+ }
63
+ }
64
+ export function isCurrentlyRecording() {
65
+ return isRecording;
66
+ }
67
+ export function markRemoveSegmentStart() {
68
+ if (!isRecording) {
69
+ return;
70
+ }
71
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
72
+ removeSegment.start = currentTime;
73
+ }
74
+ export function markRemoveSegmentEnd() {
75
+ if (!isRecording || removeSegment.start === undefined) {
76
+ return;
77
+ }
78
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
79
+ removeSegment.end = currentTime;
80
+ }
81
+ export function clearRemoveSegment() {
82
+ removeSegment = {};
83
+ }
84
+ export function addZoomEffect(is_input, params) {
85
+ if (!isRecording) {
86
+ return;
87
+ }
88
+ const FPS = 30;
89
+ const currentTime = (Date.now() - recordingStartTime) / 1000;
90
+ const currentFrame = is_input
91
+ ? Math.floor((currentTime - 2) * FPS)
92
+ : Math.floor(currentTime * FPS);
93
+ if (params.boundingBox &&
94
+ params.boundingBox.topLeft &&
95
+ params.boundingBox.bottomRight &&
96
+ params.boundingBox.topLeft.length === 2 &&
97
+ params.boundingBox.bottomRight.length === 2) {
98
+ const newEffectDuration = params.duration || 2.0;
99
+ const newEffectEndFrame = currentFrame + Math.floor(newEffectDuration * FPS);
100
+ const hasOverlap = zoomEffects.some((existingEffect) => {
101
+ const existingEffectEndFrame = existingEffect.start_frame +
102
+ Math.floor((existingEffect.duration || 2.0) * FPS);
103
+ return ((currentFrame >= existingEffect.start_frame &&
104
+ currentFrame <= existingEffectEndFrame) ||
105
+ (newEffectEndFrame >= existingEffect.start_frame &&
106
+ newEffectEndFrame <= existingEffectEndFrame) ||
107
+ (currentFrame <= existingEffect.start_frame &&
108
+ newEffectEndFrame >= existingEffectEndFrame));
109
+ });
110
+ if (!hasOverlap) {
111
+ zoomEffects.push({
112
+ boundingBox: params.boundingBox,
113
+ start_frame: currentFrame,
114
+ duration: newEffectDuration
115
+ });
116
+ }
117
+ }
118
+ }
119
+ export function zoom(is_input, elements, duration = 2.0) {
120
+ if (!isRecording) {
121
+ return;
122
+ }
123
+ try {
124
+ setTimeout(() => {
125
+ if (!elements || elements.length === 0) {
126
+ return;
127
+ }
128
+ let minLeft = Infinity;
129
+ let minTop = Infinity;
130
+ let maxRight = 0;
131
+ let maxBottom = 0;
132
+ let foundElements = false;
133
+ for (const elementId of elements) {
134
+ const selector = `#component-${elementId}`;
135
+ const element = document.querySelector(selector);
136
+ if (element) {
137
+ foundElements = true;
138
+ const rect = element.getBoundingClientRect();
139
+ minLeft = Math.min(minLeft, rect.left);
140
+ minTop = Math.min(minTop, rect.top);
141
+ maxRight = Math.max(maxRight, rect.right);
142
+ maxBottom = Math.max(maxBottom, rect.bottom);
143
+ }
144
+ }
145
+ if (!foundElements) {
146
+ return;
147
+ }
148
+ const viewportWidth = window.innerWidth;
149
+ const viewportHeight = window.innerHeight;
150
+ const boxWidth = Math.min(maxRight, viewportWidth) - Math.max(0, minLeft);
151
+ const boxHeight = Math.min(maxBottom, viewportHeight) - Math.max(0, minTop);
152
+ const widthPercentage = boxWidth / viewportWidth;
153
+ const heightPercentage = boxHeight / viewportHeight;
154
+ if (widthPercentage >= 0.8 || heightPercentage >= 0.8) {
155
+ return;
156
+ }
157
+ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
158
+ let topLeft = [
159
+ Math.max(0, minLeft) / viewportWidth,
160
+ Math.max(0, minTop) / viewportHeight
161
+ ];
162
+ let bottomRight = [
163
+ Math.min(maxRight, viewportWidth) / viewportWidth,
164
+ Math.min(maxBottom, viewportHeight) / viewportHeight
165
+ ];
166
+ if (isSafari) {
167
+ topLeft[0] = Math.max(0, topLeft[0] * 0.9);
168
+ bottomRight[0] = Math.min(1, bottomRight[0] * 0.9);
169
+ const width = bottomRight[0] - topLeft[0];
170
+ const center = (topLeft[0] + bottomRight[0]) / 2;
171
+ const newCenter = center * 0.9;
172
+ topLeft[0] = Math.max(0, newCenter - width / 2);
173
+ bottomRight[0] = Math.min(1, newCenter + width / 2);
174
+ }
175
+ topLeft[0] = Math.max(0, topLeft[0]);
176
+ topLeft[1] = Math.max(0, topLeft[1]);
177
+ bottomRight[0] = Math.min(1, bottomRight[0]);
178
+ bottomRight[1] = Math.min(1, bottomRight[1]);
179
+ addZoomEffect(is_input, {
180
+ boundingBox: {
181
+ topLeft,
182
+ bottomRight
183
+ },
184
+ duration: duration
185
+ });
186
+ }, 300);
187
+ }
188
+ catch (error) {
189
+ // pass
190
+ }
191
+ }
192
+ function handleDataAvailable(event) {
193
+ if (event.data.size > 0) {
194
+ recordedChunks.push(event.data);
195
+ }
196
+ }
197
+ function handleStop() {
198
+ isRecording = false;
199
+ if (onRecordingStateChange) {
200
+ onRecordingStateChange(false);
201
+ }
202
+ const blob = new Blob(recordedChunks, {
203
+ type: "video/mp4"
204
+ });
205
+ handleRecordingComplete(blob);
206
+ const screenStream = mediaRecorder?.stream?.getTracks() || [];
207
+ screenStream.forEach((track) => track.stop());
208
+ if (animationFrameId !== null) {
209
+ cancelAnimationFrame(animationFrameId);
210
+ animationFrameId = null;
211
+ }
212
+ }
213
+ async function handleRecordingComplete(recordedBlob) {
214
+ try {
215
+ add_message_callback("Processing video", "This may take a few seconds...", "info");
216
+ const formData = new FormData();
217
+ formData.append("video", recordedBlob, "recording.mp4");
218
+ if (removeSegment.start !== undefined && removeSegment.end !== undefined) {
219
+ formData.append("remove_segment_start", removeSegment.start.toString());
220
+ formData.append("remove_segment_end", removeSegment.end.toString());
221
+ }
222
+ if (zoomEffects.length > 0) {
223
+ formData.append("zoom_effects", JSON.stringify(zoomEffects));
224
+ }
225
+ const response = await fetch(root + "/gradio_api/process_recording", {
226
+ method: "POST",
227
+ body: formData
228
+ });
229
+ if (!response.ok) {
230
+ throw new Error(`Server returned ${response.status}: ${response.statusText}`);
231
+ }
232
+ const processedBlob = await response.blob();
233
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
234
+ saveWithDownloadAttribute(processedBlob, defaultFilename);
235
+ zoomEffects = [];
236
+ }
237
+ catch (error) {
238
+ add_message_callback("Processing Error", "Failed to process recording. Saving original version.", "warning");
239
+ const defaultFilename = `gradio-screen-recording-${new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "")}.mp4`;
240
+ saveWithDownloadAttribute(recordedBlob, defaultFilename);
241
+ }
242
+ }
243
+ function saveWithDownloadAttribute(blob, suggestedName) {
244
+ const url = URL.createObjectURL(blob);
245
+ const a = document.createElement("a");
246
+ a.style.display = "none";
247
+ a.href = url;
248
+ a.download = suggestedName;
249
+ document.body.appendChild(a);
250
+ a.click();
251
+ setTimeout(() => {
252
+ document.body.removeChild(a);
253
+ URL.revokeObjectURL(url);
254
+ }, 100);
255
+ }