Skip to content

Commit 43207c9

Browse files
committed
Video Recorder
1 parent 82702c5 commit 43207c9

File tree

4 files changed

+193
-0
lines changed

4 files changed

+193
-0
lines changed

features/features.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[
2+
{
3+
"version": 2,
4+
"id": "video-recorder",
5+
"versionAdded": "v4.0.0"
6+
},
27
{
38
"version": 2,
49
"id": "studio-creation-date",

features/video-recorder/data.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"title": "Video Recorder",
3+
"description": "Record videos of Scratch projects.",
4+
"credits": [
5+
{
6+
"username": "stio_studio",
7+
"url": "https://stio.studio/"
8+
},
9+
{
10+
"username": "blob2763",
11+
"url": "https://blob2763.is-a.dev/"
12+
}
13+
],
14+
"type": ["Editor"],
15+
"tags": ["New", "Featured"],
16+
"dynamic": true,
17+
"scripts": [
18+
{
19+
"file": "video-recorder.js",
20+
"runOn": "/projects/*"
21+
}
22+
],
23+
"resources": [
24+
{ "name": "popup-html", "path": "/popup.html" }
25+
]
26+
}

features/video-recorder/popup.html

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<div class="ReactModalPortal">
2+
<div class="ReactModal__Overlay ReactModal__Overlay--after-open modal_modal-overlay_1Lcbx">
3+
<div class="ReactModal__Content ReactModal__Content--after-open modal_modal-content_1h3ll prompt_modal-content_1BfWj"
4+
tabindex="-1" role="dialog" aria-label="Rename Variable">
5+
<div class="box_box_2jjDp" dir="ltr" style="flex-direction: column; flex-grow: 1;">
6+
<div class="modal_header_1h7ps">
7+
<div class="modal_header-item_2zQTd modal_header-item-title_tLOU5">Video Recording</div>
8+
<div class="modal_header-item_2zQTd modal_header-item-close_2XDeL">
9+
<div aria-label="Close" class="close-button_close-button_lOp2G close-button_large_2oadS"
10+
role="button" tabindex="0"><img class="close-button_close-icon_HBCuO"
11+
src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3LjQ4IDcuNDgiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDpub25lO3N0cm9rZTojZmZmO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6MnB4O308L3N0eWxlPjwvZGVmcz48dGl0bGU+aWNvbi0tYWRkPC90aXRsZT48bGluZSBjbGFzcz0iY2xzLTEiIHgxPSIzLjc0IiB5MT0iNi40OCIgeDI9IjMuNzQiIHkyPSIxIi8+PGxpbmUgY2xhc3M9ImNscy0xIiB4MT0iMSIgeTE9IjMuNzQiIHgyPSI2LjQ4IiB5Mj0iMy43NCIvPjwvc3ZnPg==">
12+
</div>
13+
</div>
14+
</div>
15+
<style>
16+
video {
17+
width: 100%;
18+
height: 100%;
19+
border: 10px solid #ccc;
20+
border-radius: 10px;
21+
}
22+
.STE-hide-button {
23+
display: none;
24+
}
25+
.STE-left-text {
26+
text-align: left;
27+
}
28+
.stopButton, .startButton, .downloadButton, .video-format-select {
29+
width: 100%;
30+
}
31+
</style>
32+
<div class="prompt_body_18Z-I box_box_2jjDp">
33+
<!-- <div class="prompt_label_tWjYZ box_box_2jjDp"></div>
34+
<div class="box_box_2jjDp"><input class="prompt_variable-name-text-input_1iu8-"
35+
name="Rename all &quot;box size&quot; variables to:" value="box size"></div> -->
36+
<div class="prompt_button-row_3Wc5Z box_box_2jjDp STE-left-text">
37+
<button class="stopButton STE-hide-button"><span>Stop Recording</span></button>
38+
<button class="prompt_ok-button_3QFdD startButton"><span>Start Recording</span></button>
39+
<br><br>
40+
<select class="video-format-select">
41+
<option value="mp4">mp4</option>
42+
<option value="webm">webm</option>
43+
</select>
44+
<br><br>
45+
Video viewed here: <br>
46+
<video></video>
47+
<button class="prompt_ok-button_3QFdD downloadButton"><span>Download Video</span></button>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
</div>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
export default async function ({ feature, console }) {
2+
3+
const row = await new Promise(async (resolve, reject) => {
4+
(async () => {
5+
const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons")
6+
resolve(rem);
7+
})();
8+
(async () => {
9+
const rem = await ScratchTools.waitForElement(".menu-bar_account-info-group_MeJZP")
10+
resolve(rem);
11+
})();
12+
})
13+
14+
let openPopup = document.createElement("button");
15+
openPopup.className = "button action-button ste-video-recorder-open";
16+
openPopup.textContent = "Record Video";
17+
row.insertAdjacentElement("afterbegin", openPopup);
18+
19+
let popup = document.createElement("div");
20+
popup.insertAdjacentHTML("afterbegin", await (await fetch(feature.self.getResource("popup-html"))).text())
21+
popup = popup.querySelector("div.ReactModalPortal")
22+
23+
let stopButton = popup.querySelector(".stopButton");
24+
let startButton = popup.querySelector(".startButton");
25+
let closeButton = popup.querySelector(".close-button_close-button_lOp2G");
26+
let downloadButton = popup.querySelector(".downloadButton");
27+
let lastDownloadFunction = ()=>{}
28+
let mimeType = popup.querySelector("select");
29+
30+
31+
// console.log([stopButton, startButton])
32+
33+
openPopup.addEventListener('click', () => {
34+
document.body.append(popup)
35+
})
36+
37+
// console.log(closeButton)
38+
closeButton.addEventListener('click', () => {
39+
document.querySelector(".ReactModalPortal").remove()
40+
})
41+
popup.querySelector(".ReactModal__Overlay").addEventListener('click', () => {
42+
document.querySelector(".ReactModalPortal").remove()
43+
})
44+
addEventListener("keydown", (e) => {
45+
if (e.key === "Escape") {
46+
document.querySelector(".ReactModalPortal").remove()
47+
}
48+
})
49+
50+
const canvas = feature.traps.vm.renderer.canvas;
51+
const preview = popup.querySelector("video")
52+
const projectTitle = document.querySelector("input.inplace-input") || document.querySelector("input.project-title-input_title-field_en5Gd")
53+
// document.querySelector(".menu-bar_account-info-group_MeJZP").append(preview)
54+
55+
let mediaRecorder;
56+
let recordedChunks = [];
57+
// console.log(startButton)
58+
// Start recording
59+
startButton.addEventListener('click', () => {
60+
startButton.classList.add("STE-hide-button");
61+
stopButton.classList.remove("STE-hide-button");
62+
63+
// Capture the canvas element as a stream
64+
const stream = canvas.captureStream(30); // 30 FPS
65+
mediaRecorder = new MediaRecorder(stream);
66+
67+
mediaRecorder.ondataavailable = function (event) {
68+
if (event.data.size > 0) {
69+
recordedChunks.push(event.data);
70+
}
71+
};
72+
73+
mediaRecorder.onstop = function () {
74+
const blob = new Blob(recordedChunks, {
75+
type: `video/${mimeType.value}`
76+
});
77+
preview.src = URL.createObjectURL(blob);
78+
preview.controls = true;
79+
preview.download = `${projectTitle.value}.${mimeType.value}`
80+
downloadButton.removeEventListener("click", lastDownloadFunction)
81+
lastDownloadFunction = async () => {
82+
const url = URL.createObjectURL(blob)
83+
const a = document.createElement('a')
84+
a.href = url
85+
a.download = `${projectTitle.value}.${mimeType.value}`
86+
document.body.appendChild(a)
87+
a.click()
88+
document.body.removeChild(a)
89+
URL.revokeObjectURL(url)
90+
}
91+
downloadButton.addEventListener("click", lastDownloadFunction)
92+
recordedChunks = [];
93+
};
94+
95+
mediaRecorder.start();
96+
startButton.disabled = true;
97+
stopButton.disabled = false;
98+
});
99+
100+
// Stop recording
101+
stopButton.addEventListener('click', () => {
102+
mediaRecorder.stop();
103+
startButton.disabled = false;
104+
stopButton.disabled = true;
105+
106+
stopButton.classList.add("STE-hide-button");
107+
startButton.classList.remove("STE-hide-button");
108+
});
109+
}

0 commit comments

Comments
 (0)