let projectName = "komorebi"; //to be updated //detect user browser var ua = navigator.userAgent; var isSafari = false; var isFirefox = false; var isIOS = false; var isAndroid = false; if(ua.includes("Safari")){ isSafari = true; } if(ua.includes("Firefox")){ isFirefox = true; } if(ua.includes("iPhone") || ua.includes("iPad") || ua.includes("iPod")){ isIOS = true; } if(ua.includes("Android")){ isAndroid = true; } console.log("isSafari: "+isSafari+", isFirefox: "+isFirefox+", isIOS: "+isIOS+", isAndroid: "+isAndroid); let useMobileRecord = false; if(isIOS || isAndroid || isFirefox){ useMobileRecord = true; } var mediaRecorder; var recordedChunks; var finishedBlob; var recordingMessageDiv = document.getElementById("videoRecordingMessageDiv"); var recordVideoState = false; var videoRecordInterval; var videoEncoder; var muxer; var mobileRecorder; var videofps = 30; let bitrate = 16_000_000; function saveImage() { console.log("Export png image"); // Create a temporary canvas with the same dimensions const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempContext = tempCanvas.getContext('2d', { willReadFrequently: true, alpha: true // Enable alpha for transparency }); // Skip filling the background, leaving it transparent // Force a render frame to ensure latest content drawScene(); gl.flush(); gl.finish(); // Draw the WebGL canvas onto the temporary canvas tempContext.drawImage(canvas, 0, 0); // Create download link const link = document.createElement('a'); link.href = tempCanvas.toDataURL('image/png'); const date = new Date(); const filename = projectName + `_${date.toLocaleDateString()}_${date.toLocaleTimeString()}.png`; link.download = filename; link.click(); // Cleanup tempCanvas.remove(); } function toggleVideoRecord(){ if(recordVideoState == false){ recordVideoState = true; chooseRecordingFunction(); } else { recordVideoState = false; chooseEndRecordingFunction(); } } function chooseRecordingFunction(){ //resetAnimation(); if(useMobileRecord){ startMobileRecording(); }else { recordVideoMuxer(); } } function chooseEndRecordingFunction(){ if(useMobileRecord){ mobileRecorder.stop(); }else { finalizeVideo(); } } //record html canvas element and export as mp4 video //source: https://devtails.xyz/adam/how-to-save-html-canvas-to-mp4-using-web-codecs-api async function recordVideoMuxer() { console.log("start muxer video recording"); var videoWidth = Math.floor(canvas.width/2)*2; var videoHeight = Math.floor(canvas.height/4)*4; //force a number which is divisible by 4 console.log("Video dimensions: "+videoWidth+", "+videoHeight); //display user message recordingMessageDiv.classList.remove("hidden"); recordVideoState = true; const ctx = canvas.getContext("2d", { // This forces the use of a software (instead of hardware accelerated) 2D canvas // This isn't necessary, but produces quicker results willReadFrequently: true, // Desynchronizes the canvas paint cycle from the event loop // Should be less necessary with OffscreenCanvas, but with a real canvas you will want this desynchronized: true, }); muxer = new Mp4Muxer.Muxer({ target: new Mp4Muxer.ArrayBufferTarget(), video: { // If you change this, make sure to change the VideoEncoder codec as well codec: "avc", width: videoWidth, height: videoHeight, }, firstTimestampBehavior: 'offset', // mp4-muxer docs claim you should always use this with ArrayBufferTarget fastStart: "in-memory", }); videoEncoder = new VideoEncoder({ output: (chunk, meta) => muxer.addVideoChunk(chunk, meta), error: (e) => console.error(e), }); // This codec should work in most browsers // See https://dmnsgn.github.io/media-codecs for list of codecs and see if your browser supports videoEncoder.configure({ codec: "avc1.4d0032", width: videoWidth, height: videoHeight, bitrate: bitrate, bitrateMode: "variable", }); //NEW codec: "avc1.4d0032", //ORIGINAL codec: "avc1.42003e", var frameNumber = 0; //take a snapshot of the canvas every x miliseconds and encode to video videoRecordInterval = setInterval( function(){ if(recordVideoState == true){ //gl.flush(); //gl.finish(); drawScene(); renderCanvasToVideoFrameAndEncode({ canvas, videoEncoder, frameNumber, videofps }) frameNumber++; }else{ } } , 1000/videofps); } //finish and export video async function finalizeVideo(){ console.log("finalize muxer video"); togglePlayPause(); clearInterval(videoRecordInterval); //playAnimationToggle = false; recordVideoState = false; // Forces all pending encodes to complete await videoEncoder.flush(); muxer.finalize(); let buffer = muxer.target.buffer; finishedBlob = new Blob([buffer]); downloadBlob(new Blob([buffer])); //hide user message recordingMessageDiv.classList.add("hidden"); togglePlayPause(); } async function renderCanvasToVideoFrameAndEncode({ canvas, videoEncoder, frameNumber, videofps, }) { let frame = new VideoFrame(canvas, { // Equally spaces frames out depending on frames per second timestamp: (frameNumber * 1e6) / videofps, }); // The encode() method of the VideoEncoder interface asynchronously encodes a VideoFrame videoEncoder.encode(frame); // The close() method of the VideoFrame interface clears all states and releases the reference to the media resource. frame.close(); } function downloadBlob() { console.log("download video"); let url = window.URL.createObjectURL(finishedBlob); let a = document.createElement("a"); a.style.display = "none"; a.href = url; const date = new Date(); const filename = projectName+`_${date.toLocaleDateString()}_${date.toLocaleTimeString()}.mp4`; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); } //record and download videos on mobile devices function startMobileRecording(){ var stream = canvas.captureStream(videofps); mobileRecorder = new MediaRecorder(stream, { 'type': 'video/mp4' }); mobileRecorder.addEventListener('dataavailable', finalizeMobileVideo); console.log("start simple video recording"); console.log("Video dimensions: "+canvas.width+", "+canvas.height); recordingMessageDiv.classList.remove("hidden"); recordVideoState = true; mobileRecorder.start(); //start mobile video recording } function finalizeMobileVideo(e) { setTimeout(function(){ console.log("finish simple video recording"); togglePlayPause(); recordVideoState = false; /* mobileRecorder.stop();*/ var videoData = [ e.data ]; finishedBlob = new Blob(videoData, { 'type': 'video/mp4' }); downloadBlob(finishedBlob); //hide user message recordingMessageDiv.classList.add("hidden"); togglePlayPause(); },500); }