Can not join meeting with laravel project

Hello, I am building a Laravel project that try to create a meeting through API, and then join the meeting created. But I can not join the meeting, it is showing the error:

Uncaught (in promise) Objectdata: undefinederrorCode: "NOT_CONNECTED"evt: "ERROR"type: “VIDEO”[[Prototype]]: Object

Here step by step what I did:

Step 1: Make an app on zoom market place with a callback handle to authorize Zoom.

Step 2: I can create a sample meeting and then show it on the UI

Meeting ID: 89801712874
Join URL: xxxx
Start Time: 2024-11-28 13:44:31

Step 3: I am making a controller for use when clicking on Zoom meeting ID, I want the zoom client can be embedded in my website with meeting SDK

But it is getting error above.

Here is my structure of code:

  1. web.php
    Route::group(['prefix' => 'zoom'], function () {
        Route::post('join', [ZoomController::class, 'join'])->name('zoom.join');
        Route::get('callback', [LiveStreamController::class, 'handleZoomCallback'])->name('zoom.callback');
    });

    Route::get('/zoom-meeting', [ZoomController::class, 'showMeeting'])->name('zoom.meeting');

2.ZoomController

<?php
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use GuzzleHttp\Client;
use Illuminate\Support\Str;

class ZoomController extends Controller
{
    public function handleZoomCallback(Request $request)
    {
        $code = $request->input('code');

        $response = Http::withOptions([
            'verify' => storage_path('certs/cacert.pem'), // Ensure this path is correct
        ])->asForm()->withHeaders([
            'Authorization' => 'Basic ' . base64_encode(config('services.zoom.client_id') . ':' . config('services.zoom.client_secret')),
        ])->post('https://zoom.us/oauth/token', [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => config('services.zoom.redirect'),
        ]);

        // return $response;

        if ($response->successful()) {
            $accessToken = $response->json()['access_token'];
            $refreshToken = $response->json()['refresh_token'];
            $expiresIn = $response->json()['expires_in'];
            $expiresAt = now()->addSeconds($expiresIn);

            Auth::user()->update([
                'zoom_access_token' => $accessToken,
                'zoom_token_expires_at' => $expiresAt,
                'zoom_refresh_token' => $refreshToken,
            ]);

            return redirect()->route('live.stream')->with('success', 'Zoom authorized successfully.');
        } else {
            return redirect()->route('live.stream')->with('error', 'Failed to authorize Zoom.');
        }
    }

    public function showMeeting(Request $request)
    {
        // Add CORS headers
        header("Access-Control-Allow-Origin: https://zoom.us");
        header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
        header("Access-Control-Allow-Headers: Content-Type, Authorization");

        $meetingNumber = $request->input('meetingNumber');
        $password = $request->input('password');
        $signature = $request->input('signature');
        $leaveUrl = url('/live-stream');
        $userName = auth()->user()->full_name; 
        $sdkKey = config('services.zoom.client_id');
        $role = 1; // Set the role to 1 for host
        $zakToken = auth()->user()->zoom_access_token; // Assuming the ZAK token is stored as zoom_access_token

        return view('zoom', compact('meetingNumber', 'password', 'signature', 'leaveUrl', 'userName', 'sdkKey', 'role', 'zakToken'));
    }
}
  1. Livestream controller
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GuzzleHttp\Client;
use Firebase\JWT\JWT;
use App\Models\Zoom; // Add this import statement

class LiveStreamController extends Controller
{
    public function index()
    {
        $user = auth()->user();
        if ($user->zoom_access_token && $this->isTokenExpired()) {
            $this->refreshAccessToken();
        }

        $meetingNumber = session('meetingNumber', 'YOUR_MEETING_NUMBER'); // Replace with your actual Zoom meeting number
        $password = session('password', 'YOUR_MEETING_PASSWORD'); // Replace with your actual Zoom meeting password or set to an empty string if not required

        $meetings = Zoom::where('user_id', $user->id)
                        ->get();

        foreach ($meetings as $meeting) {
            $meeting->signature = $this->generateSignature($meeting->meeting_number);
        }

        return view('live-stream.index', compact('meetingNumber', 'password', 'meetings'));
    }

