Zoom Native SDK Integration for Flutter App via Method channel

Ashok
9 min readNov 28, 2023

--

Made by me

In this article, we are going to see — how we can add native zoom SDK into your flutter app. We will cover SDK initialisation, Join and Start meeting. Zoom screen share on iOS will be shared on my next article. I slipt this article into 4 sessions. Zoom Marketplace setup, Dart, iOS and Android implementations.

Personal credits 😆

Hey devs, I have already covered the Native iOS Zoom SDK Installation. Here the guide. You can check it out and come back here. Further here, we will cover Join and Start zoom meeting stuffs as told in the article.

Time is precious, but truth is more precious than time — Benjamin Disraeli

I’m not good at explaining the documentations. But, I have pretty good experience in implementing stuffs. So, this article will guide you to achieve the results. I’m suggesting you to read the stuffs and implement. Zoom have well written documentations. Hope you will read that before you get into this.

Zoom Marketplace Setup

You can create Zoom account here.

  1. After login into the marketplace, you have options to build your app
Build App

2. Create Meeting SDK app and fill all details

Crearte SDK app

3. After creating, you will get the SDK Key and SDK Secret. Copy and save it somewhere. We will make use of it later

Credentials

4. Setup Redirect-Url and add same URL in allow list. Im my case, it was:

ashokapp://com.ashok.zoomapp

All right. All setup was done in marketplace dashboard.

Dart

We are going to implement method channel for initiating native zoom SDK on the dart side. Just add the following methods in the place where you want to initiate the zoom meetings.

Method : _initializeZoomSDK we going to initialize zoom SDK


void _initializeZoomSDK() async {
const platformChannel = MethodChannel('zoom_sdk_channel');
try {
await platformChannel.invokeMethod('initializeZoomSDK');
debugPrint("Zoom SDK intialization status: $result");
} catch (e) {
debugPrint("$e");
}
}

Method : _joinMeeting via this method, we can join in the zoom meetings

 void _joingMeeting() async {
const platformChannel = MethodChannel('zoom_sdk_channel');
try {
await platformChannel.invokeMethod(
'joinMeeting',
{
"meetingID": "ZOOM-MEETING-ID",
"meetingPasscode": "ZOOM-MEETING-PASSCODE",
},
);
} catch (e) {
debugPrint("$e");
}
}

Method : _startMeeting via this method, we can join in the zoom meetings

 void _startMeeting() async {
const platformChannel = MethodChannel('zoom_sdk_channel');
try {
final version = await platformChannel.invokeMethod<bool>(
'startMeeting',
{
"meetingID": "ZOOM-MEETING-ID",
},
);
debugPrint("$version");
} catch (e) {
debugPrint("$e");
}
}

Thats it. These method can be wherever you want to initiate the zoom meetings in your flutter app.

iOS

Im going to share here the entire AppDelegate class for better understandings.

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
private var clientKey = "ZOOM-CLIENT-KEY"
private var clientSecret = "ZOOM-CLIENT-SECRET"
private var channelName = "zoom_sdk_channel"
private var meetingNumber = ""
private var appScheme = "ashokapp"
private var redirectURI = "ashokapp://com.ashok.zoom"
private var zoomJWTtoken = "ZOOM-JWT-TOKEN"
private let codeChallengeHelper = CodeChallengeHelper()

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let deviceChannel = FlutterMethodChannel(name: self.channelName,
binaryMessenger: controller.binaryMessenger)
prepareMethodHandler(deviceChannel: deviceChannel)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

private func prepareMethodHandler(deviceChannel: FlutterMethodChannel) {
deviceChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "initializeZoomSDK" {
self.inititalizeSDK(result: result)
}else if call.method == "joinMeeting" {
guard let args = call.arguments as? Dictionary<String, String> else { return }
let meetingNumber = args["meetingID"] ?? ""
let meetingPassword = args["meetingPasscode"] ?? ""
self.joinMeeting(meetingNumber: meetingNumber, meetingPassword: meetingPassword)
}else if call.method == "startMeeting" {
guard let args = call.arguments as? Dictionary<String, String> else { return }
self.meetingNumber = args["meetingID"] ?? ""
self.setUpZoomOAuthService()
}
else {
result(FlutterMethodNotImplemented)
return
}
})
}

private func inititalizeSDK(result: FlutterResult) {
self.initialiseSDK(result: result)
}

private func handleAuthResult(callbackUrl: URL?, error: Error?) {
guard let callbackUrl = callbackUrl else { return }
if (error == nil) {
guard let url = URLComponents(string: callbackUrl.absoluteString) else { return }
guard let code = url.queryItems?.first(where: { $0.name == "code" })?.value else { return }
self.requestAccessToken(code: code, codeChallengeHelper: self.codeChallengeHelper)
}
}
func getIOSDeviceID() -> String {
return UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
}

