Skip to content

Commit

Permalink
Merge pull request #74 from owulveryck/recording
Browse files Browse the repository at this point in the history
Recording and other improvements
  • Loading branch information
owulveryck authored Oct 1, 2023
2 parents 214981d + 84f9e49 commit 7f99494
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 33 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ The app will start, displaying a message similar to:

Then, connect to `https://72e5-22-159-32-48.ngrok-free.app` to view the result.

## Exxperimental feature: video recording

There is anew exxperimental feature to record the stream in [webm](https://en.wikipedia.org/wiki/WebM) format. This is available on the side menu.

## Technical Details

### Remarkable HTTP Server
Expand Down
41 changes: 39 additions & 2 deletions assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
.sidebar {
width: 150px;
height: 100vh;
background-color: #f1f1f1;
background-color: #a19131;
position: fixed;
top: 0;
left: -140px;
Expand Down Expand Up @@ -74,8 +74,36 @@
cursor: pointer;
border-radius: 4px;
}
.my-button.toggled {
/* Styles for the toggled state */
background-color: #2E7D32; /* Darker green for example */
color: #f0f0f0; /* Slightly off-white for contrast */
}

.icon {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 5px;
background-color: transparent;
position: relative;
border-radius: 50%;
border: 2px solid white; /* White border for record button */
background-color: red; /* Red circle for record button */
}

@keyframes fadeInOut {
0%, 100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}

.recording {
animation: fadeInOut 1s linear infinite;
}
</style>
</head>
<body>
Expand All @@ -84,7 +112,16 @@
<div class="sidebar" id="sidebar">
<ul class="menu">
<li><button id="rotate" class="my-button">Rotate</button></li>
<li><button id="screenshotButton" class="my-button">Screenshot</button><a id="screenshot"></a></li>
<li><button id="colors" class="my-button">Colors</button></li>
<li><button id="startStopButton" class="my-button">
<div class="icon" id="icon"></div>
<span id="label">Record</span>
</button></li>
<li><button id="startStopButtonWithSound" class="my-button">
<div class="icon" id="icon2"></div>
<span id="label2">Record with audio</span>
</button></li>
<li><button id="screenshotButton" class="my-button">Screenshot</button><a id="screenshot"></a></li>
</ul>
</div>
</div>
Expand Down
250 changes: 219 additions & 31 deletions assets/stream.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
let rotate = true; // start with a false boolean
let withColor = true; // start with a false boolean
let recordingWithSound = false;
document.getElementById('rotate').addEventListener('click', function() {
rotate = !rotate; // toggle the boolean

// Toggle the 'toggled' class on the button
this.classList.toggle('toggled');

resizeAndCopy();
});

document.getElementById('colors').addEventListener('click', function() {
withColor = !withColor; // toggle the boolean

// Toggle the 'toggled' class on the button
this.classList.toggle('toggled');

resizeAndCopy();
});

Expand Down Expand Up @@ -182,37 +197,44 @@ async function initiateStream() {
const value = uint8Array[i];
for (let c=0;c<count;c++) {
offset += 4;
switch (value) {
case 10: // red
imageData.data[offset] = 255;
imageData.data[offset+1] = 0;
imageData.data[offset+2] = 0;
imageData.data[offset+3] = 255;
break;
case 18: // blue
imageData.data[offset] = 0;
imageData.data[offset+1] = 0;
imageData.data[offset+2] = 255;
imageData.data[offset+3] = 255;
break;
case 20: // green
imageData.data[offset] = 125;
imageData.data[offset+1] = 184;
imageData.data[offset+2] = 86;
imageData.data[offset+3] = 255;
break;
case 24: // yellow
imageData.data[offset] = 255;
imageData.data[offset+1] = 253;
imageData.data[offset+2] = 84;
imageData.data[offset+3] = 255;
break;
default:
imageData.data[offset] = value * 17;
imageData.data[offset+1] = value * 17;
imageData.data[offset+2] = value * 17;
imageData.data[offset+3] = 255;
break;
if (withColor) {
switch (value) {
case 10: // red
imageData.data[offset] = 255;
imageData.data[offset+1] = 0;
imageData.data[offset+2] = 0;
imageData.data[offset+3] = 255;
break;
case 18: // blue
imageData.data[offset] = 0;
imageData.data[offset+1] = 0;
imageData.data[offset+2] = 255;
imageData.data[offset+3] = 255;
break;
case 20: // green
imageData.data[offset] = 125;
imageData.data[offset+1] = 184;
imageData.data[offset+2] = 86;
imageData.data[offset+3] = 255;
break;
case 24: // yellow
imageData.data[offset] = 255;
imageData.data[offset+1] = 253;
imageData.data[offset+2] = 84;
imageData.data[offset+3] = 255;
break;
default:
imageData.data[offset] = value * 10;
imageData.data[offset+1] = value * 10;
imageData.data[offset+2] = value * 10;
imageData.data[offset+3] = 255;
break;
}
} else {
imageData.data[offset] = value * 10;
imageData.data[offset+1] = value * 10;
imageData.data[offset+2] = value * 10;
imageData.data[offset+3] = 255;
}
}
// value is treated, wait for a count
Expand Down Expand Up @@ -255,5 +277,171 @@ async function initiateStream() {
}
}

let mediaRecorder;
let recordedChunks = [];
async function startRecording() {
const tempCanvas = createTempCanvas(); // Create the temporary canvas

console.log("recording in progress");
let videoStream = tempCanvas.captureStream(25); // 25 fps

if (recordingWithSound) {
// Capture audio stream from the user's microphone
let audioStream;
try {
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err) {
console.error("Error capturing audio:", err);
return;
}

// Combine video and audio streams
let combinedStream = new MediaStream([...videoStream.getTracks(), ...audioStream.getTracks()]);

mediaRecorder = new MediaRecorder(combinedStream, {
mimeType: 'video/webm;codecs=vp9'
});
} else {
mediaRecorder = new MediaRecorder(videoStream, {
mimeType: 'video/webm;codecs=vp9'
});
}

mediaRecorder.ondataavailable = function(event) {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};

mediaRecorder.onstop = function() {
download();
};

mediaRecorder.start();
}

function stopRecording() {
mediaRecorder.stop();
removeTempCanvas(); // Remove the temporary canvas after recording

// Stop updating tempCanvas
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
}

function download() {
let blob = new Blob(recordedChunks, {
type: 'video/webm'
});

let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'goMarkableStreamRecording.webm';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}

document.getElementById('startStopButtonWithSound').addEventListener('click', function() {
let icon = document.getElementById('icon2');
let label = document.getElementById('label2');


if (label.textContent === 'Record with audio') {
label.textContent = 'Stop';
icon.classList.add('recording');
recordingWithSound = true;
startRecording();
} else {
label.textContent = 'Record with audio';
icon.classList.remove('recording');
recordingWithSound = false;
stopRecording();
}
});
document.getElementById('startStopButton').addEventListener('click', function() {
let icon = document.getElementById('icon');
let label = document.getElementById('label');


if (label.textContent === 'Record') {
label.textContent = 'Stop';
icon.classList.add('recording');
startRecording();
} else {
label.textContent = 'Record';
icon.classList.remove('recording');
stopRecording();
}
});
function createTempCanvas() {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = fixedCanvas.width;
tempCanvas.height = fixedCanvas.height;
tempCanvas.id = 'tempCanvas'; // Assign an ID for easy reference

// Hide the tempCanvas
tempCanvas.style.display = 'none';

// Start updating tempCanvas
updateTempCanvas(tempCanvas);

// Append tempCanvas to the body (or any other container)
document.body.appendChild(tempCanvas);

return tempCanvas;
}
function removeTempCanvas() {
const tempCanvas = document.getElementById('tempCanvas');
if (tempCanvas) {
tempCanvas.remove();
}
}
let animationFrameId;
function updateTempCanvas(tempCanvas) {
const tempContext = tempCanvas.getContext('2d');

if (rotate) {
// Set tempCanvas dimensions to match fixedCanvas
tempCanvas.width = fixedCanvas.height;
tempCanvas.height = fixedCanvas.width;

// Clear the canvas
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempContext.save(); // Save the current state

// Move the rotation point to the center of the canvas
tempContext.translate(tempCanvas.width / 2, tempCanvas.height / 2);

// Rotate the canvas by 270 degrees
tempContext.rotate(Math.PI / 180 * 270);

// Draw the image from fixedCanvas onto tempCanvas
tempContext.drawImage(fixedCanvas, -fixedCanvas.width / 2, -fixedCanvas.height / 2);

tempContext.restore(); // Restore the state
} else {
// Reset the dimensions of tempCanvas to match fixedCanvas
tempCanvas.width = fixedCanvas.width;
tempCanvas.height = fixedCanvas.height;

// Clear the canvas
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);

tempContext.drawImage(fixedCanvas, 0, 0);
}

// Continue updating tempCanvas
animationFrameId = requestAnimationFrame(() => updateTempCanvas(tempCanvas));
}

initiateStream();



0 comments on commit 7f99494

Please sign in to comment.