    public function createMeeting(Request $request)
    {
        $accessToken = $this->getAccessToken();

        if ($this->isTokenExpired()) {
            $this->refreshAccessToken();
            $accessToken = $this->getAccessToken();
        }

        $client = new Client([
            'verify' => storage_path('certs/cacert.pem'), // Ensure this path is correct
        ]);
        try {
            $response = $client->post('https://api.zoom.us/v2/users/me/meetings', [
                'headers' => [
                    'Authorization' => 'Bearer ' . $accessToken,
                    'Content-Type' => 'application/json',
                ],
                'json' => [
                    'topic' => 'Test Zoom Meeting',
                    'type' => 2, // Scheduled meeting
                    'start_time' => '2024-11-20T10:00:00Z',
                    'duration' => 30, // 30 minutes
                    'timezone' => 'UTC',
                    'password' => 'test123',
                    'agenda' => 'Testing Zoom API',
                ],
            ]);

            $data = json_decode($response->getBody(), true);
            $meetingNumber = $data['id'];
            $password = $data['password'];
            $joinUrl = $data['join_url'];
            $signature = $this->generateSignature($meetingNumber);

            // Convert start_time to a format compatible with the database
            $startTime = \Carbon\Carbon::parse($data['start_time'])->toDateTimeString();

            // Save the new meeting details in the database
            Zoom::create([
                'user_id' => auth()->id(),
                'meeting_number' => $meetingNumber,
                'password' => $password,
                'join_url' => $joinUrl,
                'start_time' => $startTime,
                'duration' => 30,
                'agenda' => 'Testing Zoom API',
                'signature' => $signature,
            ]);

            return redirect()->route('live.stream')->with(compact('meetingNumber', 'password', 'joinUrl', 'accessToken', 'signature'));
        } catch (\GuzzleHttp\Exception\ClientException $e) {
            if ($e->getResponse()->getStatusCode() == 401 || $e->getResponse()->getStatusCode() == 400) {
                // Access token is invalid or expired, refresh the token
                $this->refreshAccessToken();
                return $this->createMeeting($request); // Retry the request
            }
            throw $e;
        }
    }

    private function generateSignature($meetingNumber)
    {
        $apiKey = config('services.zoom.client_id');
        $apiSecret = config('services.zoom.client_secret');
        $role = 1;

        $time = time() * 1000 - 30000; // 30 seconds before the current time
        $data = base64_encode($apiKey . $meetingNumber . $time . $role);

        $hash = hash_hmac('sha256', $data, $apiSecret, true);
        $signature = rtrim(strtr(base64_encode($data . '.' . $hash), '+/', '-_'), '=');

        return $signature;
    }

    private function getAccessToken()
    {
        $user = auth()->user();
        return $user->zoom_access_token;
    }

    private function isTokenExpired()
    {
        $user = auth()->user();
        return now()->greaterThan($user->zoom_token_expires_at);
    }

    private function refreshAccessToken()
    {
        $clientId = config('services.zoom.client_id');
        $clientSecret = config('services.zoom.client_secret');
        $user = auth()->user();
        $refreshToken = $user->zoom_refresh_token;

        $client = new Client([
            'verify' => storage_path('certs/cacert.pem'), // Ensure this path is correct
        ]);

        try {
            $response = $client->post('https://zoom.us/oauth/token', [
                'form_params' => [
                    'grant_type' => 'refresh_token',
                    'refresh_token' => $refreshToken,
                    'client_id' => $clientId,
                    'client_secret' => $clientSecret,
                ],
            ]);

            $data = json_decode($response->getBody(), true);
            $accessToken = $data['access_token'];
            $newRefreshToken = $data['refresh_token'];
            $expiresIn = $data['expires_in'];
            $expiresAt = now()->addSeconds($expiresIn);

            $user->update([
                'zoom_access_token' => $accessToken,
                'zoom_refresh_token' => $newRefreshToken,
                'zoom_token_expires_at' => $expiresAt,
            ]);
        } catch (\GuzzleHttp\Exception\ClientException $e) {
            if ($e->getResponse()->getStatusCode() == 400) {
                // Redirect to Zoom authorization URL if refresh token is invalid
                return redirect('https://zoom.us/oauth/authorize?response_type=code&client_id=' . $clientId . '&redirect_uri=' . config('services.zoom.redirect'));
            }
            throw $e;
        }
    }
}
  1. UI show list of meetings
