Using the Zoom API to deal with webinar panelists

In response to the pandemic my school is using Zoom Webinars for various performances. Our student Improv group has done a number of performances. The cast all need to be “panelists” in Zoom parlance, and when you duplicate a Webinar it does not duplicate the panelists. That’s a pain since there are a lot of panelists, and for each you need a name and e-mail address.

I finally got tired of dealing with it and spent a few minutes figuring out the Zoom API. If I ever have a little more time, I’ll make a Google Apps Manager (GAM) knock off that I’ll call ZAM! In the meantime, perhaps someone else will find this helpful.

First, Zoom has good documentation of its API. To start, you will need to build an app to get credentials. For my scripts, I’m using JSON Web Tokens (JWT) for authentication. I found this post in the Zoom Dev Forums helpful as a starting point.

Get information on a Zoom user

Here is a short Python script to get information on a user to give a little idea on how to use the API.

import jwt   # pip3 install pyjwt --user
import http.client
import datetime
import json
import sys

api_key = '********' # replace with credentials from
api_sec = '********' # your app in Zoom Marketplace

# generate JWT
payload = {
'iss': api_key,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2)
}

jwt_encoded = str(jwt.encode(payload, api_sec), 'utf-8')


# call API: get user list
conn = http.client.HTTPSConnection("api.zoom.us")
headers = {
'authorization': "Bearer %s" % jwt_encoded,
'content-type': "application/json"
}
email = str(sys.argv[1])
conn.request("GET", "/v2/users/" +  email, headers=headers)
res = conn.getresponse()
response_string = res.read().decode('utf-8')

user_json = json.loads(response_string)
json_formatted_str = json.dumps(user_json, indent=2)
print(json_formatted_str)

You can use this like this:

# python3.7 zoom-user.py user@domain.com

{
  "id": "xxxxxxxxxxx",
  "first_name": "John",
  "last_name": "Smith",
  "email": "user@domain.com",
  "type": 2,
  "role_name": "Admin",
  "pmi": 111111111,
  "use_pmi": false,
  "personal_meeting_url": "https://myschool.zoom.us/j/111111111?pwd=UmVTASDFc3pXUTNpOFZNb34A09",
  "timezone": "America/Chicago",
  "verified": 0,
  "dept": "",
  "created_at": "2020-04-13T02:42:31Z",
  "last_login_time": "2020-05-22T19:38:06Z",
  "last_client_version": "4.6.20561.0413(mac)",
  "pic_url": "https://lh3.googleusercontent.com/a-/AOh14GgQadfagrA8l4U9fhsbZ1hOTMSvDxE3gGQ",
  "host_key": "111861",
  "jid": "3timqwabrvs00ayurhg_ha@xmpp.zoom.us",
  "group_ids": [
    "qCpDwoo5T1qS4cn8RSy8rQ"
  ],
  "im_group_ids": [],
  "account_id": "fhsbZ1cOTMSvDxE3g",
  "language": "en-US",
  "phone_country": "",
  "phone_number": "",
  "status": "active",
  "job_title": "",
  "location": ""
}

Note: I’ve redacted/change all of the ID and info above for obvious reasons.

Getting a list of all webinars panelists

This Python script lists all of the panelist names and email addresses.

import jwt   # pip3 install pyjwt --user
import http.client
import datetime
import json
import sys

api_key = '********' # replace with credentials from
api_sec = '********' # your app in Zoom Marketplace

# generate JWT
payload = {
'iss': api_key,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2)
}

jwt_encoded = str(jwt.encode(payload, api_sec), 'utf-8')

# call API: get webinar panelists
conn = http.client.HTTPSConnection("api.zoom.us")
headers = {
'authorization': "Bearer %s" % jwt_encoded,
'content-type': "application/json"
}
webinarid = str(sys.argv[1])
conn.request("GET", "/v2/webinars/" + webinarid + "/panelists", headers=headers)
res = conn.getresponse()
response_string = res.read().decode('utf-8')
response_json = json.loads(response_string)

for panelist in response_json["panelists"]:
    print(panelist["name"],  panelist["email"])

Here is an example of the script in action listing panelists for webinar id 91119118113:

# python3.7 zoom-webinar-panelists.py 91119118113
John Smith jsmith@domain.com
Jane Smith jane.smith@anotherdomain.com

Adding Panelists to a Webinar

Next is a Python script to add a panelist to a webinar. It takes the webinar ID, panelist name and email address as parameters.

import jwt   # pip3 install pyjwt --user
import http.client
import datetime
import json
import sys

api_key = '********' # replace with credentials from
api_sec = '********' # your app in Zoom Marketplace

# generate JWT
payload = {
'iss': api_key,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=2)
}

jwt_encoded = str(jwt.encode(payload, api_sec), 'utf-8')

# call API: add panelist
conn = http.client.HTTPSConnection("api.zoom.us")
headers = {
'authorization': "Bearer %s" % jwt_encoded,
'content-type': "application/json"
}

num_arguments = len(sys.argv)-1
webinar = str(sys.argv[1])
email = sys.argv[num_arguments]
name = ""
i = 2
while i < num_arguments:
    name = name + sys.argv[i] + " "
    i = i+1
