@@ -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