Skip to content

Commit

Permalink
Add video record function
Browse files Browse the repository at this point in the history
  • Loading branch information
collidingScopes authored Aug 8, 2024
1 parent b68d4ab commit c68e26c
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 4 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@

<script src='perlin.js'></script>
<script src="liquify.js"></script>
<script src="mp4-muxer-main/build/mp4-muxer.js"></script>


</html>
207 changes: 203 additions & 4 deletions liquify.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ var recordBtn = document.getElementById("recordVideoButton");
recordBtn.addEventListener('click', chooseRecordingFunction);
*/

var recording = false;
var mediaRecorder;
var recordedChunks;
var finishedBlob;
Expand Down Expand Up @@ -471,10 +470,15 @@ function startGenerativeDraw(){
*/

if(playAnimationToggle==true){
/*
setTimeout(function(){
liquify(x,y);
animationRequest = requestAnimationFrame(loop);
},MOUSE_UPDATE_DELAY);
*/
liquify(x,y);
animationRequest = requestAnimationFrame(loop);

}

}
Expand Down Expand Up @@ -634,6 +638,10 @@ document.addEventListener('keydown', function(event) {
toggleGUI();
} else if(event.key === 'p'){
pausePlayAnimation();
} else if(event.key === 'v'){
chooseRecordingFunction();
} else if(event.key === 'b'){
chooseEndRecordingFunction();
}
});

Expand Down Expand Up @@ -739,7 +747,7 @@ function pausePlayAnimation(){
//SOURCE: https://github.com/joeiddon/perlin

var perlinDataArray;
const GRID_SIZE = 4;
const GRID_SIZE = 3;
const RESOLUTION = 128;
const COLOR_SCALE = 250;
var numPerlinRows = GRID_SIZE*RESOLUTION;
Expand Down Expand Up @@ -772,11 +780,202 @@ function generatePerlinData(){
}
}

console.log(perlinDataArray);
}


function toggleVideoRecord(){
if(recordVideoState == false){
recordVideoState = true;
chooseRecordingFunction();
} else {
recordVideoState = false;
chooseEndRecordingFunction();
}
}

function chooseRecordingFunction(){
if(isIOS || isAndroid || isFirefox){
startMobileRecording();
}else {
recordVideoMuxer();
}
}

function chooseEndRecordingFunction(){
if(isIOS || isAndroid || isFirefox){
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/8)*8; //force a number which is divisible by 8
console.log("Video dimensions: "+videoWidth+", "+videoHeight);

//display user message
//recordingMessageCountdown(videoDuration);
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(),
//let muxer = new Muxer({
//target: new 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.42003e",
width: videoWidth,
height: videoHeight,
bitrate: 4_000_000,
bitrateMode: "constant",
});
//NEW codec: "avc1.42003e",
//ORIGINAL codec: "avc1.42001f",

var frameNumber = 0;
//setTimeout(finalizeVideo,1000*videoDuration+200); //finish and export video after x seconds

//take a snapshot of the canvas every x miliseconds and encode to video
videoRecordInterval = setInterval(
function(){
if(recordVideoState == true){
renderCanvasToVideoFrameAndEncode({
canvas,
videoEncoder,
frameNumber,
videofps
})
frameNumber++;
}else{
}
} , 1000/videofps);

}

//finish and export video
async function finalizeVideo(){
console.log("finalize muxer video");
clearInterval(videoRecordInterval);
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");

}

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 = `liquify_${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);

//display user message
//recordingMessageCountdown(videoDuration);
recordingMessageDiv.classList.remove("hidden");

recordVideoState = true;

mobileRecorder.start(); //start mobile video recording

/*
setTimeout(function() {
recorder.stop();
}, 1000*videoDuration+200);
*/
}

function finalizeMobileVideo(e) {
setTimeout(function(){
console.log("finish simple video recording");
recordVideoState = false;
/*
mobileRecorder.stop();*/
var videoData = [ e.data ];
finishedBlob = new Blob(videoData, { 'type': 'video/mp4' });
downloadBlob(finishedBlob);

//hide user message
recordingMessageDiv.classList.add("hidden");

},500);

}


//MAIN METHOD
chooseBackground();
//setTimeout(startGenerativeDraw,2000);

0 comments on commit c68e26c

Please sign in to comment.