name = name.rstrip()
print(name, email,webinar)
body = "{\"panelists\":[{\"name\":\"" + name + "\",\"email\":\"" + email + "\"}]}"

webinar = str(sys.argv[1])
conn.request("POST", "/v2/webinars/" + webinar + "/panelists", body, headers=headers)
res = conn.getresponse()
response_string = res.read().decode('utf-8')

Here is an exampling of using the script to add “John Q. Public” john.q.public@domain.com to webinar id 91119118113:

python3.7 zoom-webinar-panelists-add.py 91119118113 John Q. Public john.q.public@domain.com

{"id":"91119118113","updated_at":"2020-05-25T23:25:22Z"}

Closing Thoughts

Using these ideas in these two scripts, I can easily copy panelists from one webinar to another. Hopefully this is enough to get you started using the Zoom API. As I mentioned at the start, I think it would be awesome to have a tool like GAM but for Zoom. I’d much rather manage Zoom from the command line.

Cropping PDFs with AppleScript

Due to Coronavirus the printing of our yearbooks was delayed, so we decided to distribute it digitally until the print version was available. There are lots of tools, like ISSUU, Uberflip, or even Google Drive that can do a decent job of displaying a PDF, password protecting it, and preventing it from being downloaded. The challenge was to get the galley proofs with crop marks, etc. into the right format. Further complicating things were that the odd/even pages had crop markets that were offset, so they had to be cropped differently.

Here is an example of a page with the dotted crop marks and the metadata on the bottom.

