ASCII-GLB-Viewer / ASCII.js
Suryash Malviya
reconfigured the setup
695ea30
Raw History Blame Contribute Delete
30.3 kB
/*
To do:
Allow toggle for different monospace fonts (Japanese, bolder font, etc.)
Allow image upload, with function to determine based on the file extension and handle accordingly
When image is uploaded, use a new function that accepts the image and then runs scanLines algo on it -- as pseudo video feed
Investigate frame rate unsynced issue when video recording -- video export can have dropped frames / uneven time / low quality
Allow custom char set during Random Text mode (so that you can try with only a few chars)
Add button that exports the relevant chars / line breaks into a textfield, for a specific frame
*/
var webcamVideo = document.getElementById('webcamVideo');
var userVideo = document.getElementById('userVideo');
var defaultVideo = document.getElementById('defaultVideo');
//Final animation canvas
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
//Canvas for raw still images from video
const canvasRaw = document.getElementById('canvas-video')
const ctx2 = canvasRaw.getContext("2d", {
willReadFrequently: true,
});
//canvas for pixelated grayscale images
const canvasPixel = document.getElementById('canvas-video-pixel')
const ctx3 = canvasPixel.getContext("2d");
var webcamAspectRatio = 1;
var webcamVideoMaxWidth = 1080;
var resizedWebcamWidth = Math.min(webcamVideoMaxWidth,Math.floor(window.innerWidth));
var resizedWebcamHeight = Math.round(resizedWebcamWidth / webcamAspectRatio);
var defaultVideoWidth = 480;
var defaultVideoHeight = 848;
var canvasWidth = defaultVideoWidth;
var canvasHeight = defaultVideoHeight;
var maxCanvasWidth = 1080;
var pixelSize;
var numCols;
var numRows;
var alpha=1;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
var effectWidthInput = document.getElementById("effectWidthInput");
effectWidthInput.style.width = canvasWidth;
var effectWidth = Number(effectWidthInput.value)/100;
effectWidthInput.addEventListener("change",refresh);
var effectWidthLabel = document.getElementById("effectWidthLabel");
var videoPixels = [];
var grayscaleDataArray = [];
var fontFamily = "Courier New";
var fontSize;
//ctx.font;
//this defines the character set. ordered by darker to lighter colour
const gradient = "____``..--^^~~<>??123456789%%&&@@";
//const gradient = "_______.:!/r(l1Z4H9W8$@";
//const gradient = "`.-':_,^=;><+!rc*/z?sLTv)J7(|Fi{C}fI31tlu[neoZ5Yxjya]2ESwqkP6h9d4VpOGbUAKXHm8RD#$Bg0MNWQ%&@"
//const gradient = "`.-':_,^=;<+!rc*/z?sTv7(|Fi{C}fI31tu[neoZ5xjya]2EwqkP6h94VpOGbUKXHm8R#$Bg0NW%&@"
const preparedGradient = gradient.replaceAll('_', '\u00A0')
var randomColumnArray = [];
var startingRowArray = [];
var animationRequest;
var playAnimationToggle = false;
var counter = 0;
var webcamStream;
var mediaRecorder;
var recordedChunks;
var finishedBlob;
var recordingMessageDiv = document.getElementById("videoRecordingMessageDiv");
var recordVideoState = false;
var videoRecordInterval;
var videoEncoder;
var muxer;
var mobileRecorder;
var videofps = 12;
var frameNumber = 0;
//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);
//CREATE USER GUI MENU
var obj = {
backgroundColor: "#080c37",
backgroundGradient: true,
backgroundSaturation: 60,
fontColor: "#c7205b",
fontColor2: "#0032ff",
fontSizeFactor: 3,
pixelSizeFactor: 70,
threshold: 30,
textInput: "wavesand",
randomness: 15,
invert: false,
animationType: 'Random Text',
};
var videoType = "Default";
var animationType = obj.animationType;
var backgroundColor = obj.backgroundColor;
var backgroundRGB = hexToRgb(backgroundColor)
var backgroundHue = getHueFromHex(backgroundColor);
var backgroundSaturation = obj.backgroundSaturation;
var backgroundGradient = obj.backgroundGradient;
var fontSizeFactor = obj.fontSizeFactor;
var pixelSizeFactor = obj.pixelSizeFactor;
var fontColor = obj.fontColor;
var fontHue = getHueFromHex(fontColor);
var fontColor2 = obj.fontColor2;
var threshold = obj.threshold/100;
var textInput = obj.textInput;
var randomness = obj.randomness/100;
var invertToggle = obj.invert;
var gui = new dat.gui.GUI({ autoPlace:false });
gui.close();
var guiOpenToggle = false;
obj['selectVideo'] = function () {
videoType = "Select Video";
fileInput.click();
};
gui.add(obj, 'selectVideo').name('Upload Video');
obj['useWebcam'] = function () {
videoType = "Webcam";
changeVideoType();
};
gui.add(obj, 'useWebcam').name('Use Webcam');
gui.addColor(obj, "backgroundColor").name("Background Color").onFinishChange(refresh);
gui.add(obj,"backgroundGradient").name('Bg Gradient?').onChange(refresh);
gui.add(obj, "backgroundSaturation").min(0).max(100).step(1).name('Bg Saturation').onChange(refresh);
gui.addColor(obj, "fontColor").name("Font Color").onFinishChange(refresh);
gui.addColor(obj, "fontColor2").name("Font Color2").onFinishChange(refresh);
gui.add(obj, "fontSizeFactor").min(0).max(10).step(1).name('Font Size Factor').onChange(refresh);
gui.add(obj, "pixelSizeFactor").min(10).max(200).step(1).name('Resolution').onChange(refresh);
gui.add(obj, "threshold").min(0).max(95).step(1).name('Threshold').onChange(refresh);
gui.add(obj,"invert").name('Invert?').onChange(refresh);
gui.add(obj, "randomness").min(0).max(100).step(1).name('Randomness').onChange(refresh);
gui.add(obj, 'animationType', [ 'Random Text', 'User Text'] ).name('Text Type').onChange(refresh);
gui.add(obj, "textInput").onFinishChange(refresh);
obj['pausePlay'] = function () {
togglePausePlay();
};
gui.add(obj, 'pausePlay').name("Pause/Play");
obj['saveImage'] = function () {
saveImage();
};
gui.add(obj, 'saveImage').name("Image Export");
obj['saveVideo'] = function () {
toggleVideoRecord();
};
gui.add(obj, 'saveVideo').name("Start/Stop Video Export");
customContainer = document.getElementById( 'gui' );
customContainer.appendChild(gui.domElement);
var guiCloseButton = document.getElementsByClassName("close-button");
console.log(guiCloseButton.length);
guiCloseButton[0].addEventListener("click",updateGUIState);
var useWebcamButton = document.getElementById("useWebcamButton");
useWebcamButton.addEventListener("click",function(){
videoType = "Webcam";
changeVideoType();
});
//turn video input into still images, and then into pixelated grayscale values
const render = (ctx) => {
if (canvasWidth && canvasHeight) {
canvasRaw.width = canvasWidth;
canvasRaw.height = canvasHeight;
//choose video feed
if(videoType == "Webcam"){
ctx2.drawImage(webcamVideo, 0, 0, canvasWidth, canvasHeight);
} else if(videoType == "Select Video"){
ctx2.drawImage(userVideo, 0, 0, canvasWidth, canvasHeight);
} else if(videoType == "Default"){
ctx2.drawImage(defaultVideo, 0, 0, canvasWidth, canvasHeight);
}
var pixelData = ctx2.getImageData(0, 0, canvasWidth, canvasHeight);
var pixels = pixelData.data;
//new canvas with a pixelated image
canvasPixel.width = canvasWidth;
canvasPixel.height = canvasHeight;
videoPixels = [];
grayscaleDataArray = [];
for(var cellY=0; cellY<numRows; cellY++){
grayscaleDataArray[cellY] = [];
for(var cellX=0; cellX<numCols; cellX++){
var cellPixels = [];
for(var pixelY=0; pixelY<pixelSize; pixelY++){
for(var pixelX=0; pixelX<pixelSize; pixelX++){
var currentXPosition = cellX*pixelSize + pixelX;
var currentYPosition = cellY*pixelSize + pixelY;
var currentPixelDataValue = (currentYPosition * canvasWidth + currentXPosition) * 4;
if(currentXPosition < canvasWidth && currentYPosition < canvasHeight){
cellPixels.push(pixels[currentPixelDataValue]);
cellPixels.push(pixels[currentPixelDataValue + 1]);
cellPixels.push(pixels[currentPixelDataValue + 2]);
cellPixels.push(pixels[currentPixelDataValue + 3]);
}
}
}
var avgColor = getAverageColor(cellPixels);
//ctx3.fillStyle = `rgba(${avgColor[0]}, ${avgColor[1]}, ${avgColor[2]}, ${alpha})`;
//ctx3.fillRect(cellX*pixelSize, cellY*pixelSize, pixelSize, pixelSize);
//videoPixels.push(avgColor[0]);
//videoPixels.push(avgColor[1]);
//videoPixels.push(avgColor[2]);
//videoPixels.push(alpha);
var grayScaleValue = (0.299*avgColor[0] + 0.587*avgColor[1] + 0.114*avgColor[2]); //perceived luminosity value
grayscaleDataArray[cellY][cellX] = grayScaleValue;
}
}
} else {
ctx2.fillStyle = "#fff";
ctx2.fillRect(0, 0, canvasWidth, canvasHeight);
}
}
const getCharByScale = (scale) => {
const val = Math.floor(scale / 255 * (gradient.length - 1));
return preparedGradient[val];
}
//draw the text and background color for each frame onto the final canvas
function renderText(){
ctx.fillStyle = backgroundColor;
ctx.fillRect(0,0,canvasWidth*effectWidth,canvasHeight);
for(var col=0; col<numCols; col++){
for(var row=0; row<numRows; row++){
var adjustedThreshold = threshold + (0.2 * Math.sin(counter/30) * randomness);
var currentGrayValue = grayscaleDataArray[row][col];
var char;
var currentFontSize = Math.min(fontSize*3, fontSize * fontSizeFactor/3);
//draw background color of pixels
if(counter%8==0 && Math.random() < randomness*0.002){
ctx.fillStyle = tweakHexColor(backgroundColor,100*randomness);
ctx.fillRect(col*pixelSize,row*pixelSize,pixelSize,pixelSize);
} else if(backgroundGradient){
var currentBackgroundColor = "hsl("+backgroundHue+","+backgroundSaturation+"%,"+Math.pow(currentGrayValue/255,2)*100+"%)";
var currentBackgroundColorInvert = "hsl("+backgroundHue+","+backgroundSaturation+"%,"+(1-Math.pow(currentGrayValue/255,2))*100+"%)";
if(invertToggle == false){
if(currentGrayValue/255 > adjustedThreshold){
ctx.fillStyle = currentBackgroundColor;
} else {
ctx.fillStyle = "hsl("+backgroundHue+","+backgroundSaturation+"%,"+adjustedThreshold/4*100+"%)";
}
} else {
if(currentGrayValue/255 < (1-adjustedThreshold)){
ctx.fillStyle = currentBackgroundColorInvert;
} else {
ctx.fillStyle = "hsl("+backgroundHue+","+backgroundSaturation+"%,"+adjustedThreshold/4*100+"%)";
}
}
ctx.fillRect(col*pixelSize,row*pixelSize,pixelSize,pixelSize);
} else {
}
//choose text character to draw
if(randomColumnArray[col]){
if( (((counter+startingRowArray[col]) % 100)/100*numRows)+startingRowArray[col] > row){
char = getCharByScale(currentGrayValue);
} else {
char = "";
}
} else if(Math.random()<0.005*randomness){
char = preparedGradient[Math.floor(Math.random()*preparedGradient.length)]; //draw random char
} else if(animationType == "Random Text"){
char = getCharByScale(currentGrayValue);
} else if(animationType == "User Text"){
char = textInput[(row*numCols+col)%textInput.length];
if(invertToggle){
currentFontSize = Math.min(fontSize*3, Math.floor( (1-Math.pow(currentGrayValue/255,1)) * fontSizeFactor/3 * fontSize ));
} else {
currentFontSize = Math.min(fontSize*3, Math.floor( (Math.pow(currentGrayValue/255,1)) * fontSizeFactor/3 * fontSize ));
}
}
//draw text onto canvas
ctx.font = currentFontSize+"px "+fontFamily;
if(invertToggle == false){
if(currentGrayValue/255 > adjustedThreshold){
ctx.fillStyle = interpolateHex(fontColor,fontColor2,(currentGrayValue/255-adjustedThreshold) / (1-adjustedThreshold))
ctx.fillText(char, col*pixelSize, row*(pixelSize) + pixelSize);
//ctx.strokeStyle = fontColor;
//ctx.strokeText(char, col*pixelSize + pixelSize/4, row*(pixelSize) + pixelSize/2);
}
} else {
if(currentGrayValue/255 < (1-adjustedThreshold)){
ctx.fillStyle = interpolateHex(fontColor2,fontColor,(currentGrayValue/255) / (1-adjustedThreshold))
ctx.fillText(char, col*pixelSize, row*(pixelSize) + pixelSize);
}
}
}
}
}
//animation loop to go frame by frame
function loop(){
if(counter==0){
console.log("start animation, first frame");
}
if(playAnimationToggle){
counter++;
render(ctx);
if(effectWidth < 1){
//draw the chosen video onto the final canvas
if(videoType == "Webcam"){
ctx.drawImage(webcamVideo, 0, 0, canvasWidth, canvasHeight);
} else if(videoType == "Select Video"){
ctx.drawImage(userVideo, 0, 0, canvasWidth, canvasHeight);
} else if(videoType == "Default"){
ctx.drawImage(defaultVideo, 0, 0, canvasWidth, canvasHeight);
}
}
renderText();
if(recordVideoState == true){
renderCanvasToVideoFrameAndEncode({
canvas,
videoEncoder,
frameNumber,
videofps
})
frameNumber++;
}
animationRequest = requestAnimationFrame(loop);
}
}
//HELPER FUNCTIONS BELOW
function selectVideo(){
videoType = "Select Video";
fileInput.click();
}
function updateGUIState(){
if(guiOpenToggle){
guiOpenToggle = false;
} else {
guiOpenToggle = true;
}
}
function refresh(){
console.log("refresh");
console.log("canvas width/height: "+canvasWidth+", "+canvasHeight);
document.getElementById("canvasDiv").setAttribute("style", "width: "+canvasWidth+"px;");
//effectWidthInput.style.width = canvasWidth;
effectWidth = Number(effectWidthInput.value)/100;
effectWidthLabel.innerHTML = "Effect Width: "+Math.round(effectWidth*100)+"%";
animationType = obj.animationType;
fontSizeFactor = obj.fontSizeFactor;
pixelSizeFactor = obj.pixelSizeFactor;
pixelSize = Math.ceil(Math.min(canvasWidth,canvasHeight)/pixelSizeFactor);
numCols = Math.ceil(Math.ceil(canvasWidth / pixelSize) * effectWidth);
numRows = Math.ceil(canvasHeight / pixelSize);
fontSize = pixelSize/0.65;
ctx.font = fontSize+"px "+fontFamily;
fontColor = obj.fontColor;
fontColor2 = obj.fontColor2;
fontHue = getHueFromHex(fontColor);
backgroundColor = obj.backgroundColor;
backgroundRGB = hexToRgb(backgroundColor)
backgroundHue = getHueFromHex(backgroundColor);
backgroundSaturation = obj.backgroundSaturation;;
backgroundGradient = obj.backgroundGradient;
threshold = obj.threshold/100;
textInput = obj.textInput;
counter = 0;
randomness = obj.randomness/100;
invertToggle = obj.invert;
randomColumnArray = [];
startingRowArray = [];
for(var i=0; i<numCols; i++){
if(Math.random() < randomness){
randomColumnArray[i] = true;
startingRowArray[i] = Math.floor( Math.random() * numRows );
} else {
randomColumnArray[i] = false;
}
}
}
function togglePausePlay(){
if(playAnimationToggle == false){
if(videoType == "Webcam"){
startWebcam();
} else if(videoType == "Select Video"){
refresh();
userVideo.play();
playAnimationToggle = true;
animationRequest = requestAnimationFrame(loop);
} else if(videoType == "Default"){
startDefaultVideo();
}
} else {
stopVideo();
}
}
function changeVideoType(){
stopVideo();
if(videoType == "Webcam"){
startWebcam();
} else if(videoType == "Select Video"){
console.log("select video file");
selectVideo();
} else if(videoType == "Default"){
startDefaultVideo();
}
refresh();
}
function startDefaultVideo(){
if(playAnimationToggle==true){
playAnimationToggle = false;
cancelAnimationFrame(animationRequest);
console.log("cancel animation");
}
canvasWidth = defaultVideoWidth;
canvasHeight = defaultVideoHeight;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
defaultVideo.play();
playAnimationToggle = true;
animationRequest = requestAnimationFrame(loop);
}
function startWebcam() {
if(playAnimationToggle==true){
playAnimationToggle = false;
cancelAnimationFrame(animationRequest);
console.log("cancel animation");
}
navigator.mediaDevices.getUserMedia({
audio: false,
video: true
})
.then(stream => {
window.localStream = stream;
webcamVideo.srcObject = stream;
webcamVideo.play();
if(isIOS || isAndroid){
webcamAspectRatio = 3/4;
} else {
webcamAspectRatio = stream.getVideoTracks()[0].getSettings().aspectRatio;
}
if(webcamAspectRatio == undefined){
webcamAspectRatio = 1.33333;
}
console.log("Aspect Ratio: "+webcamAspectRatio);
resizedWebcamWidth = Math.min(webcamVideoMaxWidth,Math.floor(window.innerWidth));
resizedWebcamHeight = Math.round(resizedWebcamWidth / webcamAspectRatio);
canvasWidth = resizedWebcamWidth;
canvasHeight = resizedWebcamHeight;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
refresh();
playAnimationToggle = true;
animationRequest = requestAnimationFrame(loop);
})
.catch((err) => {
console.log(err);
});
}
var localStream;
function stopVideo(){
if(playAnimationToggle==true){
playAnimationToggle = false;
cancelAnimationFrame(animationRequest);
console.log("cancel animation");
}
webcamVideo.pause();
userVideo.pause();
defaultVideo.pause();
if(localStream == null){
} else {
localStream.getVideoTracks()[0].stop();
}
}
var fileInput = document.getElementById("fileInput");
fileInput.addEventListener('change', (e) => {
if(playAnimationToggle==true){
playAnimationToggle = false;
cancelAnimationFrame(animationRequest);
console.log("cancel animation");
}
videoType = "Select Video";
const file = e.target.files[0];
const url = URL.createObjectURL(file);
userVideo.src = url;
userVideo.addEventListener('loadedmetadata', () => {
userVideo.width = userVideo.videoWidth;
userVideo.height = userVideo.videoHeight;
console.log("user video width/height: "+userVideo.width+", "+userVideo.height);
canvasWidth = Math.min(userVideo.videoWidth, maxCanvasWidth);
canvasHeight = Math.floor(canvasWidth * (userVideo.videoHeight / userVideo.videoWidth));
canvas.width = canvasWidth;
canvas.height = canvasHeight;
});
setTimeout(function(){
userVideo.play();
refresh();
playAnimationToggle = true;
animationRequest = requestAnimationFrame(loop);
},2000);
});
function getAverageColor(chosenPixels) {
var r = 0;
var g = 0;
var b = 0;
var count = chosenPixels.length / 4;
for (let i = 0; i < count; i++) {
r += chosenPixels[i * 4];
g += chosenPixels[i * 4 + 1];
b += chosenPixels[i * 4 + 2];
}
return [r / count, g / count, b / count];
}
function getHueFromHex(hex) {
const rgb = hexToRgb(hex);
const r = rgb.r / 255;
const g = rgb.g / 255;
const b = rgb.b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let hue = 0;
if (delta === 0) {
hue = 0;
} else if (max === r) {
hue = (g - b) / delta;
} else if (max === g) {
hue = 2 + (b - r) / delta;
} else {
hue = 4 + (r - g) / delta;
}
hue *= 60;
if (hue < 0) {
hue += 360;
}
return hue;
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function rgbToHue(r, g, b) {
const rNorm = r / 255;
const gNorm = g / 255;
const bNorm = b / 255;
const hue = Math.atan2(Math.sqrt(3) * (gNorm - bNorm), 2 * rNorm - gNorm - bNorm);
return hue * 180 / Math.PI;
}
function rgbToSaturation(r, g, b) {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
return (max - min) / max;
}
function rgbToLightness(r, g, b) {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
return (max + min) / 2 / 255;
}
function interpolateHex(hex1,hex2,factor){
hex1RGB = hexToRgb(hex1);
hex2RGB = hexToRgb(hex2);
var newR = Math.round(hex1RGB.r + (hex2RGB.r - hex1RGB.r)*factor);
var newG = Math.round(hex1RGB.g + (hex2RGB.g - hex1RGB.g)*factor);
var newB = Math.round(hex1RGB.b + (hex2RGB.b - hex1RGB.b)*factor);
var rgbResult = "rgb("+newR+","+newG+","+newB+")";
return rgbResult;
}
function tweakHexColor(hexColor, range){
var rgb = hexToRgb(hexColor);
var newRGBArray = [];
newRGBArray.push(Math.floor(rgb.r+range*Math.random()-range/2));
newRGBArray.push(Math.floor(rgb.b+range*Math.random()-range/2));
newRGBArray.push(Math.floor(rgb.g+range*Math.random()-range/2));
var newHexColor = rgbToHex(newRGBArray[0],newRGBArray[1],newRGBArray[2]);
return newHexColor;
}
function rgbToHex(r, g, b) {
return "#" + (
(r.toString(16).padStart(2, "0")) +
(g.toString(16).padStart(2, "0")) +
(b.toString(16).padStart(2, "0"))
);
}
/*
//shortcut hotkey presses
document.addEventListener('keydown', function(event) {
if(event.shiftKey && event.key == 'p'){
togglePausePlay();
} else if (event.key === 'i' && event.shiftKey) {
saveImage();
} else if (event.key === 'v' && event.shiftKey) {
toggleVideoRecord();
} else if (event.key === 'o' && event.shiftKey) {
dat.GUI.toggleHide();
}
});
//shortcut hotkey presses
document.addEventListener('keydown', function(event) {
if(event.key === 'h') {
toggleGUI();
}
});
*/
function saveImage(){
const link = document.createElement('a');
link.href = canvas.toDataURL();
const date = new Date();
const filename = `ASCII_${date.toLocaleDateString()}_${date.toLocaleTimeString()}.png`;
link.download = filename;
link.click();
}
function toggleGUI(){
if(guiOpenToggle == false){
gui.open();
guiOpenToggle = true;
} else {
gui.close();
guiOpenToggle = false;
}
}
function toggleVideoRecord(){
userVideo.currentTime = 0;
defaultVideo.currentTime = 0;
setTimeout(function(){
if(recordVideoState == false){
recordVideoState = true;
chooseRecordingFunction();
} else {
recordVideoState = false;
chooseEndRecordingFunction();
}
},250);
}
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);
frameNumber = 0;
//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: 14_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 = `ASCII_${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
refresh();
startDefaultVideo();