Integrating Zoom Meeting SDK into Blazor

Introduction

This blog demonstrates how to embed an instance of Zoom’s Meeting SDK Client View in a Blazor project. A similar approach can be used to embed Component View, but for simplicity, this guide focuses on Client View. It also highlights several useful configuration options, such as hiding meeting information. This blog is intended for developers with a basic understanding of modern C# practices, including building a simple Blazor project.

Prerequisites

In order to build the sample project, you will need the following:

Please note the most current Meeting SDK - you will not need to download it, though. Also note that you may develop on Windows, macOS, or Linux. My original use case was developed using Rider on macOS and deployed on Linux.

Development

Create Project

To begin, start your IDE (I am using Rider) and follow its instructions for creating a Blazor Web App in .NET 10.

Name the project "BlazorMeetingApp" and take the defaults, including creating sample pages. Once done, run the project. Your browser should open, displaying something like this:

BlazorMeetingApp

There are three pages on this app:

  • Home page
  • Counter page (with a button that increments a value)
  • Weather page (with random data).

We are going to add another page, containing the Zoom meeting.

Add Credentials

Create a directory under the project directory called "Data". Within that directory, create a static class called "AppConstants" and add two static string properties, "ZoomClientID" and "ZoomClientSecret". It should look like this:

namespace BlazorMeetingApp.Data;
public static class AppConstants
{
   public static string? ZoomClientId { get; set; }
   public static string? ZoomClientSecret { get; set; }
}

We are going to place the ClientID and the ClientSecret in appsettings.json - not the most secure place to put credentials, to be sure, but this is a sample project. Open appsettings.json and insert them like this:

  "Authentication" : {
    "Zoom" : {
      "ClientID" : "your_client_ID",
      "ClientSecret" : "your_client_secret"
    }
  },

Now open program.cs and add the following code before app = builder.Build(); - don’t forget to reference AppConstants:

var config = builder.Configuration;
var zoomAuthSection = config.GetSection("Authentication:Zoom");
AppConstants.ZoomClientId = zoomAuthSection["ClientID"];
AppConstants.ZoomClientSecret = zoomAuthSection["ClientSecret"];

Obtaining the Zoom SDK

Open App.razor and insert the following into the <head> section of the file, before the <HeadOutlet/> tag:

<!-- Dependencies for client view and component view -->
    <script src="https://source.zoom.us/5.0.0/lib/vendor/react.min.js"></script>
    <script src="https://source.zoom.us/5.0.0/lib/vendor/react-dom.min.js"></script>
    <script src="https://source.zoom.us/5.0.0/lib/vendor/redux.min.js"></script>
    <script src="https://source.zoom.us/5.0.0/lib/vendor/redux-thunk.min.js"></script>
    <script src="https://source.zoom.us/5.0.0/lib/vendor/lodash.min.js"></script>
    <!-- CDN for client view -->
    <script src="https://source.zoom.us/5.0.0/zoom-meeting-5.0.0.min.js"></script>
    <script src="client-view.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/8.0.20/jsrsasign-all-min.js"></script>

Consult the Zoom Developer documentation for the most recent versions. We will be writing client-view.js in the next section.

Adding the JavaScript component

The Blazor integration uses the Web platform, rather than the Windows platform, in its integration. To do so, it uses its JS Interop capability to call JavaScript code to start and join the Zoom meeting. The code used is a customized version of the code found in the sample Web app on the Zoom repository and reads like the following:

sign()
ZoomMtg.preLoadWasm();
ZoomMtg.prepareWebSDK();
var leaveUrl = "https://zoom.us";
function sign(clientId, clientSecret, userName, meetingNumber, passWord, isHost) {
  const iat = Math.round(new Date().getTime() / 1000) - 30;
  const exp = iat + 60 * 60 * 2;
  const oHeader = { alg: 'HS256', typ: 'JWT' };
  const oPayload = {
    appKey: clientId,
    mn: meetingNumber,
    role: isHost,
    iat: iat,
    exp: exp,
    tokenExp: exp,
    video_webrtc_mode: 1
  };
  const sHeader = JSON.stringify(oHeader);
  const sPayload = JSON.stringify(oPayload);
  const meetingJwt = KJUR.jws.JWS.sign('HS256', sHeader, sPayload, clientSecret);
  startMeeting(meetingJwt, userName, meetingNumber, passWord);
}