func getIOSRootController() -> UIViewController {
let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
let topController = (keyWindow?.rootViewController)!
return topController
}
func requestAccessToken(code: String, codeChallengeHelper: CodeChallengeHelper) {
guard let url = self.buildAccessTokenUrl(code: code, verifier: codeChallengeHelper.verifier) else { return }
guard let encoded = "\(clientKey):\(clientSecret)".data(using: .utf8)?.base64EncodedString() else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("Basic \(encoded)", forHTTPHeaderField: "Authorization")
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpBody = nil
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard self.checkRequestResult(data: data, response: response, error: error) else { return }

let response = try! JSONDecoder().decode(AccessTokenResponse.self, from: data!)
self.requestZak(accessToken: response.access_token)
}

task.resume()
}
private func buildAccessTokenUrl(code: String, verifier: String?) -> URL? {
var urlComp = URLComponents()
urlComp.scheme = "https"
urlComp.host = "zoom.us"
urlComp.path = "/oauth/token"
let grantType = URLQueryItem(name: "grant_type", value: "authorization_code")
let code = URLQueryItem(name: "code", value: code)
let redirectUri = URLQueryItem(name: "redirect_uri", value: self.redirectURI)
let codeVerifier = URLQueryItem(name: "code_verifier", value: verifier)
let params = [grantType, code, redirectUri, codeVerifier]
urlComp.queryItems = params
return urlComp.url
}
private func requestZak(accessToken: String) {
var urlComp = URLComponents()
urlComp.scheme = "https"
urlComp.host = "api.zoom.us"
urlComp.path = "/v2/users/me/token"

let tokenType = URLQueryItem(name: "type", value: "zak")
urlComp.queryItems = [tokenType]
guard let url = urlComp.url else { return }
var request = URLRequest(url: url)
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard self.checkRequestResult(data: data, response: response, error: error) else { return }
let response = try! JSONDecoder().decode(ZakResponse.self, from: data!)
DispatchQueue.main.async {
self.startMeeting(zak: response.token)
}
}
task.resume()
}
private func checkRequestResult(data: Data?, response: URLResponse?, error: Error?) -> Bool {
if error != nil { return false }
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { return false }
guard data != nil else { return false }
return true
}
}

extension AppDelegate{

func initialiseSDK(result: FlutterResult) {
let context = MobileRTCSDKInitContext()
context.domain = "zoom.us"
context.enableLog = false
context.appGroupId = self.appGroupID
let sdkInitializedSuccessfully = MobileRTC.shared().initialize(context)
if sdkInitializedSuccessfully == true, let authorizationService = MobileRTC.shared().getAuthService() {
authorizationService.delegate = self
authorizationService.jwtToken = self.zoomJWTtoken
authorizationService.sdkAuth()
result(true)
}
}

func joinMeeting(meetingNumber: String, meetingPassword: String) {
MobileRTC.shared().setMobileRTCRootController(UIApplication.shared.keyWindow?.rootViewController?.navigationController)
if let meetingService = MobileRTC.shared().getMeetingService() {
meetingService.delegate = self
let joinMeetingParameters = MobileRTCMeetingJoinParam()
joinMeetingParameters.meetingNumber = meetingNumber
joinMeetingParameters.password = meetingPassword
meetingService.joinMeeting(with: joinMeetingParameters)
}
}

func setUpZoomOAuthService(){
codeChallengeHelper.createCodeVerifier()
guard var oauthUrlComp = URLComponents(string: "https://zoom.us/oauth/authorize") else { return }
let codeChallenge = URLQueryItem(name: "code_challenge", value: codeChallengeHelper.getCodeChallenge())
let codeChallengeMethod = URLQueryItem(name: "code_challenge_method", value: "S256")
let responseType = URLQueryItem(name: "response_type", value: "code")
let clientId = URLQueryItem(name: "client_id", value: self.clientKey)
let redirectUri = URLQueryItem(name: "redirect_uri", value: self.redirectURI)
oauthUrlComp.queryItems = [responseType, clientId, redirectUri, codeChallenge, codeChallengeMethod]
guard let oauthUrl = oauthUrlComp.url else { return }
let session = ASWebAuthenticationSession(url: oauthUrl, callbackURLScheme: self.appScheme) { callbackUrl, error in
self.handleAuthResult(callbackUrl: callbackUrl, error: error)
}
session.presentationContextProvider = self
session.start()
}

private func startMeeting(zak: String) {
MobileRTC.shared().getMeetingSettings()?.meetingInviteUrlHidden = true
MobileRTC.shared().setMobileRTCRootController(UIApplication.shared.keyWindow?.rootViewController?.navigationController)
let startParams = MobileRTCMeetingStartParam4WithoutLoginUser()
startParams.zak = zak
startParams.meetingNumber = self.meetingNumber
if let meetingService = MobileRTC.shared().getMeetingService() {
meetingService.delegate = self
meetingService.startMeeting(with: startParams)
}
}
}

