Scripting based on active zoom or Teams or Webex meeting

I'm wondering if there is a way to have a given trigger do different actions based on whether a on-line meeting is active or not. i.e. I'd like to "Mute" if a meeting is on, and "Play/Pause" if not. There are 'Conditions' for active window/running apps, but the meeting may be in the background. MS Teams has chat windows, so there's always a running process, similar with zoom.

FWIW- my trigger is an old Griffen PowerMate, configured based on this thread.

I've been doing this recently:

  1. Python script that

    • runs a shell script query (pmset -g) every N seconds
    • Checks the output to see if the "meeting state" has changed
    • If the meeting state has changed, print some status information and update the BTT variable "meeting_app" so that its value is one of '', 'MSTeams' 'Webex', 'zoom.us', where the empty value indicates no meeting in progress.
  2. BTT Trigger on Variable Value did change / variable "meeting_app".

    • Using this to run actions that change the set of buttons visible on my StreamDeck.

This are lots of ways to improve this*, but it does reliably work to change the streamdeck buttons soon after a meeting starts, and change them back when the meeting finishes. I'm on Sonoma 14.6 and have not tested this on other OS versions. The frequency with which you run pmset -g is related to how long this takes to notice that a meeting has started/stopped, and how much extra load you want to put on your mac in doing the monitoring. I found 5 sec to be reasonable.

I'd be happy to have some discussion about how other people are doing this; I found the pmset -g method by doing a lot of googling.

(*for instance, I'm just starting the py script in a terminal window rather than running it as a daemon. It dies occasionally for reasons I haven't tried to figure out yet, then I just restart it...)

The "upload" button doesn't seem to allow me to upload a python script, so I'll paste it in below...

# TODO add shebang
# meetingMonitor.py

# code to check whether we are in a Teams or Webex or Zoom Meeting

import re, argparse, subprocess, time

sleep_CRE = re.compile('sleep prevented by ([^()]+)')

# return the meeting app preventing sleep or None
def check_status():
    #output = subprocess.check_output(['pmset', '-g']
    # pmset -g for Teams:   sleep                0 (sleep prevented by MSTeams, ...
    # pmset -g for zoom :   displaysleep         180 (display sleep prevented by zoom.us)
    # Note, this is a separate line from ' sleep'  (zoom only shows up in this line on Sonoma 14.6)
    #pmsetCmd = 'pmset -g | grep "^ sleep"'
    # Note, will return several lines
    pmsetCmd = 'pmset -g | grep "^ [[:alpha:]]*sleep"'
    output = subprocess.check_output(pmsetCmd, shell=True, encoding='UTF-8')
    # The output of this will be more than one line
    for line in output.splitlines():
        m = sleep_CRE.search(line)
        if m is not None:
            # m.group(1) will be the list of things preventing sleep, i.e. : 'coreaudiod, WebexHelper'
            unsleepers = m.group(1)
            for meetingApp in ['MSTeams', 'Webex', 'zoom.us']:
                if meetingApp in unsleepers:
                    return meetingApp
    return None                                

# TODO maybe notify btt of the app the first time through the loop
def notify_btt(meetingApp=''):
    if meetingApp is None:
        meetingApp = ''
    btt_args = ['open']
    btt_url = f'"btt://set_persistent_string_variable/?variableName=meeting_app&to={meetingApp}"'
    btt_args.append(btt_url)
    if True:
        print(f'[notify btt subproc args] {btt_args}')
    try:
        btt_args = " ".join(btt_args)
        subprocess.check_call(btt_args, shell=True)
    except:
        # TODO, what to do on fail, for instance, if btt not open?  TEST?
        print('unable to update btt with meeting status')

def monitor_status(interval_sec=10, print_updates=True):
    was_meeting = False
    while True:
        meetingApp = check_status()
        is_meeting = meetingApp is not None
        if was_meeting != is_meeting:
            if print_updates:
                print(f'Now Meeting [{is_meeting}] in app {meetingApp}')
            was_meeting = is_meeting
            notify_btt(meetingApp)
        time.sleep(interval_sec) 

if __name__ == '__main__':
    parser = argparse.ArgumentParser(usage='MacOS monitor status of MSTeams and Webex and Zoom meetings')
    # TODO maybe add print/noprint option
    parser.add_argument('-i', '--interval', help='polling interval in sec', default=5, type=int)
    args = parser.parse_args()

    interval_sec = args.interval
    if interval_sec < 1:
        interval_sec = 1
    try:
        monitor_status(interval_sec)
    except KeyboardInterrupt:
        # OK, user pressed ctrl-c, clean exit
        print('  Done.')
1 Like

thanks so much for sharing this. I'll give it a go.

Let me know how it works for you - I'd be happy to get improvements that I can use. In case it is helpful, I am using Python 3.11.

I've started experimenting with this for Google Meet also. The following new version contains some slightly improved error handling, and also will detect a meeting in Google Meet, but so far I don't see a way to tell the difference between watching a video in Chrome and using Google Meet, so it triggers on both. This isn't a problem for me, as I don't tend to use Chrome for browsing, but might be for others. If you find a solution to this please let me know.

# TODO add shebang
# meetingMonitor.py

# code to check whether we are in a Teams or Webex or Zoom Meeting

import re, argparse, subprocess, time, traceback

sleep_CRE = re.compile('sleep prevented by ([^()]+)')

