gstaff commited on
Commit
78477bb
β€’
1 Parent(s): e38daff

Add demo files.

Browse files
Files changed (5) hide show
  1. README.md +28 -3
  2. app.py +42 -0
  3. main.js +74 -0
  4. record_button.js +40 -0
  5. recorder.js +112 -0
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
  title: Gradio Screen Recorder
3
- emoji: πŸ’»
4
  colorFrom: blue
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 4.7.1
8
  app_file: app.py
@@ -10,4 +10,29 @@ pinned: false
10
  license: apache-2.0
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Gradio Screen Recorder
3
+ emoji: πŸ’»πŸŽ₯
4
  colorFrom: blue
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 4.7.1
8
  app_file: app.py
 
10
  license: apache-2.0
11
  ---
12
 
13
+ # Gradio Screen Recorder
14
+
15
+ This is a simple example of how a screen recorder button can be configured in Gradio.
16
+
17
+ Depending on your current settings, you may need to grant permission to your browser to record your screen or to use your microphone if recording a voiceover.
18
+
19
+ When there is time I may make it into a standalone component using the new support for custom components.
20
+
21
+ ## Dependencies
22
+ This demo has a dependency on `ffmpeg` to convert `webm` files to `mp4` format. On linux (debian) install with `apt-get install ffmpeg`.
23
+
24
+ ## Limitations
25
+ This demo uses the Media Recording API of supported browsers. Some details on Media Recording API limitations can be found [here](
26
+ https://blog.addpipe.com/mediarecorder-api/#:~:text=Firefox%20or%20Chrome.-,Final%20Conclusions,-Although%20a%20simple)
27
+
28
+ ### TODO Features
29
+ - Any way to work within colab cells given js restrictions?
30
+ - Option to convert and save as gif rather than webm or mp4
31
+ - Hotkeys for start / stop
32
+ - Prerecording countdown [as in Streamlit](https://docs.streamlit.io/library/advanced-features/app-menu#:~:text=to%2Dpdf%20function.-,Record%20a%20screencast,-You%20can%20easily)
33
+ - Streaming support via [MediaStream Web API?](https://dev.to/antopiras89/using-the-mediastream-web-api-to-record-screen-camera-and-audio-1c4n)?
34
+
35
+ ### TODO Cleanup
36
+ - Address any limits on recording size (base64 string an issue?)
37
+ - Additional error handling around recorder setup
38
+ - Namespace window variables to prevent possible collisions
app.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import pathlib
3
+ import tempfile
4
+ import gradio as gr
5
+
6
+ recorder_js = pathlib.Path('recorder.js').read_text()
7
+ main_js = pathlib.Path('main.js').read_text()
8
+ record_button_js = pathlib.Path('record_button.js').read_text().replace('let recorder_js = null;', recorder_js).replace(
9
+ 'let main_js = null;', main_js)
10
+
11
+
12
+ def save_base64_video(base64_string):
13
+ base64_video = base64_string
14
+ video_data = base64.b64decode(base64_video)
15
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file:
16
+ temp_filename = temp_file.name
17
+ temp_file.write(video_data)
18
+ print(f"Temporary MP4 file saved as: {temp_filename}")
19
+ return temp_filename
20
+
21
+
22
+ with gr.Blocks(title="Screen Recorder Demo") as demo:
23
+ start_button = gr.Button("Record Screen πŸ”΄")
24
+ video_component = gr.Video(interactive=True, show_share_button=True)
25
+
26
+
27
+ def toggle_button_label(returned_string):
28
+ if returned_string.startswith("Record"):
29
+ return gr.Button(value="Stop Recording βšͺ"), None
30
+ else:
31
+ try:
32
+ temp_filename = save_base64_video(returned_string)
33
+ except Exception as e:
34
+ return gr.Button(value="Record Screen πŸ”΄"), gr.Warning(f'Failed to convert video to mp4:\n{e}')
35
+ return gr.Button(value="Record Screen πŸ”΄"), gr.Video(value=temp_filename, interactive=True,
36
+ show_share_button=True)
37
+
38
+
39
+ start_button.click(toggle_button_label, start_button, [start_button, video_component], js=record_button_js)
40
+
41
+ if __name__ == "__main__":
42
+ demo.launch()
main.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // main.js
2
+ if (!ScreenCastRecorder.isSupportedBrowser()) {
3
+ console.error("Screen Recording not supported in this browser");
4
+ }
5
+ let recorder;
6
+ let outputBlob;
7
+ const stopRecording = () => __awaiter(void 0, void 0, void 0, function* () {
8
+ let currentState = "RECORDING";
9
+ // We should do nothing if the user try to stop recording when it is not started
10
+ if (currentState === "OFF" || recorder == null) {
11
+ return;
12
+ }
13
+ // if (currentState === "COUNTDOWN") {
14
+ // this.setState({
15
+ // currentState: "OFF",
16
+ // })
17
+ // }
18
+ if (currentState === "RECORDING") {
19
+ if (recorder.getState() === "inactive") {
20
+ // this.setState({
21
+ // currentState: "OFF",
22
+ // })
23
+ console.log("Inactive");
24
+ }
25
+ else {
26
+ outputBlob = yield recorder.stop();
27
+ console.log("Done recording");
28
+ // this.setState({
29
+ // outputBlob,
30
+ // currentState: "PREVIEW_FILE",
31
+ // })
32
+ window.currentState = "PREVIEW_FILE";
33
+ const videoSource = URL.createObjectURL(outputBlob);
34
+ window.videoSource = videoSource;
35
+ const fileName = "recording";
36
+ const link = document.createElement("a");
37
+ link.setAttribute("href", videoSource);
38
+ link.setAttribute("download", `${fileName}.webm`);
39
+ link.click();
40
+ }
41
+ }
42
+ });
43
+ const startRecording = () => __awaiter(void 0, void 0, void 0, function* () {
44
+ const recordAudio = true;
45
+ recorder = new ScreenCastRecorder({
46
+ recordAudio,
47
+ onErrorOrStop: () => stopRecording(),
48
+ });
49
+ try {
50
+ yield recorder.initialize();
51
+ }
52
+ catch (e) {
53
+ console.warn(`ScreenCastRecorder.initialize error: ${e}`);
54
+ // this.setState({ currentState: "UNSUPPORTED" })
55
+ window.currentState = "UNSUPPORTED";
56
+ return;
57
+ }
58
+ // this.setState({ currentState: "COUNTDOWN" })
59
+ const hasStarted = recorder.start();
60
+ if (hasStarted) {
61
+ // this.setState({
62
+ // currentState: "RECORDING",
63
+ // })
64
+ console.log("Started recording");
65
+ window.currentState = "RECORDING";
66
+ }
67
+ else {
68
+ stopRecording().catch(err => console.warn(`withScreencast.stopRecording threw an error: ${err}`));
69
+ }
70
+ });
71
+
72
+ // Set global functions to window.
73
+ window.startRecording = startRecording;
74
+ window.stopRecording = stopRecording;
record_button.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Setup if needed and start recording.
2
+ async () => {
3
+ // Set up recording functions if not already initialized
4
+ if (!window.startRecording) {
5
+ let recorder_js = null;
6
+ let main_js = null;
7
+ }
8
+
9
+ // Function to fetch and convert video blob to base64 using async/await without explicit Promise
10
+ async function getVideoBlobAsBase64(objectURL) {
11
+ const response = await fetch(objectURL);
12
+ if (!response.ok) {
13
+ throw new Error('Failed to fetch video blob.');
14
+ }
15
+
16
+ const blob = await response.blob();
17
+
18
+ const reader = new FileReader();
19
+ reader.readAsDataURL(blob);
20
+
21
+ return new Promise((resolve, reject) => {
22
+ reader.onloadend = () => {
23
+ if (reader.result) {
24
+ resolve(reader.result.split(',')[1]); // Return the base64 string (without data URI prefix)
25
+ } else {
26
+ reject('Failed to convert blob to base64.');
27
+ }
28
+ };
29
+ });
30
+ }
31
+
32
+ if (window.currentState === "RECORDING") {
33
+ await window.stopRecording();
34
+ const base64String = await getVideoBlobAsBase64(window.videoSource);
35
+ return base64String;
36
+ } else {
37
+ window.startRecording();
38
+ return "Record";
39
+ }
40
+ }
recorder.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // recorder.js
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ const BLOB_TYPE = "video/webm";
12
+ class ScreenCastRecorder {
13
+ /** True if the current browser likely supports screencasts. */
14
+ static isSupportedBrowser() {
15
+ return (navigator.mediaDevices != null &&
16
+ navigator.mediaDevices.getUserMedia != null &&
17
+ navigator.mediaDevices.getDisplayMedia != null &&
18
+ MediaRecorder.isTypeSupported(BLOB_TYPE));
19
+ }
20
+ constructor({ recordAudio, onErrorOrStop }) {
21
+ this.recordAudio = recordAudio;
22
+ this.onErrorOrStopCallback = onErrorOrStop;
23
+ this.inputStream = null;
24
+ this.recordedChunks = [];
25
+ this.mediaRecorder = null;
26
+ }
27
+ /**
28
+ * This asynchronous method will initialize the screen recording object asking
29
+ * for permissions to the user which are needed to start recording.
30
+ */
31
+ initialize() {
32
+ return __awaiter(this, void 0, void 0, function* () {
33
+ const desktopStream = yield navigator.mediaDevices.getDisplayMedia({
34
+ video: true,
35
+ });
36
+ let tracks = desktopStream.getTracks();
37
+ if (this.recordAudio) {
38
+ const voiceStream = yield navigator.mediaDevices.getUserMedia({
39
+ video: false,
40
+ audio: true,
41
+ });
42
+ tracks = tracks.concat(voiceStream.getAudioTracks());
43
+ }
44
+ this.recordedChunks = [];
45
+ this.inputStream = new MediaStream(tracks);
46
+ this.mediaRecorder = new MediaRecorder(this.inputStream, {
47
+ mimeType: BLOB_TYPE,
48
+ });
49
+ this.mediaRecorder.ondataavailable = e => this.recordedChunks.push(e.data);
50
+ });
51
+ }
52
+ getState() {
53
+ if (this.mediaRecorder) {
54
+ return this.mediaRecorder.state;
55
+ }
56
+ return "inactive";
57
+ }
58
+ /**
59
+ * This method will start the screen recording if the user has granted permissions
60
+ * and the mediaRecorder has been initialized
61
+ *
62
+ * @returns {boolean}
63
+ */
64
+ start() {
65
+ if (!this.mediaRecorder) {
66
+ console.warn(`ScreenCastRecorder.start: mediaRecorder is null`);
67
+ return false;
68
+ }
69
+ const logRecorderError = (e) => {
70
+ console.warn(`mediaRecorder.start threw an error: ${e}`);
71
+ };
72
+ this.mediaRecorder.onerror = (e) => {
73
+ logRecorderError(e);
74
+ this.onErrorOrStopCallback();
75
+ };
76
+ this.mediaRecorder.onstop = () => this.onErrorOrStopCallback();
77
+ try {
78
+ this.mediaRecorder.start();
79
+ }
80
+ catch (e) {
81
+ logRecorderError(e);
82
+ return false;
83
+ }
84
+ return true;
85
+ }
86
+ /**
87
+ * This method will stop recording and then return the generated Blob
88
+ *
89
+ * @returns {(Promise|undefined)}
90
+ * A Promise which will return the generated Blob
91
+ * Undefined if the MediaRecorder could not initialize
92
+ */
93
+ stop() {
94
+ if (!this.mediaRecorder) {
95
+ return undefined;
96
+ }
97
+ let resolver;
98
+ const promise = new Promise(r => {
99
+ resolver = r;
100
+ });
101
+ this.mediaRecorder.onstop = () => resolver();
102
+ this.mediaRecorder.stop();
103
+ if (this.inputStream) {
104
+ this.inputStream.getTracks().forEach(s => s.stop());
105
+ this.inputStream = null;
106
+ }
107
+ return promise.then(() => this.buildOutputBlob());
108
+ }
109
+ buildOutputBlob() {
110
+ return new Blob(this.recordedChunks, { type: BLOB_TYPE });
111
+ }
112
+ }