Fortunately Adobe Acrobat is scriptable. Normally, I tend to avoid installing Acrobat, in favor of using Preview, but its support for scripting is surprisingly good. The follow AppleScript crops odd/even pages differently and solved my problem. (based on https://macscripter.net/viewtopic.php?id=32586)

tell application "Adobe Acrobat"
activate tell active doc repeat with i from 1 to count of pages tell page i set {L, T, R, B} to media box if ((i mod 2) = 0) then set crop box to {L + 37, T - 39, R, B + 156} else set crop box to {L, T - 39, R - 37, B + 156} end if end tell end repeat end tell end tell

Virtual Graduations & End of Year Events in the time or Coronavirus: Stream Deck

In a previous post, I mentioned that I used Wirecast to do live video production of events I broadcast using Zoom. Normally, I’d like to have someone helping me, but that’s much harder when everyone is remote. Fortunately, I found the Elgato StreamDeck to help. It’s a USB hardware device with a bunch of buttons. Each button can trigger an action. Each button acts as a little screen that can be customized. There is a rich ecosystem of plugins for Strem Deck that make it work with other tools.

Here is what it looks like from a recent production:

Most of the blue buttons are clips in Wirecast and the bottom left button is the “play” button for Wirecast. The little red dot on the top left clip shows that it is live. A green dot on another clip shows that it is “next.” I really like the Wirecast plugin for Stream Deck since it gives feedback on what is playing (the red/green dots).

On the bottom right are five buttons that control Zoom (focus/ turn on/off webcam, turn on/off mic, show participants window, show chat window). They are based on the work of LostDomain and make use of Keyboard Maestro, and it’s plug-in for StreamDeck.

The black buttons are for when we went live and I muted the virtual webcam so that the active speaker became the live speaker.

What is nice is that I can control Wirecast while it is in the background and I’m managing Zoom.

Here is another picture of the StreamDeck from another production:

For this production I didn’t use the virtual webcam and instead played videos in QuickTime. The video quality seemed to be a bit better when sharing my screen rather than using a virtual webcam, and video quality was really important for this production.

The two columns of buttons on the right are mostly to control Zoom (except the bottom two). I added a shortcut to the “Share” button and “Record” (again, making use of Keyboard Maestro). The bottom right is a clock that shows the time (including seconds).

All of the other buttons are AppleScripts to control QuickTime Player and Preview. One of the challenges of using QuickTime to play videos in front of an audience (Zoom or in-person) is that they can see you futzing around opening the video and going full-screen. Using AppleScript can really minimize this, especially when triggering it from the StreamDeck.

Here is a little AppleScript to open and play fullscreen a Quicktime video (based on https://gist.github.com/biojazzard/2829190):

set unixpath to "/Users/ssimon/Desktop/PA Night Long.mp4"
set macfile to (POSIX file unixpath)
tell application "QuickTime Player"
	activate
	delay 0.5
	open file macfile
	set looping of document 1 to false
	--FullScreen
	--FullScreen
	--set presenting of document 1 to true
	--GetBounds
	present document 1
	play document 1
	
end tell

I also wanted to be able to pause the video:

tell application "QuickTime Player"
	activate
	delay 0.5
	pause document 1
end tell

And to play:

tell application "QuickTime Player"
	activate
	delay 0.5
	present document 1
	play document 1
end tell

I also wanted to close a video (QuickTime Player will open additional videos in tabs when in fullscreen):

tell application "QuickTime Player"
	activate
	close document 4
end tell

You can also open a document in Preview in fullscreen:

set unixpath to "/Users/ssimon/Desktop/roman.jpg"
set macfile to (POSIX file unixpath)

tell application "Preview"
	activate
	open file macfile
	tell application "System Events"
		keystroke "f" using {control down, command down}
	end tell
end tell

The end result is that nobody sees me moving my mouse around to control QuickTime Player or Zoom and I can rapidly play a video.

For the show, I launched the video (via a StreamDeck button / AppleScript), immediately paused it (via a StreamDeck button / AppleScript), used the Stream Deck “Share” button for Zoom to open the screen sharing window, picked the QuickTime Player window, checked “Optimize for full-screen video clip” and “Share computer sound”, and then the “Share Screen” button. Then I hit “play” (via a StreamDeck button / AppleScript). When the video was over, I stopped sharing my screen and let the speaker talk, and when he was done, I repeated the process all over for the next video.

In the future, I’ll probably change the AppleScript not to play the video and leave it paused, to simplify the process.

I haven’t had a chance to play with it, but there is a mobile version of the StreamDeck software for Android and iOS which looks like it could be a cheaper alternative to buying a StreamDeck.

 

Virtual Graduations & End of Year Events in the time or Coronavirus: Zoom

I’m the tech guy for a private K-12 school and have had to do a lot of work to pull off the various end of year events. Many other people have shared content that I’ve found helpful, and I hope in posting this someone else might find it useful. Mostly, I write these blogs for me, to document stuff and help get my thoughts together. I’m going to do a series of posts on the tools and techniques I’ve used. I’ll start with Zoom.

Despite privacy and security concerns, we have been using Zoom for many of our meetings and classes. Many of those concerns are addressed by paying for a Zoom for Education account which has a better privacy policy and security features.

Our licensed Zoom accounts support 300 attendees, but that isn’t enough for end-of-year events. I have upgraded my account to support 1,000 users and we also have a 500 user Webinar account. Zoom can also stream to an outside service like YouTube Live. We have invited students to join an event via Zoom, and parents, and other adults to use the YouTube Live stream.

Webinar vs. Meeting

The main differences between the more expensive Webinar version of Zoom and the regular version:

  1. Meeting attendees cannot share video or audio. You can enable users to unmute themselves, but by default attendees are passive participants.
  2. Webinars have a new role of “Panelist” who can share their audio, video (with permission), or screen. They can also use the chat feature to chat with all panelists, or all panelists and attendees.
  3. Webinar supports a Q&A function that enables attendees to ask questions and for panelists to answers those questions either publicly or privately.
  4. Chat in webinar has some different options, including allowing attendees to only post to attendees or everyone. Direct chat between individual participants is not allowed.
  5. Webinar allows you to force what view attendees see (host view, active speaker view, gallery/grid view).

We use the webinar version for our various performing arts shows. By setting “Set video layout for attendees” to “Follow host view mode” and setting “Hide Non-Video Participants” in the host’s Zoom application settings. This allows us to rapidly hide/show webcams of people we want on stage. Our improv groups used this to great effect, constantly changing who was on stage throughout the show.

For end of year events, if you want everyone to see each other then you should use the standard version of Zoom. If you want to control what attendees see, you should use Webinar.

Virtual Webcams

One very useful trick is to use virtual webcam software. This basically adds a fake webcam to your computer that you can use in Zoom. You can use the virtual webcam to play recorded video or output from live video production software. I’ve used this to mix recorded video into a Zoom without resorting to screen sharing (which has drawbacks, including really slow transitions when starting/stopping sharing, and that it takes over the entire screen).

I primarily use Telestream Wirecast for both live video production and to act as a virtual webcam. They have a tutorial on “Using Virtual Camera Out in a Zoom Meeting” which gives a pretty good overview of the process. I’ve also experimented with using ManyCam.

I’ve used Wirecast and a virtual webcam to have slides and video side-by-side in some end of year events. This allows me to make the speaker (in the video) the same size or larger than the slides.

WARNING: security features in macOS Catalina and newer versions of Zoom broke virtual webcam support. Apparently, there is a new version of Zoom that is supposed to come out today (5/22) that fixes this. Prior to that, your only choices were to downgrade to Zoom 4.6.x or to run the following from the command line:

codesign --remove-signature /Applications/zoom.us.app/

Streaming to YouTube Live

Sometimes it is really helpful to stream to more people than your Zoom account can support, provide an option for those who don’t want to use Zoom to attend, or to be able to embed a stream within a webpage. Zoom has documentation on how to “Streaming a Meeting or Webinar on YouTube Live.”

Note: you will have to enable your YouTube account for live streaming and it can take 24 hours for the change to go into effect.

For an event, you’ll probably want to know the URL of the stream ahead of time. For that you’ll have to enable Custom Live Streaming instead of using the built-in support for YouTube Live within Zoom.

WARNING: be careful in testing your setup between Zoom and YouTube Live. If you start streaming and then stop, you won’t be able to use that URL again. I now send out links to a landing page on our website instead of the direct link to YouTube. That way I can change the link at the last second if I need to.