Files
drags-and-nerds/v1-com-officielle/public/canvasVideoExport.js
T
vgaNAR6ta 13c91b464f add: PASSAGE V1 COM OFFICIELLE
préparation du template
intégration du fond animé
fusion du style background et style pcp
2026-03-18 20:15:01 +01:00

265 lines
7.0 KiB
JavaScript

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);
}