The sign() function will be called when we want to enter a meeting. It creates a JWT (JSON Web Token), which is passed on to the startMeeting() function.

Of note in the parameters is isHost: a 1 in this variable indicates that this person is the host, while a 0 in this variable indicates a Participant. Video_webrtc_mode is optional but highly encouraged - this mode will help facilitate the various View modes (Gallery, Speaker, Multi-Speaker). Also, note that email address is not among the parameters. Including email will add Zoom authentication to the user’s experience. Excluding email will omit Zoom authentication, but will force all Participants to go through the Waiting Room.

startMeeting()
function startMeeting(signature, userName, meetingNumber, passWord) {
  document.getElementById("zmmtg-root");
  ZoomMtg.init({
    leaveUrl: leaveUrl,
    patchJsMedia: true,
    defaultView: "gallery",
    leaveOnPageUnload: true,
    showMeetingHeader: false,
    disableInvite: true,
    meetingInfo: [""],
    success: (success) => {
      ZoomMtg.join({
        signature: signature,
        meetingNumber: meetingNumber,
        passWord: passWord,
        userName: userName,
        success: (success) => {
          console.log(success);
          ZoomMtg.showInviteFunction({ show: false });
        },
        error: (error) => {
          console.log(error);
        },
      });
    },
    error: (error) => {
      console.log(error);
    },
  });
}

The startMeeting() function takes the previously generated JWT, username, meeting number, and password as parameters and starts (or joins) the requested meeting.

There are several optional settings in this use case. Of note are:

  1. disableInvite: if true, removes the invite link
  2. meetingInfo: if passed an empty array ([""]), all meeting information, including meeting number and password is removed
  3. ZoomMtg.showInviteFunction: when set to {show: false}, the Invite button in the Participants list is not shown.

These optional settings are important if your use case includes only making the meeting accessible from the web page. Otherwise, the meeting can easily be accessed from an external Zoom client.

This JavaScript code is to reside in one file, in this case called client-view.js, and should be placed in the wwwroot directory of the project. Because the file is named in a script tag in App.razor, it will be read at the beginning of program execution.

Adding the .razor page

Now we are ready to create the page that will contain the meeting itself. In your IDE, go to the Pages folder, and create a file within it called Meeting.razor. We are going to place several controls in this file to make this project more useful and flexible. The header of the markup section of the file should look like this:

@page "/meeting"
@rendermode InteractiveServer
@using System.ComponentModel.DataAnnotations
@using BlazorMeetingApp.Data;
@inject IJSRuntime JsRuntime
<PageTitle>Zoom Sample Meeting</PageTitle>
The rest of the markup section reads like this:
    <h1 class="text-center" style="font-size: 4vw;">Zoom Sample Meeting</h1>
    <EditForm Model="_mtgData" OnValidSubmit="@Click" FormName="MtgData">
        <DataAnnotationsValidator />
        <div class="row">
            <div class="col-3">
                &nbsp;
                <label>Select "Host" if meeting has not been started, otherwise select "Participant".</label>
                <div class="form-group">
                    <InputRadioGroup @bind-Value="@_mtgData.UserType">
                    <div class="form-check">
                        <InputRadio id="type-0" Value="0" class="form-check-input" />
                        <label for="type-0" class="form-check-label">Participant</label>
                        <InputRadio id="type-1" Value="1" class="form-check-input" />
                        <label for="type-1" class="form-check-label">Host</label>
                    </InputRadioGroup>
                </div>
                <hr/>
                <label>Meeting Data:</label>
                <label for="user">Your Name:</label>
                <InputText id="user" @bind-Value="@_mtgData.UserName"  class="w-50"/>
                <ValidationMessage For="() => _mtgData.UserName" />
                <label for="mtg">Meeting Number:</label>
                <InputText id="mtg" @bind-Value="@_mtgData.MeetingNumber"  class="w-50"/>
                <ValidationMessage For="() => _mtgData.MeetingNumber" />
                <label for="pass">Password:</label>
                <InputText id="pass" @bind-Value="@_mtgData.Password" class="w-50"/>
                <ValidationMessage For="() => _mtgData.Password" />
                <hr/>
            </div>
                &nbsp;
        </div>
    </EditForm>
    </div>
    <div id="aria-notify-area">