@extends('layouts.app')

@section('content')
<div class="container">
    <h1>Live Stream</h1>

    <a href="https://zoom.us/oauth/authorize?response_type=code&client_id={{ config('services.zoom.client_id') }}&redirect_uri={{ config('services.zoom.redirect') }}" class="btn btn-primary">Authorize Zoom</a>

    <p>Access Token: {{ session('zoom_access_token') }}</p>
    <form action="{{ route('live.stream.create') }}" method="POST">
        @csrf
        <button type="submit" class="btn btn-primary">Create New Meeting</button>
    </form>
    
    <h2>Available Meetings</h2>
    <ul>
        @foreach($meetings as $meeting)
            <li>
                <p>Meeting ID: <a href="{{ route('zoom.meeting', ['meetingNumber' => $meeting->meeting_number, 'password' => $meeting->password, 'signature' => $meeting->signature]) }}" target="_blank">{{ $meeting->meeting_number }}</a></p>
                <p>Join URL: <a href="{{ $meeting->join_url }}" target="_blank">{{ $meeting->join_url }}</a></p>
                <p>Start Time: {{ $meeting->start_time }}</p>
            </li>
        @endforeach
    </ul>
</div>
@endsection
  1. UI zoom client opening (issue)
<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Zoom Meeting</title>
    <!-- import #zmmtg-root css -->
    <meta name="format-detection" content="telephone=no">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <link type="text/css" rel="stylesheet" href="https://source.zoom.us/2.11.0/css/bootstrap.css" />
    <link type="text/css" rel="stylesheet" href="https://source.zoom.us/2.11.0/css/react-select.css" />
</head>
<body>
<div id="zmmtg-root">
    <p>Meeting Number: {{ $meetingNumber }}</p>
    <p>Password: {{ $password }}</p>
    <p>User Name: {{ $userName }}</p>
    <p>Leave URL: {{ $leaveUrl }}</p>
    <p>SDK Key: {{ $sdkKey }}</p>
    <p>Signature: {{ $signature }}</p>
</div>
<div id="aria-notify-area"></div>
<!-- added on meeting init -->
<div class="ReactModalPortal"></div>
<div class="ReactModalPortal"></div>
<div class="ReactModalPortal"></div>
<div class="ReactModalPortal"></div>
<div class="global-pop-up-box"></div>
<div class="sharer-controlbar-container sharer-controlbar-container--hidden"></div>

<!-- For component and client view -->
<script src="https://source.zoom.us/2.11.0/lib/vendor/react.min.js"></script>
<script src="https://source.zoom.us/2.11.0/lib/vendor/react-dom.min.js"></script>
<script src="https://source.zoom.us/2.11.0/lib/vendor/redux.min.js"></script>
<script src="https://source.zoom.us/2.11.0/lib/vendor/redux-thunk.min.js"></script>
<script src="https://source.zoom.us/2.11.0/lib/vendor/lodash.min.js"></script>

<!-- For client view -->
<script src="https://source.zoom.us/zoom-meeting-2.11.0.min.js"></script>

<script>
    var leaveUrl = @json($leaveUrl);
    var meetingNumber = @json($meetingNumber);
    var password = @json($password);
    var userName = @json($userName); 
    var sdkKey = @json($sdkKey);
    var signature = @json($signature);
    var role = @json($role);
    var zakToken = @json($zakToken);

    console.log('Meeting Number:', meetingNumber);
    console.log('Password:', password);
    console.log('User Name:', userName);
    console.log('Leave URL:', leaveUrl);
    console.log('SDK Key:', sdkKey);
    console.log('Signature:', signature);
    console.log('Role:', role);
    console.log('ZAK Token:', zakToken);

    // Global CDN, use source.zoom.us:
    ZoomMtg.setZoomJSLib('https://source.zoom.us/2.11.0/lib', '/av')
    // loads dependent assets
    ZoomMtg.preLoadWasm()
    ZoomMtg.prepareWebSDK()
    // loads language files, also passes any error messages to the ui
    ZoomMtg.i18n.load('en-US')
    ZoomMtg.i18n.reload('en-US')

    ZoomMtg.init({
        leaveUrl: leaveUrl,
        isSupportAV: true,
        success: (success) => {
            console.log('Zoom SDK initialized successfully.');
            ZoomMtg.join({
                sdkKey: sdkKey,
                signature: signature, // role in SDK signature needs to be 1
                meetingNumber: meetingNumber,
                passWord: password,
                userName: userName,
                role: role,
                zak: zakToken, // the host's ZAK token
                success: (success) => {
                    console.log('Join meeting success:', success);
                },
                error: (error) => {
                    console.error('Join meeting error:', error);
                    alert('Error joining meeting: ' + JSON.stringify(error));
                },
                timeout: 10000 // Increase timeout period to 10 seconds
            });
        },
        error: (error) => {
            console.error('Zoom SDK initialization error:', error);
            alert('Error initializing Zoom SDK: ' + JSON.stringify(error));
        }
    });