extension AppDelegate: MobileRTCAuthDelegate {

public func onMobileRTCAuthReturn(_ returnValue: MobileRTCAuthError) {
switch returnValue {
case MobileRTCAuthError.success:
print("SDK successfully initialized.")
case MobileRTCAuthError.keyOrSecretEmpty:
print("SDK Key/Secret was not provided. Replace sdkKey and sdkSecret at the top of this file with your SDK Key/Secret.")
case MobileRTCAuthError.keyOrSecretWrong, MobileRTCAuthError.unknown:
print("SDK Key/Secret is not valid.")
default:
print("SDK Authorization failed with MobileRTCAuthError: \(returnValue).")
}
}

private func onMobileRTCLoginReturn(_ returnValue: Int) {
switch returnValue {
case 0:
print("Successfully logged in")
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "userLoggedIn"), object: nil)
case 1002:
print("Password incorrect")
default:
print("Could not log in. Error code: \(returnValue)")
}
}
public func onMobileRTCLogoutReturn(_ returnValue: Int) {
switch returnValue {
case 0:
print("Successfully logged out")
NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "userLoggedIn"), object: nil)
default:
print("Could not log out. Error code: \(returnValue)")
}
}

public override func applicationWillTerminate(_ application: UIApplication) {
if let authorizationService = MobileRTC.shared().getAuthService() {
authorizationService.logoutRTC()
}
}
}

extension AppDelegate : ASWebAuthenticationPresentationContextProviding{
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) ?? UIApplication.shared.windows.first
return keyWindow!
}
}
extension AppDelegate: MobileRTCMeetingServiceDelegate {

public func onMeetingError(_ error: MobileRTCMeetError, message: String?) {
switch error {
case MobileRTCMeetError.passwordError:
print("MobileRTCMeeting : Could not join or start meeting because the meeting password was incorrect.")
default:
print("MobileRTCMeeting : Could not join or start meeting with MobileRTCMeetError: \(error) \(message ?? "")")
}
}

public func onJoinMeetingConfirmed() {
print("MobileRTCMeeting : Join meeting confirmed.")
}

public func onMeetingStateChange(_ state: MobileRTCMeetingState) {
print("MobileRTCMeeting : Current meeting state: \(state.rawValue)")
switch state{

case .idle:
print("idle")
case .connecting:
print("connecting")

case .waitingForHost:
print("waitingForHost")

case .inMeeting:
print("inMeeting")

case .disconnecting:
print("disconnecting")

case .reconnecting:
print("reconnecting")

case .failed:
print("failed")

case .ended:
print("ended")

case .locked:
print("locked")

case .unlocked:
print("unlocked")

case .inWaitingRoom:
print("inWaitingRoom")

case .webinarPromote:
print("webinarPromote")

case .webinarDePromote:
print("webinarDePromote")

case .joinBO:
print("joinBO")

case .leaveBO:
print("leaveBO")

@unknown default:
break
}
}
}

extension Date {
func adding(minutes: Int) -> Date {
return Calendar.current.date(byAdding: .minute, value: minutes, to: self)!
}
}
private struct ZakResponse : Decodable {
public let token: String
}
private struct AccessTokenResponse : Decodable {
public let access_token: String
}
class CodeChallengeHelper {

var verifier: String? = nil

func createCodeVerifier() {
var buffer = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
verifier = Data(buffer).base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
.trimmingCharacters(in: .whitespaces)
}

func getCodeChallenge() -> String {
guard let data = verifier?.data(using: .ascii) else { return "" }
var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0, CC_LONG(data.count), &buffer)
}
let hash = Data(buffer)
let challenge = hash.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
.trimmingCharacters(in: .whitespaces)
return challenge
}
}

Android

For installing zoom android SDK, You can follow this youtube tutorial. Andy Tuits was made wonderful youtube video for setting up the MobileRTC SDK for Android

Zoom Android SDK Installation Guide by Andy Tuits

Once after installing the SDK, just add this code below in your MainActivity.

The purpose of our lives is to be happy — Dalai Lama 😄

Here also, I’m going to share the entire MainActivity class for better understandings. :

package com.example.zoom_app

import android.util.Log
import android.widget.Toast
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import us.zoom.sdk.JoinMeetingOptions
import us.zoom.sdk.JoinMeetingParams
import us.zoom.sdk.MeetingError
import us.zoom.sdk.MeetingParameter
import us.zoom.sdk.MeetingService
import us.zoom.sdk.MeetingServiceListener
import us.zoom.sdk.MeetingStatus
import us.zoom.sdk.StartMeetingOptions
import us.zoom.sdk.StartMeetingParamsWithoutLogin
import us.zoom.sdk.ZoomError
import us.zoom.sdk.ZoomSDK
import us.zoom.sdk.ZoomSDKInitParams
import us.zoom.sdk.ZoomSDKInitializeListener


