From ced95e28e1e59ac59c5fb9c739518c606d55923d Mon Sep 17 00:00:00 2001 From: stio Date: Tue, 30 Jul 2024 23:13:21 +0200 Subject: [PATCH 1/6] Picture in Picture --- features/features.json | 5 ++ features/picture-in-picture/data.json | 23 ++++++++ .../picture-in-picture/picture-in-picture.js | 56 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 features/picture-in-picture/data.json create mode 100644 features/picture-in-picture/picture-in-picture.js diff --git a/features/features.json b/features/features.json index 9bc58c5c..bfbb525a 100644 --- a/features/features.json +++ b/features/features.json @@ -1,4 +1,9 @@ [ + { + "version": 2, + "id": "picture-in-picture", + "versionAdded": "v4.0.0" + }, { "version": 2, "id": "studio-creation-date", diff --git a/features/picture-in-picture/data.json b/features/picture-in-picture/data.json new file mode 100644 index 00000000..437f4566 --- /dev/null +++ b/features/picture-in-picture/data.json @@ -0,0 +1,23 @@ +{ + "title": "Picture in Picture", + "description": "Adds a button for opening a picture in picture for projects.", + "credits": [ + { + "username": "stio_studio", + "url": "https://stio.studio/" + } + ], + "type": ["Website"], + "tags": ["New", "Featured"], + "dynamic": true, + "scripts": [ + { + "file": "picture-in-picture.js", + "runOn": "/projects/*" + } + ], + "components": [{ + "type": "info", + "content": "Picture in Picture does NOT have inputs, of any kind. This means that it can mostly only be used for watching." + }] +} diff --git a/features/picture-in-picture/picture-in-picture.js b/features/picture-in-picture/picture-in-picture.js new file mode 100644 index 00000000..f97f7bf3 --- /dev/null +++ b/features/picture-in-picture/picture-in-picture.js @@ -0,0 +1,56 @@ +export default async function ({ feature, console }) { + const canvas = feature.traps.vm.renderer.canvas; + + let video = document.createElement("video"); + // video.setAttribute("controls", "controls"); + video.setAttribute("autoplay", "autoplay"); + video.setAttribute("style", "width: 100%; height: 100%"); + // document.querySelector(".preview .inner").append(video); + + video.srcObject = canvas.captureStream(30) + + await new Promise(async (resolve, reject) => { + (async () => { + const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons") + resolve(rem); + })(); + (async () => { + const rem = await ScratchTools.waitForElement(".menu-bar_account-info-group_MeJZP") + resolve(rem); + })(); + }) + + let openPopup = document.createElement("button"); + + ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) { + if (row.querySelector(".ste-video-recorder-open")) return; + openPopup = document.createElement("button"); + openPopup.className = "button action-button ste-video-recorder-open"; + openPopup.textContent = "Picture in Picture"; + row.insertAdjacentElement("afterbegin", openPopup); + openPopup.addEventListener('click', () => { + popup() + }) + }) + ScratchTools.waitForElements(".menu-bar_account-info-group_MeJZP", async function (row) { + if (row.querySelector(".ste-video-recorder-open")) return; + openPopup = document.createElement("div"); + openPopup.className = "menu-bar_menu-bar-item_oLDa- menu-bar_hoverable_c6WFB"; + let rem = document.createElement("div"); + rem.textContent = "Picture in Picture"; + openPopup.append(rem); + row.insertAdjacentElement("afterbegin", openPopup); + openPopup.addEventListener('click', () => { + popup() + }) + }) + + function popup() { + try { + video.requestPictureInPicture() + } + catch { + console.log("Picture in Picture not supported or failed to request") + } + } +} From 2e87f9c1eb4f6606a3901bb8b7fe87b28b785512 Mon Sep 17 00:00:00 2001 From: stio Date: Tue, 30 Jul 2024 23:36:36 +0200 Subject: [PATCH 2/6] Fixed mess up --- features/features.json | 5 - features/video-recorder/data.json | 35 ----- features/video-recorder/popup.html | 50 ------ features/video-recorder/style.css | 21 --- features/video-recorder/video-recorder.js | 179 ---------------------- 5 files changed, 290 deletions(-) delete mode 100644 features/video-recorder/data.json delete mode 100644 features/video-recorder/popup.html delete mode 100644 features/video-recorder/style.css delete mode 100644 features/video-recorder/video-recorder.js diff --git a/features/features.json b/features/features.json index e1575649..bfbb525a 100644 --- a/features/features.json +++ b/features/features.json @@ -4,11 +4,6 @@ "id": "picture-in-picture", "versionAdded": "v4.0.0" }, - { - "version": 2, - "id": "video-recorder", - "versionAdded": "v4.0.0" - }, { "version": 2, "id": "studio-creation-date", diff --git a/features/video-recorder/data.json b/features/video-recorder/data.json deleted file mode 100644 index 97288bc0..00000000 --- a/features/video-recorder/data.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "title": "Video Recorder", - "description": "Record videos of Scratch projects.", - "credits": [ - { - "username": "blob2763", - "url": "https://blob2763.is-a.dev/" - }, - { - "username": "stio_studio", - "url": "https://stio.studio/" - } - ], - "type": ["Editor"], - "tags": ["New", "Featured"], - "dynamic": true, - "scripts": [ - { - "file": "video-recorder.js", - "runOn": "/projects/*" - } - ], - "styles": [ - { - "file": "style.css", - "runOn": "/projects/*" - } - ], - "resources": [ - { - "name": "popup-html", - "path": "/popup.html" - } - ] -} diff --git a/features/video-recorder/popup.html b/features/video-recorder/popup.html deleted file mode 100644 index ef9c057c..00000000 --- a/features/video-recorder/popup.html +++ /dev/null @@ -1,50 +0,0 @@ -
- -
\ No newline at end of file diff --git a/features/video-recorder/style.css b/features/video-recorder/style.css deleted file mode 100644 index 7f62e73c..00000000 --- a/features/video-recorder/style.css +++ /dev/null @@ -1,21 +0,0 @@ -.STE-ReactModalPortal .STE-recorded-video { - width: 100%; - height: 100%; - border: 10px solid #ccc; - border-radius: 10px; -} - -.STE-ReactModalPortal .STE-hide-button { - display: none; -} - -.STE-ReactModalPortal .STE-left-text { - text-align: left; -} - -.STE-ReactModalPortal .stopButton, -.STE-ReactModalPortal .startButton, -.STE-ReactModalPortal .downloadButton, -.STE-ReactModalPortal .video-format-select { - width: 100%; -} diff --git a/features/video-recorder/video-recorder.js b/features/video-recorder/video-recorder.js deleted file mode 100644 index 5bdee8b5..00000000 --- a/features/video-recorder/video-recorder.js +++ /dev/null @@ -1,179 +0,0 @@ -export default async function ({ feature, console }) { - await new Promise(async (resolve, reject) => { - (async () => { - const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons") - resolve(rem); - })(); - (async () => { - const rem = await ScratchTools.waitForElement(".menu-bar_account-info-group_MeJZP") - resolve(rem); - })(); - }) - - let openPopup = document.createElement("button"); - - ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) { - if (row.querySelector(".ste-video-recorder-open")) return; - openPopup = document.createElement("button"); - openPopup.className = "button action-button ste-video-recorder-open"; - openPopup.textContent = "Record Video"; - row.insertAdjacentElement("afterbegin", openPopup); - openPopup.addEventListener('click', () => { - document.body.append(popup) - }) - }) - - ScratchTools.waitForElements(".menu-bar_account-info-group_MeJZP", async function (row) { - if (row.querySelector(".ste-video-recorder-open")) return; - openPopup = document.createElement("div"); - openPopup.className = "menu-bar_menu-bar-item_oLDa- menu-bar_hoverable_c6WFB"; - let rem = document.createElement("div"); - rem.textContent = "Record Video"; - openPopup.append(rem); - row.insertAdjacentElement("afterbegin", openPopup); - openPopup.addEventListener('click', () => { - document.body.append(popup) - }) - }) - - let popup = document.createElement("div"); - popup.insertAdjacentHTML("afterbegin", await (await fetch(feature.self.getResource("popup-html"))).text()) - popup = popup.querySelector("div.ReactModalPortal") - - let stopButton = popup.querySelector(".stopButton"); - let startButton = popup.querySelector(".startButton"); - let closeButton = popup.querySelector(".close-button_close-button_lOp2G"); - let downloadButton = popup.querySelector(".downloadButton"); - let lastDownloadFunction = () => { } - let mimeType = popup.querySelector("select"); - let microphoneCheckbox = popup.querySelector(".microphoneCheckbox"); - let desktopSoundCheckbox = popup.querySelector(".desktopSoundCheckbox"); - - closeButton.addEventListener('click', () => { - document.querySelector(".STE-ReactModalPortal").remove() - }) - addEventListener("keydown", (e) => { - if (e.key === "Escape") { - document.querySelector(".STE-ReactModalPortal").remove() - } - }) - - const canvas = feature.traps.vm.renderer.canvas; - const preview = popup.querySelector("video") - - await new Promise(async (resolve, reject) => { - (async () => { - const rem = await ScratchTools.waitForElement("input.inplace-input") - resolve(rem); - })(); - (async () => { - const rem = await ScratchTools.waitForElement("input.project-title-input_title-field_en5Gd") - resolve(rem); - })(); - (async () => { - const rem = await ScratchTools.waitForElement(".project-title") - resolve(rem); - })(); - }) - - let projectTitle = document.querySelector("input.inplace-input") || document.querySelector("input.project-title-input_title-field_en5Gd") || document.querySelector(".project-title"); - - ScratchTools.waitForElements("input.inplace-input", async function (_projectTitle) { - projectTitle = _projectTitle - }) - - ScratchTools.waitForElements("input.project-title-input_title-field_en5Gd", async function (_projectTitle) { - projectTitle = _projectTitle - }) - - ScratchTools.waitForElements(".project-title", async function (_projectTitle) { - projectTitle = _projectTitle - }) - - - let mediaRecorder; - let recordedChunks = []; - - startButton.addEventListener('click', async () => { - startButton.classList.add("STE-hide-button"); - stopButton.classList.remove("STE-hide-button"); - - // Capture the canvas element as a stream - const canvasStream = canvas.captureStream(30); // 30 FPS - - // Get the audio context from the Scratch VM - const audioContext = feature.traps.vm.runtime.audioEngine.audioContext; - const audioDestination = audioContext.createMediaStreamDestination(); - - if (microphoneCheckbox.checked) { - // Capture the microphone audio - let micStream; - try { - micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); - } catch (err) { - console.error("Error capturing microphone audio:", err); - } - - if (micStream) { - const micSource = audioContext.createMediaStreamSource(micStream); - micSource.connect(audioDestination); - } - } - - // Connect the audio engine's output - if (desktopSoundCheckbox.checked) { - feature.traps.vm.runtime.audioEngine.inputNode.connect(audioDestination); - } - - // Combine the canvas video track and audio tracks - const combinedStream = new MediaStream(); - canvasStream.getVideoTracks().forEach(track => combinedStream.addTrack(track)); - if (microphoneCheckbox.checked || desktopSoundCheckbox.checked) { - audioDestination.stream.getAudioTracks().forEach(track => combinedStream.addTrack(track)); - } - - mediaRecorder = new MediaRecorder(combinedStream); - - mediaRecorder.ondataavailable = function (event) { - if (event.data.size > 0) { - recordedChunks.push(event.data); - } - }; - - mediaRecorder.onstop = function () { - const blob = new Blob(recordedChunks, { - type: `video/${mimeType.value}` - }); - preview.src = URL.createObjectURL(blob); - preview.controls = true; - // console.log(projectTitle) - preview.download = `${projectTitle.value}.${mimeType.value}`; - downloadButton.removeEventListener("click", lastDownloadFunction); - lastDownloadFunction = async () => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `${projectTitle.value}.${mimeType.value}`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } - downloadButton.addEventListener("click", lastDownloadFunction); - recordedChunks = []; - }; - - mediaRecorder.start(); - startButton.disabled = true; - stopButton.disabled = false; - }); - - stopButton.addEventListener('click', () => { - mediaRecorder.stop(); - startButton.disabled = false; - stopButton.disabled = true; - - stopButton.classList.add("STE-hide-button"); - startButton.classList.remove("STE-hide-button"); - }); -} From c6abcaf9e781150eae305debfb147d19daad9348 Mon Sep 17 00:00:00 2001 From: stio Date: Tue, 30 Jul 2024 23:37:56 +0200 Subject: [PATCH 3/6] bug fix --- features/picture-in-picture/picture-in-picture.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/picture-in-picture/picture-in-picture.js b/features/picture-in-picture/picture-in-picture.js index f97f7bf3..e17c88f7 100644 --- a/features/picture-in-picture/picture-in-picture.js +++ b/features/picture-in-picture/picture-in-picture.js @@ -23,9 +23,9 @@ export default async function ({ feature, console }) { let openPopup = document.createElement("button"); ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) { - if (row.querySelector(".ste-video-recorder-open")) return; + if (row.querySelector(".ste-picture-in-picture")) return; openPopup = document.createElement("button"); - openPopup.className = "button action-button ste-video-recorder-open"; + openPopup.className = "button action-button ste-picture-in-picture"; openPopup.textContent = "Picture in Picture"; row.insertAdjacentElement("afterbegin", openPopup); openPopup.addEventListener('click', () => { @@ -33,7 +33,7 @@ export default async function ({ feature, console }) { }) }) ScratchTools.waitForElements(".menu-bar_account-info-group_MeJZP", async function (row) { - if (row.querySelector(".ste-video-recorder-open")) return; + if (row.querySelector(".ste-picture-in-picture")) return; openPopup = document.createElement("div"); openPopup.className = "menu-bar_menu-bar-item_oLDa- menu-bar_hoverable_c6WFB"; let rem = document.createElement("div"); From 2d41080a70b07bd22cae29bfbd401fba59555aa0 Mon Sep 17 00:00:00 2001 From: rgantzos <86856959+rgantzos@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:44:01 -0700 Subject: [PATCH 4/6] Update data.json --- features/picture-in-picture/data.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/picture-in-picture/data.json b/features/picture-in-picture/data.json index 437f4566..4f16fec3 100644 --- a/features/picture-in-picture/data.json +++ b/features/picture-in-picture/data.json @@ -1,6 +1,6 @@ { "title": "Picture in Picture", - "description": "Adds a button for opening a picture in picture for projects.", + "description": "Adds a button to the project page that allows you to open the stage up and continue to view it while using other tabs or apps.", "credits": [ { "username": "stio_studio", @@ -18,6 +18,6 @@ ], "components": [{ "type": "info", - "content": "Picture in Picture does NOT have inputs, of any kind. This means that it can mostly only be used for watching." + "content": "Picture in Picture will not allow you to interact with the project. You must be on the project page to interact with it." }] } From a388a0f36296b31ac64098c7642729fe4c49bbd2 Mon Sep 17 00:00:00 2001 From: rgantzos <86856959+rgantzos@users.noreply.github.com> Date: Tue, 30 Jul 2024 15:46:44 -0700 Subject: [PATCH 5/6] try to resolve some issues --- features/video-recorder/data.json | 34 ++++ features/video-recorder/popup.html | 50 ++++++ features/video-recorder/style.css | 22 +++ features/video-recorder/video-recorder.js | 180 ++++++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 features/video-recorder/data.json create mode 100644 features/video-recorder/popup.html create mode 100644 features/video-recorder/style.css create mode 100644 features/video-recorder/video-recorder.js diff --git a/features/video-recorder/data.json b/features/video-recorder/data.json new file mode 100644 index 00000000..42d327f1 --- /dev/null +++ b/features/video-recorder/data.json @@ -0,0 +1,34 @@ +{ + "title": "Record Stage", + "description": "Allows you to record the stage for projects while in the editor or on the project page.", + "credits": [ + { + "username": "blob2763", + "url": "https://blob2763.is-a.dev/" + }, + { + "username": "stio_studio", + "url": "https://stio.studio/" + } + ], + "type": ["Editor"], + "tags": ["New", "Featured"], + "scripts": [ + { + "file": "video-recorder.js", + "runOn": "/projects/*" + } + ], + "styles": [ + { + "file": "style.css", + "runOn": "/projects/*" + } + ], + "resources": [ + { + "name": "popup-html", + "path": "/popup.html" + } + ] +} diff --git a/features/video-recorder/popup.html b/features/video-recorder/popup.html new file mode 100644 index 00000000..ef9c057c --- /dev/null +++ b/features/video-recorder/popup.html @@ -0,0 +1,50 @@ +
+ +
\ No newline at end of file diff --git a/features/video-recorder/style.css b/features/video-recorder/style.css new file mode 100644 index 00000000..70fa6eac --- /dev/null +++ b/features/video-recorder/style.css @@ -0,0 +1,22 @@ +.STE-ReactModalPortal .STE-recorded-video { + width: 100%; + height: 100%; + border: 10px solid #ccc; + border-radius: 10px; +} + +.STE-ReactModalPortal .STE-hide-button { + display: none; +} + +.STE-ReactModalPortal .STE-left-text { + text-align: left; +} + +.STE-ReactModalPortal .stopButton, +.STE-ReactModalPortal .startButton, +.STE-ReactModalPortal .downloadButton, +.STE-ReactModalPortal .video-format-select { + width: 100%; +} + diff --git a/features/video-recorder/video-recorder.js b/features/video-recorder/video-recorder.js new file mode 100644 index 00000000..ccba54cd --- /dev/null +++ b/features/video-recorder/video-recorder.js @@ -0,0 +1,180 @@ +export default async function ({ feature, console }) { + await new Promise(async (resolve, reject) => { + (async () => { + const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons") + resolve(rem); + })(); + (async () => { + const rem = await ScratchTools.waitForElement(".menu-bar_account-info-group_MeJZP") + resolve(rem); + })(); + }) + + let openPopup = document.createElement("button"); + + ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) { + if (row.querySelector(".ste-video-recorder-open")) return; + openPopup = document.createElement("button"); + openPopup.className = "button action-button ste-video-recorder-open"; + openPopup.textContent = "Record Video"; + row.insertAdjacentElement("afterbegin", openPopup); + openPopup.addEventListener('click', () => { + document.body.append(popup) + }) + }) + + ScratchTools.waitForElements(".menu-bar_account-info-group_MeJZP", async function (row) { + if (row.querySelector(".ste-video-recorder-open")) return; + openPopup = document.createElement("div"); + openPopup.className = "menu-bar_menu-bar-item_oLDa- menu-bar_hoverable_c6WFB"; + openPopup.style.padding = "0 0.75rem" + let rem = document.createElement("div"); + rem.textContent = "Record Video"; + openPopup.append(rem); + row.insertAdjacentElement("afterbegin", openPopup); + openPopup.addEventListener('click', () => { + document.body.append(popup) + }) + }) + + let popup = document.createElement("div"); + popup.insertAdjacentHTML("afterbegin", await (await fetch(feature.self.getResource("popup-html"))).text()) + popup = popup.querySelector("div.ReactModalPortal") + + let stopButton = popup.querySelector(".stopButton"); + let startButton = popup.querySelector(".startButton"); + let closeButton = popup.querySelector(".close-button_close-button_lOp2G"); + let downloadButton = popup.querySelector(".downloadButton"); + let lastDownloadFunction = () => { } + let mimeType = popup.querySelector("select"); + let microphoneCheckbox = popup.querySelector(".microphoneCheckbox"); + let desktopSoundCheckbox = popup.querySelector(".desktopSoundCheckbox"); + + closeButton.addEventListener('click', () => { + document.querySelector(".STE-ReactModalPortal").remove() + }) + addEventListener("keydown", (e) => { + if (e.key === "Escape") { + document.querySelector(".STE-ReactModalPortal").remove() + } + }) + + const canvas = feature.traps.vm.renderer.canvas; + const preview = popup.querySelector("video") + + await new Promise(async (resolve, reject) => { + (async () => { + const rem = await ScratchTools.waitForElement("input.inplace-input") + resolve(rem); + })(); + (async () => { + const rem = await ScratchTools.waitForElement("input.project-title-input_title-field_en5Gd") + resolve(rem); + })(); + (async () => { + const rem = await ScratchTools.waitForElement(".project-title") + resolve(rem); + })(); + }) + + let projectTitle = document.querySelector("input.inplace-input") || document.querySelector("input.project-title-input_title-field_en5Gd") || document.querySelector(".project-title"); + + ScratchTools.waitForElements("input.inplace-input", async function (_projectTitle) { + projectTitle = _projectTitle + }) + + ScratchTools.waitForElements("input.project-title-input_title-field_en5Gd", async function (_projectTitle) { + projectTitle = _projectTitle + }) + + ScratchTools.waitForElements(".project-title", async function (_projectTitle) { + projectTitle = _projectTitle + }) + + + let mediaRecorder; + let recordedChunks = []; + + startButton.addEventListener('click', async () => { + startButton.classList.add("STE-hide-button"); + stopButton.classList.remove("STE-hide-button"); + + // Capture the canvas element as a stream + const canvasStream = canvas.captureStream(30); // 30 FPS + + // Get the audio context from the Scratch VM + const audioContext = feature.traps.vm.runtime.audioEngine.audioContext; + const audioDestination = audioContext.createMediaStreamDestination(); + + if (microphoneCheckbox.checked) { + // Capture the microphone audio + let micStream; + try { + micStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + } catch (err) { + console.error("Error capturing microphone audio:", err); + } + + if (micStream) { + const micSource = audioContext.createMediaStreamSource(micStream); + micSource.connect(audioDestination); + } + } + + // Connect the audio engine's output + if (desktopSoundCheckbox.checked) { + feature.traps.vm.runtime.audioEngine.inputNode.connect(audioDestination); + } + + // Combine the canvas video track and audio tracks + const combinedStream = new MediaStream(); + canvasStream.getVideoTracks().forEach(track => combinedStream.addTrack(track)); + if (microphoneCheckbox.checked || desktopSoundCheckbox.checked) { + audioDestination.stream.getAudioTracks().forEach(track => combinedStream.addTrack(track)); + } + + mediaRecorder = new MediaRecorder(combinedStream); + + mediaRecorder.ondataavailable = function (event) { + if (event.data.size > 0) { + recordedChunks.push(event.data); + } + }; + + mediaRecorder.onstop = function () { + const blob = new Blob(recordedChunks, { + type: `video/${mimeType.value}` + }); + preview.src = URL.createObjectURL(blob); + preview.controls = true; + // console.log(projectTitle) + preview.download = `${projectTitle.value}.${mimeType.value}`; + downloadButton.removeEventListener("click", lastDownloadFunction); + lastDownloadFunction = async () => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${projectTitle.value}.${mimeType.value}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + downloadButton.addEventListener("click", lastDownloadFunction); + recordedChunks = []; + }; + + mediaRecorder.start(); + startButton.disabled = true; + stopButton.disabled = false; + }); + + stopButton.addEventListener('click', () => { + mediaRecorder.stop(); + startButton.disabled = false; + stopButton.disabled = true; + + stopButton.classList.add("STE-hide-button"); + startButton.classList.remove("STE-hide-button"); + }); +} From 48191f29d3779e3d938525fda10f34f1ee05292a Mon Sep 17 00:00:00 2001 From: stio Date: Thu, 1 Aug 2024 18:39:41 +0200 Subject: [PATCH 6/6] Using the Document Picture-in-Picture API (optional) --- features/picture-in-picture/data.json | 23 ++-- .../picture-in-picture/picture-in-picture.js | 119 +++++++++++++++--- features/picture-in-picture/popup.html | 40 ++++++ 3 files changed, 160 insertions(+), 22 deletions(-) create mode 100644 features/picture-in-picture/popup.html diff --git a/features/picture-in-picture/data.json b/features/picture-in-picture/data.json index 4f16fec3..9da27acc 100644 --- a/features/picture-in-picture/data.json +++ b/features/picture-in-picture/data.json @@ -7,8 +7,13 @@ "url": "https://stio.studio/" } ], - "type": ["Website"], - "tags": ["New", "Featured"], + "type": [ + "Website" + ], + "tags": [ + "New", + "Featured" + ], "dynamic": true, "scripts": [ { @@ -16,8 +21,12 @@ "runOn": "/projects/*" } ], - "components": [{ - "type": "info", - "content": "Picture in Picture will not allow you to interact with the project. You must be on the project page to interact with it." - }] -} + "resources": [{ "name": "popup-html", "path": "/popup.html" }], + "options": [ + { + "id": "interactivity-PiP", + "name": "Make the project in picture popup interactive. (Experimental)", + "type": 1 + } + ] +} \ No newline at end of file diff --git a/features/picture-in-picture/picture-in-picture.js b/features/picture-in-picture/picture-in-picture.js index e17c88f7..e1c45e0c 100644 --- a/features/picture-in-picture/picture-in-picture.js +++ b/features/picture-in-picture/picture-in-picture.js @@ -1,14 +1,4 @@ export default async function ({ feature, console }) { - const canvas = feature.traps.vm.renderer.canvas; - - let video = document.createElement("video"); - // video.setAttribute("controls", "controls"); - video.setAttribute("autoplay", "autoplay"); - video.setAttribute("style", "width: 100%; height: 100%"); - // document.querySelector(".preview .inner").append(video); - - video.srcObject = canvas.captureStream(30) - await new Promise(async (resolve, reject) => { (async () => { const rem = await ScratchTools.waitForElement(".preview .inner .flex-row.action-buttons") @@ -20,6 +10,7 @@ export default async function ({ feature, console }) { })(); }) + const canvas = feature.traps.vm.renderer.canvas; let openPopup = document.createElement("button"); ScratchTools.waitForElements(".preview .inner .flex-row.action-buttons", async function (row) { @@ -45,12 +36,110 @@ export default async function ({ feature, console }) { }) }) - function popup() { - try { - video.requestPictureInPicture() + let popup; + + if (feature.settings.get("interactivity-PiP")) { + if (!"documentPictureInPicture" in window) console.error("Picture in Picture not supported") + + let pipWindow + + let docPopup = document.createElement("div"); + docPopup.insertAdjacentHTML("afterbegin", await (await fetch(feature.self.getResource("popup-html"))).text()) + docPopup = docPopup.querySelector("div.popup-GUI") + + let video = docPopup.querySelector("video"); + + const greenFlag = document.querySelector(".green-flag_green-flag_1kiAo") + docPopup.querySelector(".popup-greenflag").addEventListener("click", () => { + greenFlag.click() + }); + const redFlag = document.querySelector(".stop-all_stop-all_1Y8P9") + docPopup.querySelector(".popup-redflag").addEventListener("click", () => { + redFlag.click() + }); + + // video.addEventListener("mousedown", (old_event) => { + function translateEvent_pointer(old_event) { + // Calculate the canvas position relative to the viewport + const a_rect = canvas.getBoundingClientRect(); + const b_rect = video.getBoundingClientRect(); + + // console.log(old_event) + // Create a new event with the adjusted coordinates + + let new_event = new old_event.constructor(old_event.type, { + bubbles: old_event.bubbles, + cancelable: old_event.cancelable, + clientX: (old_event.clientX - b_rect.left) * (a_rect.width / b_rect.width) + a_rect.left, + clientY: (old_event.clientY - b_rect.top) * (a_rect.height / b_rect.height) + a_rect.top, + // Copy over other necessary properties from the old event + screenX: (old_event.screenX - pipWindow.screenLeft + window.screenLeft - b_rect.left) * (a_rect.width / b_rect.width) + a_rect.left, + screenY: (old_event.screenY - pipWindow.screenTop + window.screenTop - b_rect.top) * (a_rect.height / b_rect.height) + a_rect.top, + layerX: old_event.layerX, + layerY: old_event.layerY, + button: old_event.button, + buttons: old_event.buttons, + relatedTarget: old_event.relatedTarget, + altKey: old_event.altKey, + ctrlKey: old_event.ctrlKey, + shiftKey: old_event.shiftKey, + metaKey: old_event.metaKey, + movementX: old_event.movementX, + movementY: old_event.movementY, + }); + + // Dispatch the new event + canvas.dispatchEvent(new_event); + } + video.addEventListener("mousedown", translateEvent_pointer) + video.addEventListener("mouseup", translateEvent_pointer) + video.addEventListener("mousemove", translateEvent_pointer) + video.addEventListener("wheel", translateEvent_pointer) + video.addEventListener("touchstart", translateEvent_pointer) + video.addEventListener("touchend", translateEvent_pointer) + video.addEventListener("touchmove", translateEvent_pointer) + + function translateEvent_key(old_event) { + let new_event = new KeyboardEvent(old_event.type, old_event) + document.dispatchEvent(new_event); } - catch { - console.log("Picture in Picture not supported or failed to request") + + let buttonClickedTimes = 0 + popup = async function () { + if (buttonClickedTimes === 0) { + video.srcObject = canvas.captureStream() + buttonClickedTimes++ + } + // Open a Picture-in-Picture window. + pipWindow = await window.documentPictureInPicture.requestWindow({ + width: canvas.width, + height: canvas.height + 20 + 6 * 2, + }); + + // Move the player to the Picture-in-Picture window. + pipWindow.document.body.append(docPopup); + + pipWindow.document.addEventListener("keydown", translateEvent_key) + pipWindow.document.addEventListener("keypress", translateEvent_key) + pipWindow.document.addEventListener("keyup", translateEvent_key) + } + } + else { + let video = document.createElement("video"); + // video.setAttribute("controls", "controls"); + video.setAttribute("autoplay", "autoplay"); + video.setAttribute("style", "width: 100%; height: 100%"); + // document.querySelector(".preview .inner").append(video); + + video.srcObject = canvas.captureStream() + + popup = function () { + try { + video.requestPictureInPicture() + } + catch { + console.log("Picture in Picture not supported or failed to request") + } } } } diff --git a/features/picture-in-picture/popup.html b/features/picture-in-picture/popup.html new file mode 100644 index 00000000..1cee1d36 --- /dev/null +++ b/features/picture-in-picture/popup.html @@ -0,0 +1,40 @@ + \ No newline at end of file