</script>
</body>
</html>

6.evn setting

ZOOM_CLIENT_ID=2A1tDNKQkuanonCBNYNOw
ZOOM_CLIENT_SECRET=MtcRcybnrxA7OkuQv7AcnDdqVGeNv4DR
ZOOM_REDIRECT_URI=http://127.0.0.1:8000/zoom/callback

Mysql database setting, I just create Zoom & User table for testing only

Zoom table:

CREATE TABLE `zooms` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint unsigned NOT NULL,
  `meeting_number` varchar(255) NOT NULL,
  `password` varchar(255) DEFAULT NULL,
  `join_url` varchar(255) NOT NULL,
  `start_time` timestamp NOT NULL,
  `duration` int NOT NULL,
  `agenda` varchar(255) DEFAULT NULL,
  `signature` varchar(255) DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) 

User table:

CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `core_id` int NOT NULL DEFAULT '0',
  `full_name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL,
  `username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL,
  `role_id` int DEFAULT NULL,
  `referral` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `referral_id` int DEFAULT '0',
  `path` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `level` int DEFAULT '0',
  `avatar` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '',
  `cover` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `phone` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `cmnd` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `gender` tinyint(1) DEFAULT '0',
  `password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `remember_token` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `status` smallint NOT NULL DEFAULT '1',
  `auth_key` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `access_token` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `logged_in_ip` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `logged_in_at` timestamp NULL DEFAULT NULL,
  `created_ip` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `banned_at` timestamp NULL DEFAULT NULL,
  `banned_reason` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `country` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `address` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `address2` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `verify_code` varchar(20) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `email_verified` tinyint NOT NULL DEFAULT '1',
  `email_verified_at` timestamp NULL DEFAULT NULL,
  `phone_verified` tinyint NOT NULL DEFAULT '0',
  `birthday` int DEFAULT '0',
  `qr_url` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `public_key` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `kyc1` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `kyc2` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `kyc_level` int NOT NULL DEFAULT '0',
  `main_wallet` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `finance_wallet` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `market_wallet` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `nid` int NOT NULL DEFAULT '0',
  `vip` int DEFAULT '0',
  `trust` tinyint(1) DEFAULT '0',
  `search_config` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `city_id` int NOT NULL DEFAULT '0',
  `bid_numbers` int DEFAULT '0',
  `bet_numbers` int DEFAULT '0',
  `sell_numbers` int DEFAULT '0',
  `follow_ids` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `block_ids` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `term_service` tinyint(1) NOT NULL DEFAULT '0',
  `point` int DEFAULT '0',
  `session` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `provider` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `provider_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `type` tinyint DEFAULT NULL,
  `facebook` varchar(300) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `instagram` varchar(300) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `tiktok` varchar(300) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `youtube` varchar(300) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `description` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `detail` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci,
  `subject_id` int NOT NULL DEFAULT '0',
  `zoom_user_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `zoom_account_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `zoom_account_number` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `identifier` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `restore` tinyint DEFAULT '0',
  `assistant_teacher_id` int DEFAULT NULL,
  `fcm_token` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `device_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL,
  `display` int DEFAULT NULL,
  `vip1` int DEFAULT '0',
  `vip3` int DEFAULT '0',
  `session_id` text,
  `zoom_access_token` longtext,
  `zoom_refresh_token` longtext,
  `zoom_token_expires_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`)
)

Please kindly support