Kingsley
I'm a freelance web developer, looking to make lives and processes better with CODE.
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
By Kingsley
Electron is an open-source software framework developed and maintained by GitHub. It allows for the development of desktop GUI applications using web technologies.