Controlling Google Meet – Part 1: Build the Chrome Extension

Vinay Thakker
  • 7 min read

At Wursta, we have a fully distributed team with a geo-diverse customer base. In the day-to-day, we rely heavily on digital meetings to engage with our customers and interact with our internal teams. As a beloved G Suite partner (and customer), we use Google Meet a lot. Proper meeting etiquette suggests that you keep your mic muted unless you are speaking. The frequent annoyance we have all experienced is: someone asks you a question while you are muted. You are multitasking in another window, and you scramble to find the meeting window so you can unmute yourself.

Today, we are going to build a Chrome Extension to solve that problem. In this multi-part series, we will show you:

What exactly are we building?

The end goal of our extension is simple.

  1. Pressing a hotkey will toggle the audio mute, if a Google Meet is running in the Chrome session.
  2. When the mute state is changed, present a notification to the user.

Anatomy of our extension

Our extension is comprised of 3 main files.

  1. manifest.json
  2. keyboard_listener.js
  3. meet_controller.js 

With the exception of manifest.json, you can use your own file naming and folder scheme.  As you build out your own extensions, you may find yourself with many other files, such as supporting HTML, CSS, and images. Our extension is generally simplified by not having a user interface.


This file is mandatory in all Chrome extensions; it gives Chrome meaningful metadata about your extension: its name, version, required permissions, the type and location of your various scripts, and more.

Note: We added some /*-- comment descriptions --*/, which are not JSON-compliant. You’ll need to remove them if you copy and paste from this example.

  1. {
  2. /* —- Metadata —- */
  3. “update_url”: “”,
  4. “manifest_version”: 2,
  5. “name”: “Google Meet Controls”,
  6. “short_name”: “Meet Controls”,
  7. “description”: “Control your microphone manners for Meet”,
  8. “version”: “1.0”,
  9. “icons”: {
  10. “128”: “notmuted.png”
  11. },
  12. /* —- Inject our Meet content script into all Google Meetings, based on URL —- */
  13. “content_scripts”: [{
  14. “matches”: [“*”],
  15. “run_at”: “document_start”,
  16. “js”: [“meet_controller.js”]
  17. }],
  18. /* —- Let Chrome know that we have the following background script(s) that should run —- */
  19. “background”: {
  20. “scripts”: [“keyboard_listener.js”],
  21. “persistent”: false
  22. },
  23. /* —- This extension accesses the chrome.tabs and chrome.notifications APIs —- */
  24. “permissions”: [
  25. “tabs”,
  26. “notifications”
  27. ],
  28. /* —- Hotkeys that we want Chrome to listen for and pass to this extension —- */
  29. “commands”: {
  30. “toggle”: {
  31. “description”: “Toggle mute”,
  32. “global”: true,
  33. “suggested_key”: {
  34. “default”: “Ctrl+Shift+T”,
  35. “mac”: “MacCtrl+Shift+T”
  36. }
  37. },
  38. }
  39. }


This is known in Chrome Extension parlance as an Event Page. Event Pages listen for events from the various Chrome APIs, and do something in response. In this case, it listens for the hotkey command events that are identified in manifest.json (e.g., “toggle“). When it receives  this command, it responds by sends a message to every tab running Google Meet, indicating that it is time to toggle the mute.

  1. // —- listen for the keyboard shortcut to be pressed
  2. chrome.commands.onCommand.addListener(command => {
  3. // —- we want to find all Chrome tabs that have Google Meet open
  4. chrome.tabs.query({url: ‘*’}, tabs => {
  5. tabs.forEach(tab => {
  6. // —- callback that is invoked by the recipient of the message that we will send next.
  7. // —- we expect the recipient to give us some text present in a notification bubble.
  8. const responseHandler = (response) => {
  9. chrome.notifications.create(undefined, response.notification,
  10. function(notificationId) {}
  11. )
  12. }
  13. // —- send each Meet tab a message, which is the hotkey command from manifest.json.
  14. // —- this will always be “toggle”, since that’s the only one we defined.
  15. chrome.tabs.sendMessage(, {command: command}, responseHandler)
  16. })
  17. })
  18. })

Here, when we pass the message to each tab, we chose to include the command data string — you can use this argument to pass arbitrary information to the Content Script. You would of course modify the Content Script to use that new data.


This is known in Chrome Extension parlance as a Content Script. Content Scripts are actually injected into the web page, but are isolated from all other scripts on the page, including background scripts. This gives Content Scripts access to the DOM. In our extension, this 1) will listen for a message from the background script, 2) toggle the Meet mute on the page by issuing a click event to the mute button.

1. Add an event listener to listen for the message coming from keyboard_listener.js.  When this occurs, we look for the operative mute or unmute buttons on the page.

  1. “use strict”;
  2. chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  3. var icon = ‘notmuted.png’ // default icon for notification
  4. var message = ‘Could not locate Mute control in Google Meeting.’ // default notification message
  5. // find mute or unmute button on this Meeting page. only one of these will exist at a time
  6. const muteButton = document.querySelector(“[aria-label=’Turn off microphone’]”)
  7. const unmuteButton = document.querySelector(“[aria-label=’Turn on microphone’]”)

2. Next, we’ll build methods to actually perform the mute and unmute. Fo

  1. Click the mute button.
  2. Send a response back to the message sender (in keyboard_listener.js). This includes an object that describes the notification that we want the sender to display to the user for us. We do this because we do not have access to the chrome.notifications API from a Content script.
  1. // function for muting
  2. const mute = () => {
  3. const btn = muteButton
  4. if (btn !== null) {
  6. message = ‘Microphone is OFF’
  7. icon = ‘muted.png’
  8. sendResponse({
  9. notification:
  10. {
  11. type: ‘basic’,
  12. iconUrl: icon,
  13. title: message,
  14. message: ”
  15. }
  16. })
  17. }
  18. }

3. Basically the same thing for unmute.

  1. // function for unmuting
  2. const unmute = () => {
  3. const btn = unmuteButton
  4. if (btn !== null) {
  6. message = ‘Microphone is ON’
  7. icon = ‘notmuted.png’
  8. sendResponse({
  9. notification:
  10. {
  11. type: ‘basic’,
  12. iconUrl: icon,
  13. title: message,
  14. message: ”
  15. }
  16. })
  17. }
  18. }

4. Finally, now that we have references to the buttons, and methods that do the “heavy lifting” of clicking them and sending an appropriate message to meet_controller.js, the logic is simple.

  1. if (request.command === ‘toggle’) {
  2. if (muteButton !== null) { // if the mute button exists, then the Mic is currently unmuted.
  3. mute()
  4. }
  5. else { // … and vice-versa.
  6. unmute()
  7. }
  8. }
  9. })

You might be wondering: why not do all of this directly from keyboard_listener.js, rather than passing messages around? Great question! It has to do with Chrome’s security model. Event Pages do not have access to the DOM, or any details about the page content. They do, however, have access to all of the Chrome APIs, like the Notifications and Commands API that we use in this example. Content Scripts on the other hand, do not have access to all of the Chrome APIs (at least the ones we care about here), but do have access to the page content. This is why we must use message passing between the 2 scripts.

All done!

In Part 2 of this series, we will show you how to install the extension we wrote above, and set up the global keyboard shortcuts so that it is actually useful.

Home Stretch! Package it and install.

In a later post, we will show you how to host your extension on the Chrome Web Store for installation from there. For now though, we will simply load the extension locally.

  1. Navigate to chrome://extensions/
  2. Turn on Developer Mode
  3. Load unpacked extension by locating the directory where your extension files are saved