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!

Other Chrome Extensions for Google Meet either don't have the feature or they're buggy

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 ๐Ÿ‘‡

Automation Expectation vs. Reality


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:

  1. Keyboard shortcut pressed - chrome.commands

    keyboard shortcut event handler

    1chrome.commands.onCommand.addListener(function(command) {
    2 console.log('keyboard shortcut pressed: ', command);
  2. 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 });

    tab ID is required when sending a message from extension (aka background script) to a content script.

  3. Receiving an one-time event from background or content script (This is same for both) - runtime.onMessage

    A one-time event listener

    2 function(request, sender, sendResponse) {
    3 sendResponse('Your request was received');
    4 }
  4. 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' });
    4 // Sending a message to content script
    5 port.postMessage({joke: 'Knock knock'});
    7 // Receiving a message from content script
    8 port.onMessage.addListener(function(msg) {
    9 if (msg.question == "Who's there?")
    10 port.postMessage({answer: "Madame"});
    11 else
    12 console.log('Response received from content script: ${msg.question}');
    13 });

    Remember to pass the right tab ID.

  5. 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 });


These have access to web page's DOM and can make changes to them. Some of the APIs that I found useful:

  1. 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}');

    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.

  2. 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"});
    3// Sending a message to background script
    4port.postMessage({joke: "Knock knock"});
    6// Receiving a message from background script
    7port.onMessage.addListener(function(msg) {
    8 if (msg.question == "Who's there?")
    9 port.postMessage({answer: "Madame"});

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 โฌ‡๏ธ


"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!

Could not establish connection. Receiving end does not exist error.

"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.

The message port closed before a response was received

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...!"))()"

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:

Graph to decide whether automating something is worth the time or not

And this is reality ๐Ÿ‘‡

how much time I invested

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:

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

Liked the article? Share it on: Twitter
No spam. Unsubscribe at any time.