Skip to content

Commit e72e8f6

Browse files
authored
Fix Record canvas only, not entire screen (#4873)
* Fix Record canvas only, not entire screen * Fix message close buttons and add recording dropdown * Fix message close buttons and add recording dropdown * Fix minor typos in code * Activity.js Conflict resolved * Make Canvas only consistent with Record with menus * Fix: hide record menu in beginner mode, unify alerts, remove unrelated CSS * Add record button dropdown with canvas/screen options and fix start/stop toggle * Fix Jest tests * Fix record button visibility in beginner mode * Fix recording errors: undefined activity and null mediaRecorder * Add visual highlighting to recording mode dropdown * Prevent doRecordButton execution without valid activity context * Add error handling for recording permission and capture failures * fix: prevent null error on mediaRecorder.stop and crop canvas-only recording to remove toolbar black bar * Fix: Remove black bar from canvas-only video recordings * Fix: Resolve ESLint and Prettier formatting issues * reverted unrelated changes * removed noise from dist/css/style * removed unrelated changes from index.html js/activity.js * fix: prevent double screen recording permission dialog by ensuring single event handler on record button
1 parent b7e73c2 commit e72e8f6

File tree

4 files changed

+354
-46
lines changed

4 files changed

+354
-46
lines changed

css/style.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,3 @@ input[type="range"]:focus::-ms-fill-upper {
114114
.lego-size-1 { width: 20px; height: 10px; }
115115
.lego-size-2 { width: 40px; height: 10px; }
116116
/* ... more sizes ... */
117-

index.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,9 @@
335335
<li>
336336
<a id="stop" class="left tooltipped"><i class="material-icons main">stop</i></a>
337337
</li>
338-
<li>
339-
<a id="record" class="left tooltipped" data-tooltip="Record"></a>
338+
<li style="display: flex; align-items: center;">
339+
<a id="record" class="left tooltipped" data-position="bottom" data-tooltip="Record"></a>
340+
<a id="recordDropdownArrow" class="left dropdown-trigger" data-activates="recorddropdown" style="margin-left: -5px; padding: 0; font-size: 28px;"></a>
340341
</li>
341342
</ul>
342343

@@ -502,6 +503,11 @@
502503
<li><a id="save-blockartwork-png"></a></li>
503504
</ul>
504505

506+
<ul id="recorddropdown" class="dropdown-content">
507+
<li><a id="record-with-menus">Record canvas and toolbars</a></li>
508+
<li><a id="record-canvas-only">Record canvas only</a></li>
509+
</ul>
510+
505511
<ul id="languagedropdown" class="dropdown-content">
506512
<li><a id="enUS"></a></li>
507513
<li><a id="enUK"></a></li>

js/activity.js

Lines changed: 210 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,12 @@ class Activity {
17091709
return; // Exit the function if execution is already in progress
17101710
}
17111711

1712+
if (!activity || typeof activity._doRecordButton !== "function") {
1713+
console.warn("doRecordButton called without valid activity context");
1714+
isExecuting = false;
1715+
return;
1716+
}
1717+
17121718
isExecuting = true; // Set the flag to indicate execution has started
17131719
activity._doRecordButton();
17141720
};
@@ -1718,34 +1724,136 @@ class Activity {
17181724
* @private
17191725
*/
17201726
this._doRecordButton = () => {
1727+
const that = this;
17211728
const start = document.getElementById("record"),
17221729
recInside = document.getElementById("rec_inside");
17231730
let mediaRecorder;
17241731
var clickEvent = new Event("click");
17251732
let flag = 0;
1733+
let currentStream = null;
1734+
let audioDestination = null;
17261735

17271736
/**
17281737
* Records the screen using the browser's media devices API.
17291738
* @returns {Promise<MediaStream>} A promise resolving to the recorded media stream.
17301739
*/
1740+
17311741
async function recordScreen() {
1742+
const mode = localStorage.getItem("musicBlocksRecordMode");
1743+
1744+
if (mode === "canvas") {
1745+
return await recordCanvasOnly();
1746+
} else {
1747+
return await recordScreenWithTools();
1748+
}
1749+
}
1750+
1751+
async function recordCanvasOnly() {
17321752
flag = 1;
1733-
return await navigator.mediaDevices.getDisplayMedia({
1734-
preferCurrentTab: "True",
1735-
systemAudio: "include",
1736-
audio: "True",
1737-
video: { mediaSource: "tab" },
1738-
bandwidthProfile: {
1739-
video: {
1740-
clientTrackSwitchOffControl: "auto",
1741-
contentPreferencesMode: "auto"
1742-
}
1743-
},
1744-
preferredVideoCodecs: "auto"
1753+
const canvas = document.getElementById("myCanvas");
1754+
if (!canvas) {
1755+
throw new Error("Canvas element not found");
1756+
}
1757+
1758+
// Get the toolbar height to exclude from recording
1759+
const toolbar = document.getElementById("toolbars");
1760+
const toolbarHeight = toolbar ? toolbar.offsetHeight : 0;
1761+
1762+
// Get canvas dimensions
1763+
const canvasRect = canvas.getBoundingClientRect();
1764+
1765+
// Get the actual canvas dimensions
1766+
const canvasWidth = canvas.width;
1767+
const canvasHeight = canvas.height;
1768+
1769+
// Calculate the visible area (excluding toolbar)
1770+
const visibleHeight = canvasHeight - toolbarHeight;
1771+
1772+
// Create a clean recording canvas
1773+
const recordCanvas = document.createElement("canvas");
1774+
recordCanvas.width = canvasWidth;
1775+
recordCanvas.height = canvasHeight;
1776+
const recordCtx = recordCanvas.getContext("2d");
1777+
1778+
// Set background to match the canvas (white/light gray)
1779+
recordCtx.fillStyle = "#f5f5f5"; // Adjust this color to match your canvas background
1780+
let animationFrameId;
1781+
1782+
// Function to continuously copy canvas content
1783+
const copyFrame = () => {
1784+
// Fill background
1785+
recordCtx.fillRect(0, 0, canvasWidth, canvasHeight);
1786+
1787+
// Draw only the visible portion of the canvas (skip the toolbar area)
1788+
recordCtx.drawImage(
1789+
canvas,
1790+
0,
1791+
toolbarHeight, // Source x, y (skip toolbar)
1792+
canvasWidth,
1793+
visibleHeight, // Source width, height
1794+
0,
1795+
0, // Destination x, y
1796+
canvasWidth,
1797+
visibleHeight // Destination width, height
1798+
);
1799+
1800+
// Continue if still recording
1801+
if (flag === 1) {
1802+
animationFrameId = requestAnimationFrame(copyFrame);
1803+
}
1804+
};
1805+
1806+
// Start copying frames
1807+
copyFrame();
1808+
1809+
// Capture the canvas stream directly at 30fps
1810+
const canvasStream = recordCanvas.captureStream(30);
1811+
1812+
// Add audio track if available
1813+
const Tone = that.logo.synth.tone;
1814+
if (Tone && Tone.context) {
1815+
const dest = Tone.context.createMediaStreamDestination();
1816+
Tone.Destination.connect(dest);
1817+
audioDestination = dest;
1818+
const audioTrack = dest.stream.getAudioTracks()[0];
1819+
if (audioTrack) {
1820+
canvasStream.addTrack(audioTrack);
1821+
}
1822+
}
1823+
currentStream = canvasStream;
1824+
1825+
// Clean up animation frame when recording stops
1826+
canvasStream.getTracks()[0].addEventListener("ended", () => {
1827+
if (animationFrameId) {
1828+
cancelAnimationFrame(animationFrameId);
1829+
}
17451830
});
1831+
1832+
return canvasStream;
17461833
}
1834+
async function recordScreenWithTools() {
1835+
flag = 1;
17471836

1748-
const that = this;
1837+
try {
1838+
return await navigator.mediaDevices.getDisplayMedia({
1839+
preferCurrentTab: "True",
1840+
systemAudio: "include",
1841+
audio: "True",
1842+
video: { mediaSource: "tab" },
1843+
bandwidthProfile: {
1844+
video: {
1845+
clientTrackSwitchOffControl: "auto",
1846+
contentPreferencesMode: "auto"
1847+
}
1848+
},
1849+
preferredVideoCodecs: "auto"
1850+
});
1851+
} catch (error) {
1852+
console.error("Screen capture failed:", error);
1853+
flag = 0;
1854+
throw error;
1855+
}
1856+
}
17491857

17501858
/**
17511859
* Saves the recorded chunks as a video file.
@@ -1754,10 +1862,35 @@ class Activity {
17541862
function saveFile(recordedChunks) {
17551863
flag = 1;
17561864
recInside.classList.remove("blink");
1865+
// Prevent zero-byte files
1866+
if (!recordedChunks || recordedChunks.length === 0) {
1867+
alert(_("Recorded file is empty. File not saved."));
1868+
flag = 0;
1869+
recording();
1870+
doRecordButton();
1871+
return;
1872+
}
17571873
const blob = new Blob(recordedChunks, {
17581874
type: "video/webm"
17591875
});
1760-
1876+
if (blob.size === 0) {
1877+
alert(_("Recorded file is empty. File not saved."));
1878+
flag = 0;
1879+
recording();
1880+
doRecordButton();
1881+
return;
1882+
}
1883+
// Clean up stream after recording
1884+
if (currentStream) {
1885+
currentStream.getTracks().forEach(track => track.stop());
1886+
currentStream = null;
1887+
}
1888+
if (audioDestination && audioDestination.stream) {
1889+
audioDestination.stream.getTracks().forEach(track => track.stop());
1890+
audioDestination = null;
1891+
}
1892+
mediaRecorder = null;
1893+
// Prompt to save file
17611894
const filename = window.prompt(_("Enter file name"));
17621895
if (filename === null || filename.trim() === "") {
17631896
alert(_("File save canceled"));
@@ -1766,27 +1899,33 @@ class Activity {
17661899
doRecordButton();
17671900
return; // Exit without saving the file
17681901
}
1769-
17701902
const downloadLink = document.createElement("a");
17711903
downloadLink.href = URL.createObjectURL(blob);
17721904
downloadLink.download = `${filename}.webm`;
1773-
17741905
document.body.appendChild(downloadLink);
17751906
downloadLink.click();
17761907
URL.revokeObjectURL(blob);
17771908
document.body.removeChild(downloadLink);
17781909
flag = 0;
1779-
// eslint-disable-next-line no-use-before-define
1910+
// Allow multiple recordings
17801911
recording();
17811912
doRecordButton();
1782-
that.textMsg(_("Click on stop saving"));
1913+
that.textMsg(_("Recording stopped. File saved."));
17831914
}
17841915
/**
17851916
* Stops the recording process.
17861917
*/
17871918
function stopRec() {
17881919
flag = 0;
1789-
mediaRecorder.stop();
1920+
1921+
if (mediaRecorder && typeof mediaRecorder.stop === "function") {
1922+
mediaRecorder.stop();
1923+
}
1924+
1925+
// Clean up the recording canvas stream
1926+
if (currentStream) {
1927+
currentStream.getTracks().forEach(track => track.stop());
1928+
}
17901929
const node = document.createElement("p");
17911930
node.textContent = "Stopped recording";
17921931
document.body.appendChild(node);
@@ -1801,9 +1940,10 @@ class Activity {
18011940
function createRecorder(stream, mimeType) {
18021941
flag = 1;
18031942
recInside.classList.add("blink");
1943+
that.textMsg(_("Recording started. Click stop to finish."));
18041944
start.removeEventListener("click", createRecorder, true);
18051945
let recordedChunks = [];
1806-
const mediaRecorder = new MediaRecorder(stream);
1946+
mediaRecorder = new MediaRecorder(stream);
18071947
stream.oninactive = function () {
18081948
// eslint-disable-next-line no-console
18091949
console.log("Recording is ready to save");
@@ -1837,31 +1977,57 @@ class Activity {
18371977
* Handles the recording process.
18381978
*/
18391979
function recording() {
1840-
start.addEventListener("click", async function handler() {
1841-
const stream = await recordScreen();
1842-
const mimeType = "video/webm";
1843-
mediaRecorder = createRecorder(stream, mimeType);
1844-
if (flag == 1) {
1845-
this.removeEventListener("click", handler);
1980+
// Remove any previous handler to avoid multiple triggers
1981+
if (start._recordHandler) {
1982+
start.removeEventListener("click", start._recordHandler);
1983+
}
1984+
const handler = async function handler() {
1985+
try {
1986+
const stream = await recordScreen();
1987+
const mimeType = "video/webm";
1988+
mediaRecorder = createRecorder(stream, mimeType);
1989+
if (flag == 1) {
1990+
start.removeEventListener("click", handler);
1991+
// Add stop handler
1992+
const stopHandler = function stopHandler() {
1993+
if (mediaRecorder && mediaRecorder.state === "recording") {
1994+
mediaRecorder.stop();
1995+
mediaRecorder = new MediaRecorder(stream);
1996+
recInside.classList.remove("blink");
1997+
flag = 0;
1998+
// Clean up stream
1999+
if (currentStream) {
2000+
currentStream.getTracks().forEach(track => track.stop());
2001+
}
2002+
if (audioDestination && audioDestination.stream) {
2003+
audioDestination.stream
2004+
.getTracks()
2005+
.forEach(track => track.stop());
2006+
}
2007+
}
2008+
start.removeEventListener("click", stopHandler);
2009+
// Re-enable recording for next time
2010+
recording();
2011+
};
2012+
start.addEventListener("click", stopHandler);
2013+
}
2014+
recInside.setAttribute("fill", "red");
2015+
} catch (error) {
2016+
console.error("Recording failed:", error);
2017+
that.textMsg(_("Recording failed: ") + error.message);
2018+
flag = 0;
2019+
// Re-enable recording button
2020+
recording();
18462021
}
1847-
const node = document.createElement("p");
1848-
node.textContent = "Started recording";
1849-
document.body.appendChild(node);
1850-
recInside.setAttribute("fill", "red");
1851-
});
2022+
};
2023+
start.addEventListener("click", handler);
2024+
start._recordHandler = handler;
18522025
}
18532026

18542027
// Start recording process if not already executing
18552028
if (flag == 0 && isExecuting) {
18562029
recording();
18572030
start.dispatchEvent(clickEvent);
1858-
flag = 1;
1859-
}
1860-
1861-
// Stop recording if already executing
1862-
if (flag == 1 && isExecuting) {
1863-
start.addEventListener("click", stopRec);
1864-
flag = 0;
18652031
}
18662032
};
18672033

@@ -2000,6 +2166,11 @@ class Activity {
20002166
activity.regeneratePalettes();
20012167
}
20022168

2169+
// Update record button and dropdown visibility
2170+
if (activity.toolbar && typeof activity.toolbar.updateRecordButton === "function") {
2171+
activity.toolbar.updateRecordButton(() => doRecordButton(activity));
2172+
}
2173+
20032174
// Force immediate canvas refresh
20042175
activity.refreshCanvas();
20052176
};
@@ -7265,7 +7436,7 @@ class Activity {
72657436
this.toolbar.renderHelpIcon(showHelp);
72667437
this.toolbar.renderModeSelectIcon(
72677438
doSwitchMode,
7268-
doRecordButton,
7439+
() => doRecordButton(this),
72697440
doAnalytics,
72707441
doOpenPlugin,
72717442
deletePlugin,

0 commit comments

Comments
 (0)