# return the meeting app preventing sleep or None
def check_status():
    #output = subprocess.check_output(['pmset', '-g']
    # pmset -g for Teams:   sleep                0 (sleep prevented by MSTeams, ...
    # pmset -g for zoom :   displaysleep         180 (display sleep prevented by zoom.us)
    # pmset -g for Google Meet : 180 (display sleep prevented by Google Chrome, ...
    #   also    0 (sleep prevented by ..., Google Chrome)
    # Note, this is a separate line from ' sleep'  (zoom only shows up in this line on Sonoma 14.6)
    # Google Meet is a bit difficult if you are also trying to use Chrome for browsing videos (ex: youtube)
    #  because this also shows up as 'Google Chrome' in the sleep prevented by list.
    #  So far I don't see a way to figure out if it is a GoogleMeet meeting or a video.
    # Note, this command will return several lines
    pmsetCmd = 'pmset -g | grep "^ [[:alpha:]]*sleep"'
    output = subprocess.check_output(pmsetCmd, shell=True, encoding='UTF-8')
    # The output of this will be more than one line
    for line in output.splitlines():
        m = sleep_CRE.search(line)
        if m is not None:
            # m.group(1) will be the list of things preventing sleep, i.e. : 'coreaudiod, WebexHelper'
            unsleepers = m.group(1)
            for meetingApp in ['MSTeams', 'Webex', 'zoom.us', 'Google Chrome']:
                if meetingApp in unsleepers:
                    # special case, unclear how to differentiate 
                    if meetingApp.startswith('Google'):
                        return 'GoogleMeet' # special case to remove the space
                    return meetingApp
    return None                                

# TODO maybe notify btt of the app the first time through the loop
# returns False if there was an exception notifying btt, True otherwise
def notify_btt(meetingApp='', verbosity=0):
    if meetingApp is None:
        meetingApp = ''
    btt_args = ['open']
    btt_url = f'"btt://set_persistent_string_variable/?variableName=meeting_app&to={meetingApp}"'
    btt_args.append(btt_url)
    if verbosity > 0:
        print(f'[notify btt subproc args] {btt_args}')
    try:
        btt_args = " ".join(btt_args)
        subprocess.check_call(btt_args, shell=True)
    except:
        # TODO, what to do on fail, for instance, if btt not open?  TEST?
        print('unable to update btt with meeting status')
        return False
    return True

def monitor_status(interval_sec=10, print_updates=True, verbosity=0):
    was_meeting = False
    while True:
        meetingApp = check_status()
        is_meeting = meetingApp is not None
        if was_meeting != is_meeting:
            if print_updates:
                print(f'Now Meeting [{is_meeting}] in app {meetingApp}')
            was_meeting = is_meeting
            notify_btt(meetingApp, verbosity=verbosity)
        time.sleep(interval_sec) 

if __name__ == '__main__':
    parser = argparse.ArgumentParser(usage='MacOS monitor status of MSTeams and Webex and Zoom meetings')
    # TODO maybe add print/noprint option
    parser.add_argument('-i', '--interval', help='polling interval in sec', default=5, type=int)
    parser.add_argument('-v', help='increase verbosity', default=0, action='count')
    args = parser.parse_args()

    interval_sec = args.interval
    if interval_sec < 1:
        interval_sec = 1
    user_interrupt = False
    while not user_interrupt:
        try:
            monitor_status(interval_sec, verbosity=args.v)
        except KeyboardInterrupt:
            # OK, user pressed ctrl-c, clean exit
            print('  Done.')
            user_interrupt = True
        except RuntimeError as err:
            print('RuntimeError: ', err)
            traceback.print_tb(err.__traceback__)
        except:
            raise
    

Re checking the status of GoogleMeet, I think maybe checking the URL of the active page in javascript could be a way to tell the difference between a google meeting and anything else, something like this:

(assuming that BTT variable meeting_app was already set using the above python script, and that you already retrieved its value using javascript)

if (meeting_app === "GoogleMeet") {
	gmURL = "https://meet.google.com/"
	gmLandingURL = "https://meet.google.com/landing"
	let activeURL = await get_string_variable("active_website_url");
	if (activeURL.startsWith(gmURL) && (! activeURL.startsWith(gmLandingURL))) {
		// detected a google meeting, do something here
	} else {
               // not a google meeting, do something else
        }
}

1 Like

Here's what I did, using the pmset nugget.
I just created an AppleScript that runs as part of the action-

set pmsetOutput to do shell script "pmset -g"

set resultText to "playPauseAudio"

if pmsetOutput contains "MSTeams" then

set resultText to "muteMicrophone"

end if

if pmsetOutput contains "zoom.us" then

set resultText to "muteMicrophone"

end if

if pmsetOutput contains "Webex" then

set resultText to "muteMicrophone"

end if

tell application "BetterTouchTool"

set_persistent_string_variable "conditionalButtonAction" to resultText

end tell

and I use the persistent_string_variable in the IF logic.

I don't use GoogleMeet much...

This is working, unless these apps end up populating pmset for other reasons....
Now the proplem is getting the apps to pay attention to the keyboard shortcut when they are in the background...

In the "Send Shortcut to Specific App" config, there are two checkboxes near the bottom:

  • Bring app to front before sending
  • Immediately switch back after sending

I found that in general I didn't need to use these for Teams (even when the app didn't have focus) but that I needed to turn these on for Webex.