How I Built a Google Chrome Extension for Google Meet
Last updated on 11 April 2021
I've recently started using Google Meet a lot more than I used to. There are some cases when I need to switch to Zoom or Airmeet but Google Meet still takes a big share.
The thing with Google Meet is, it takes a bit to toggle voice/video when you're jumping around different tabs.
The default keyboard shortcuts work only if you have the tab actually open. It'd be very convenient if I can just toggle them from any tab.
Idea validation
Doing a quick search showed me these three extensions. None of them actually has what I was looking for!
This is all the validation I needed 🚀
Should I build it?
Yes. Because even if it buys me very short amount of time, the numbers get big if you consider a longer time-frame.
Just a thought experiment,
15 sec/day * 300 days/yr ~= 1500 sec/yr = ~4 hrs/decade.
While it is most likely that I'll spend those 4 hours procrastinating, I still want those 4 hours.
I just don't want this to happen 👇
Implementation
I've had some experience building a Firefox extension once and the APIs for Chrome and Firefox are quite similar.
For my use-case, there are two major components required, background script and content script. Here are some of the APIs that I came across and found useful:
Background script
Extension triggers some action in response to certain events. Some of the common APIs:
Keyboard shortcut pressed - chrome.commands
keyboard shortcut event handler
1chrome.commands.onCommand.addListener(function(command) {2 console.log('keyboard shortcut pressed: ', command);3});Sending an one-time event from background script to content script - tabs.sendMessage
Sending an event to the respective content script
1chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {2 chrome.tabs.sendMessage(tabs[0].id, {greeting: 'hello'}, function(response) {3 console.log(response.farewell);4 });5});tab ID is required when sending a message from extension (aka background script) to a content script.
Receiving an one-time event from background or content script (This is same for both) - runtime.onMessage
A one-time event listener
1chrome.runtime.onMessage.addListener(2 function(request, sender, sendResponse) {3 sendResponse('Your request was received');4 }5);Creating & managing a long-lived connection from background script to content script - tabs.connect
Creating a long lived connection
1chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {2 const port = chrome.tabs.connect(tabs[0].id, { name: 'knockknock' });34 // Sending a message to content script5 port.postMessage({joke: 'Knock knock'});67 // Receiving a message from content script8 port.onMessage.addListener(function(msg) {9 if (msg.question == "Who's there?")10 port.postMessage({answer: "Madame"});11 else12 console.log('Response received from content script: ${msg.question}');13 });14});Remember to pass the right tab ID.
Receiving an event from content or background script in case of long-lived connection (This is same for both). - runtime.onConnect
A long-lived connection listener
1chrome.runtime.onConnect.addListener(function(port) {2 port.onMessage.addListener(function(msg) {3 console.log(msg);4 });5});
Content-Script
These have access to web page's DOM and can make changes to them. Some of the APIs that I found useful:
Sending an event from content script to background script - runtime.sendMessage
Sending a message to background script
1chrome.runtime.sendMessage({foo: "bar"}, function(response) {2 console.log('response from background script = ${response}');3});As the background script does not run in the context of a browser tab, we don't need to specify any ID when sending any events to it.
Creating & managing a long-lived connection from content script to background script - runtime.connect
A long-lived connection creation from content script
1var port = chrome.runtime.connect({name: "knockknock"});23// Sending a message to background script4port.postMessage({joke: "Knock knock"});56// Receiving a message from background script7port.onMessage.addListener(function(msg) {8 if (msg.question == "Who's there?")9 port.postMessage({answer: "Madame"});10});
There are some common errors that may arise if you don't set up the above mentioned scripts correctly. Let's take a look at some that I stumbled upon ⬇️
Mistakes
"Could not establish connection. Receiving end does not exist"
You need to have the sender and the receiver in place for successful communication. When sending an event from the background script, there needs to be a receiver listening to it in the content script and vice-versa.
It is quite obvious but I managed to fall for this!
"The message port closed before a response was received."
I got this when I tried to run executeScript()
or insertCSS()
in the content-script.
There are different APIs for sending an event for one-time and long-lived connections.
If you run into this error, it is quite likely that what you need is a long-lived connection. I’ve already shown above how to create it.
Long-lived connection gives you a port which then can be used to send and receive events between background and content-script.
executeScript and insertCSS not working
Executing a custom script from content-script
didn't work out. But I found a workaround for it. Instead of triggering executeScript
or insertCSS
from content script, I triggered it directly from the background script.
And, it worked 🎉!
With this, I don't need any communication between content-script and the extension's background script. Everything is taken care of in the background script itself 😄.
Executing a script from background script
1chrome.tabs.executeScript(tabId, {2 code: "(() => console.log("Running an IIFE...!"))()"3});
Here, tabId represents the ID of the browser tab where you want your script to run.
Fun fact
I've already lost more time than what I gained by solving the problem. I spent more time building it than what it would save me in the future.
Here's the chart that I took as reference for this:
And this is reality 👇
I should have built it within 2 hours!
What next?
Building this has helped me getting a sense of what extensions are capable of.
To my surprise, commands can have global scope as of version 35 (with an exception of chrome OS). This means toggling voice/video works even when Chrome does not have the focus.
With the completion of the building phase comes the shipping. As I haven't published this yet, here's the to-do list:
📌 To-Do:
- ☑️ Landing page: meeteor.xyz
- ☑️ Buy a domain 😉
- ☑️ Ship it: The extension is live now 🔗.
And that is my (little) journey to build a chrome extension. Let me know how your experience has been with a spontaneous & time constrained side-projects. We can chat in the comments or you can shoot me a mail at hi@rrawat.com.