const val JWT_TOKEN ="ZOOM-JWT-TOKEN"
const val ZOOM_ACCESS_TOKEN = "ZOOM-ZAK-TOKEN"
//You can get this from iOS AppDelegate methods. I have added couple of methods to generate ZAK token there. You can make use of it.
const val WEB_DOMAIN = "zoom.us"
var MEETINGID: String? = null
var PASSCODE: String? = null
private const val DISPLAY_NAME = "Zoom Demo App"
private val CHANNEL = "zoom_sdk_channel"

class MainActivity : FlutterActivity(), MeetingServiceListener, ZoomSDKInitializeListener {
val sdk = ZoomSDK.getInstance()

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
if (call.method == "initializeZoomSDK") {
initialiseSDK()
} else if (call.method == "joinMeeting") {
MEETINGID = call.argument<String>("meetingID");
PASSCODE = call.argument<String>("meetingPasscode");
joinMeeting()
} else if (call.method == "startMeeting") {
MEETINGID = call.argument<String>("meetingID");
startMeeting()
} else {
result.notImplemented()
}
}
}

private fun initialiseSDK() {
val initParams = ZoomSDKInitParams()
initParams.jwtToken = JWT_TOKEN
initParams.enableLog = true
initParams.domain = WEB_DOMAIN
sdk.initialize(context, this, initParams)
}

private fun joinMeeting() {
val meetingService: MeetingService = sdk.getMeetingService()
val opts = JoinMeetingOptions()
opts.no_invite = true
val params = JoinMeetingParams()
params.displayName = DISPLAY_NAME
params.meetingNo = MEETINGID
params.password = PASSCODE
meetingService.joinMeetingWithParams(context, params, opts)
}

private fun startMeeting() {
val meetingService: MeetingService = sdk.getMeetingService()
val opts = StartMeetingOptions()
opts.no_invite = true
val params = StartMeetingParamsWithoutLogin()
params.zoomAccessToken = ZOOM_ACCESS_TOKEN
params.meetingNo = MEETINGID
params.displayName = DISPLAY_NAME
meetingService.startMeetingWithParams(this, params, opts)
}

override fun onZoomSDKInitializeResult(errorCode: Int, internalErrorCode: Int) {
if (errorCode != ZoomError.ZOOM_ERROR_SUCCESS) {
Log.d("Failed", "Failed to initialize Zoom SDK");
Toast.makeText(
this,
"Failed to initialize Zoom SDK. Error: " + errorCode + ", internalErrorCode=" + internalErrorCode,
Toast.LENGTH_LONG
).show();
} else {
Log.d("Success", "Initialize Zoom SDK successfully.");
Toast.makeText(this, "Initialize Zoom SDK successfully.", Toast.LENGTH_LONG).show();
}
}

private fun registerMeetingServiceListener() {
val zoomSDK = ZoomSDK.getInstance()
val meetingService = zoomSDK.meetingService
meetingService?.addListener(this)
}

override fun onDestroy() {
val zoomSDK = ZoomSDK.getInstance()
if (zoomSDK.isInitialized) {
val meetingService = zoomSDK.meetingService
meetingService.removeListener(this)
}
super.onDestroy()
}

override fun onMeetingStatusChanged(
meetingStatus: MeetingStatus, errorCode: Int,
internalErrorCode: Int
) {
if (meetingStatus == MeetingStatus.MEETING_STATUS_FAILED && errorCode == MeetingError.MEETING_ERROR_CLIENT_INCOMPATIBLE) {
Toast.makeText(this, "Version of ZoomSDK is too low!", Toast.LENGTH_LONG).show()
}
}

override fun onZoomAuthIdentityExpired() {}
override fun onMeetingParameterNotification(meetingParameter: MeetingParameter) {}

}

Tip: To generate ZOOM JWT TOKEN— you can use https://jwt.io/ and with playload and verify signature.

Payload:

{
"appKey": "ZOOM-CLIENT-KEY",
"iat": ANY-TIME-STAMP,
"exp": ANY-TIME-STAMP, //greater than iat
"tokenExp": ANY-TIME-STAMP //greater than iat
}

Verify Signature:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
"ZOOM-CLIENT-SECRET"
)

All done 🙌. Now it time to run the app and play around with zoom like boooooom 😆

Please buy me a beer. I made your life soooooo easy you know. 😜

You can reach me out here.

Cheers🍻 bye.

--

--

Ashok

 iOS and Flutter developer @TCS, Chennai. Here to share best practices learned through my experience. Reach me on www.linkedin.com/in/ashok1208