If you are like most web developers, you will often yourselfΒ building web apps π, and sometimes mobile appsπ±Β Β with the stack you possess.
Today we will be looking into extending that even further?
Let us dabble in Desktop Apps today π
π€
WHAT IS ELECTRON?
If you didn't get that,
Electron is an open-source software framework developed and maintained by GitHub. It allows for the development of desktop GUI applications using web technologies
This should simplify it now πͺ
Why USE ELECTRON?
ππ
π
π
β
β
SINGLE CODEBASE (Mac, Windows & Linux)
Why USE ELECTRON?
π©πΌβπ»π¨πΎβπ»π¨πΎβπ»π©πΌβπ»
π π¨πΎβπ»Β
β
β
LESS DEVELOPERS TO HIRE
Why USE ELECTRON?
π²π²π°π°πΈ
π²π²
β
β
SAVES YOU MONEY & TIME
And also, because these guysΒ are using it π
As well as these guysΒ π
Don't USE ELECTRONΒ when..
> BIG file sizes are a problem
Β
Β
Β
> Electron apps are resource-intensive, thus apps built with Electron uses more CPU and RAM.
Β
Β
Β
But wait π€«, there is a workaround?
Slack rebuilt it's desktop app with Electron and found ways to make up for the RAM and CPU-intensive nature of it.
Β
There is a workaround after all.
> npx create-electron-app twa-me-screenrecorder
> npm start
// To run the application and for testing
> npm run make
// To build/bundle it into a desktop application
// It bundles automatically according
// to the platform you are currently on
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Twa Me πΈ - Screen Recording App for MacOS, Windows & Linux </title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/foundation-sites@6.6.3/dist/css/foundation.min.css" integrity="sha256-ogmFxjqiTMnZhxCqVmcqTvjfe1Y/ec4WaRj/aQPvn+I=" crossorigin="anonymous">
<link rel="stylesheet" href="index.css">
<script defer src="render.js"></script>
</head>
<body>
<h1 class="title"> Twa Me πΈ </h1>
<h2>
Screen Recorder for MacOS, Windows & Linux
</h2>
<main>
<video src="#" id="videoPreview"></video>
<!-- Actions buttons -->
<button id="startButton" type="button" class="button success -start">
<div class="button__icon material-icons">play_arrow</div>
<div class="button__text">
Start
</div>
</button>
<button id="stopButton" type="button" class="button alert -stop">
<div class="button__icon material-icons">stop</div>
<div class="button__text">
Stop
</div>
</button>
<button id="pauseButton" type="button" class="button warning -pause">
<div class="button__icon material-icons">pause</div>
<div class="button__text">
Pause
</div>
</button>
<p id="feedback" class="saveLocation"></p>
<hr />
<button id="selectVideoSource" class="button">
<div class="button__icon material-icons">play_arrow</div>
<div class="button-text">
Choose a Video Source
</div>
</button>
</main>
</body>
</html>
// render.js file
// Getting buttons
// Click to start recording
const startButton = document.getElementById('startButton');
startButton.onclick = e => {
// Run only when a video source has been selected
if (!mediaRecorder) {
document.getElementById('feedback').innerText = 'Please select a video source first'
}
else {
mediaRecorder.start();
startButton.classList.add('warning');
startButton.querySelector('.button__text').innerText = 'Recording';
}
}
function changeText(newText) {
querySelector('.button__text').innerText = newText;
}
function reset(buttonRef) {
buttonRef.classList.remove('warning', 'secondary');
switch (buttonRef) {
case 'startButton':
buttonRef.changeText('Start');
break;
case 'pauseButton':
buttonRef.changeText('Pause');
break;
default:
return
}
}
// Click to stop the recording
const stopButton = document.getElementById('stopButton');
stopButton.onclick = e => {
// Run only when a video source has been selected
if (!mediaRecorder) {
document.getElementById('feedback').innerText = 'Please select a video source first'
}
else {
mediaRecorder.stop();
startButton.classList.add('secondary');
reset(startButton);
}
}
// Click to pause the recording
const pauseButton = document.getElementById('pauseButton');
pauseButton.onclick = e => {
// Run only when a video source has been selected
if (!mediaRecorder) {
document.getElementById('feedback').innerText = 'Please select a video source first'
}
else if (mediaRecorder.state == "inactive") {
document.getElementById('feedback').innerText = 'Please start recording first'
}
else if (mediaRecorder.state !== "paused") {
mediaRecorder.pause();
pauseButton.querySelector('.button__text').innerText = 'Resume';
pauseButton.classList.remove('warning');
pauseButton.classList.add('success');
}
else {
mediaRecorder.resume();
pauseButton.querySelector('.button__text').innerText = 'Pause';
pauseButton.classList.add('warning');
// reset(pauseButton);
}
}
// Click to list all available video sources
const selectVideoSource = document.getElementById('selectVideoSource');
// Preview for video source
const previewVideo = document.querySelector('video');
selectVideoSource.onclick = getVideoSources;
const { desktopCapturer, remote } = require('electron');
const { Menu, dialog } = remote;
const { writeFile } = require('fs');
const { checkServerIdentity } = require('tls');
// Get the Available video sources
async function getVideoSources() {
const inputSources = await desktopCapturer.getSources({
types: ['window', 'screen']
});
// Populate the available video sources in a Menu/Dropdown
const VideoSourceOptions = Menu.buildFromTemplate(
inputSources.map(source => {
return {
label: source.name,
click: () => selectSource(source),
};
})
);
VideoSourceOptions.popup();
}
// Empty array for populating recorded data
const chunks = [];
let mediaRecorder;
// Setup Options for MediaRecorder
const mediaRecorderOptions = { mimeType: 'video/webm; codecs=vp9' };
// MIME types (IANA media types)
// A media type (also known as a Multipurpose Internet Mail Extensions or MIME type)
// Is a standard that indicates the nature and format of a document, file, or assortment of bytes.
// Select video source from the list
async function selectSource(source) {
selectVideoSource.innerText = source.name;
const constraints = {
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: source.id,
}
}
}
// Create stream
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// Set the video source object on the frontend to be the stream
previewVideo.srcObject = stream;
previewVideo.play();
// ===== Start RECORDING ===== //
// The MediaRecorder interface of the MediaStream Recording API provides functionality to easily record media.
// Source: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder
mediaRecorder = new MediaRecorder(stream, mediaRecorderOptions);
// Some Events when recording starts
mediaRecorder.ondataavailable = handleVideoDataExists;
mediaRecorder.onstop = handleVideoDataStopped;
}
// Combine video chunks into one array
function handleVideoDataExists(e) {
chunks.push(e.data);
}
const { mimeType } = mediaRecorderOptions;
// Save video file
async function handleVideoDataStopped(e) {
const blob = new Blob(chunks, {
type: mimeType
});
const buffer = Buffer.from(await blob.arrayBuffer());
// https://stackoverflow.com/questions/11821096/what-is-the-difference-between-an-arraybuffer-and-a-blob
// ArrayBuffer - for manipulations to a Blob
// The buffers module provides a way of handling streams of binary data.
// Ref: https://www.w3schools.com/nodejs/ref_buffer.asp
const { filePath } = await dialog.showSaveDialog({
buttonLabel: "Save Video",
defaultPath: `twa me ScreenRecording-01.webm`,
});
document.querySelector('.saveLocation').innerText = `video saved at ${filePath}`;
writeFile(filePath, buffer);
}
Download source code for the project here:
Β
// To run live demo
> npm start
// To build into a desktop app
> npm run make
// ENJOY SCREEN RECORDING WITH TWA ME πΈ
And before I forget, I built this project in at most 3 hours, was feeling drowsy so, pardon me for some of the sphagetti code πΒ
ENJOY πΈΒ
YOU MADE IT THROUGH THIS FAR.Β π Β ππ
Resources