</div>
The only parts of this markup that are required are the button and the two  tags at the bottom. The button triggers the Zoom code and the two <div> tags connect the Zoom window to the page.
The code section of the file looks like this:
```csharp
@code {
    private readonly MeetingData _mtgData = new();
    private async Task Click()
    {
        await JsRuntime.InvokeVoidAsync("sign", AppConstants.ZoomClientId, AppConstants.ZoomClientSecret,
            _mtgData.UserName, _mtgData.MeetingNumber, _mtgData.Password, _mtgData.UserType);
    }
    public class MeetingData
    {
        [Range(0,1)]
        public int UserType { get; set; }
        [Required(ErrorMessage = "Name is required")]
        public string? UserName { get; set; }
        [Required(ErrorMessage = "Meeting Number is required")]
        [RegularExpression(@"^\d{10}$", ErrorMessage = "Meeting Number must be exactly 10 digits")]
        public string? MeetingNumber { get; set; }
        [Required(ErrorMessage = "Password is required")]
        [RegularExpression(@"^\d+$", ErrorMessage = "Password must be all digits")]
        public string? Password { get; set; }
    }
}

The class definition is part of the form created in the markup above - if you use a different mechanism to set these values, you don’t need it.

The Click() method is fired when the button is pressed on the page and is what starts the JavaScript code that starts the meeting. The call to InvokeViodAsync() takes the name of the JavaScript function to be executed and all of the arguments to that function, as documented above.

Preparing the menu

Finally, we create the menu option that invokes the Razor page that we have created. Open Layout/NavMenu.razor and add the following - it probably easiest to copy one of the existing menu options and modify it:

        <div class="nav-item px-3">
            <NavLink class="nav-link" href="meeting">
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Go to Zoom Meeting
            </NavLink>

One cosmetic item worth attending to is in Layout/MainLayout.razor: this step eliminates the horizontal menu, which covers up some of the Zoom controls. Comment out the following:

        @*  *@
        @*     <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> *@
        @*  *@

And that’s all for coding. Let’s look at the results.

Testing the results

Run the code. You should see the following:

BlazorMeetingAppReady

Click on the "Go to Zoom Meeting" menu option. You should see the following:

Zoom sample meeting

Fill in the blanks with your name, and a valid meeting number and password (your personal meeting number should work). Switch the radio button to "Host".

Zoom sample meeting ready

Click the "Join The Meeting" button, follow the instructions, and you should see yourself in a Zoom meeting.

in meeting screen

Deployment

Of course, a project involving Zoom will do little good if only one person is in the meeting. While more than one person can join the meeting using the standard Zoom client (with an appropriate meeting number and password), the ultimate goal would be to deploy the code onto a server. As with most .NET projects, this is done with the dotnet publish command, copying the results to the server of your choice.

Details on server deployment are beyond the scope of this article, but one point is worth mentioning. In order to get full browser compatibility and the full complement of features, one must set up SharedArrayBuffers in the browser.

This is done by setting the following two headers:

  • Cross-Origin-Opener-Policy: same-origin
  • Cross-Origin-Embedder-Policy: require-corp

These headers can be set in several places, depending on your server, but I’ve found it most useful to set them in program.cs. This ensures that the headers will be set, regardless of other deployment choices.

Conclusion

I hope you find this article useful. In a similar fashion, not only can one implement Component View in Meeting SDK, but also Video SDK. Enjoy!


This guest post was authored by an independent developer and does not constitute official guidance, documentation, or endorsement from Zoom Video Communications, Inc. Any code samples, integrations, or technical recommendations are provided as-is and for informational purposes only. Zoom makes no warranties, express or implied, regarding the content herein. The author may be a customer or developer partner of Zoom, but is not acting in any official capacity on behalf of Zoom.