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:
- 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'));
}
}
- 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;
}
}
}
- 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
- 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