Embedding & JS SDK
Put the GurukulLive classroom inside your own page or app. The room runs in an iframe; the @vmeet/embed SDK creates it with the right permissions and gives you typed events and commands.
Quick start
Get an embedUrl from a join link on your server, then on your page:
<div id="classroom" style="width: 100%; height: 70vh"></div>
<script src="https://gurukullive.example.com/sdk/vmeet-embed.js"></script>
<script>
const call = VMeet.createFrame(document.getElementById('classroom'), {
url: EMBED_URL, // from POST /public/meetings/:id/join-links (your backend)
theme: 'light',
prejoin: true,
brandColor: '#0F766E',
});
call.on('joined', (e) => console.log('joined as', e.participant.name, 'with', e.participants.length, 'others'));
call.on('participant-joined', (e) => console.log(e.participant.name, 'joined'));
call.on('hand-raised', (e) => e.raised && notifyTeacher(e.participant.name));
call.on('meeting-ended', () => location.assign('/classes'));
call.on('error', (e) => console.warn(e.code, e.message));
document.getElementById('mute').onclick = async () => {
const { state } = await call.toggleMic(); // { micEnabled, cameraEnabled, handRaised, displayName }
console.log('mic is now', state.micEnabled ? 'on' : 'off');
};
</script>With a bundler: import { createFrame } from '@vmeet/embed' — same API, fully typed, about 3 KB gzipped, no dependencies.
Plain iframe
Without the SDK, add the permissions the room needs and your page's origin:
<iframe
src="https://gurukullive.example.com/embed/meeting/algebra-10a-k3f9q2?jt=vjt_9Qm…&parentOrigin=https%3A%2F%2Flms.example.com&theme=light"
allow="camera; microphone; display-capture; autoplay; fullscreen; picture-in-picture; clipboard-write"
allowfullscreen
title="Algebra class"
style="border: 0; width: 100%; height: 70vh"
></iframe>Do not add a sandbox attribute — the room needs scripts, same-origin storage and media. Give the frame at least 360 × 480 px; it adapts down to phone widths.
Query parameters
| Parameter | Default | Effect |
|---|---|---|
jt | — | Join token. Already part of embedUrl. |
parentOrigin | — | Origin of your page, e.g. https://lms.example.com. Required for events/commands. The SDK adds it for you. |
theme | viewer’s theme | light or dark for the pre-join and status screens. The call itself is always dark. |
prejoin | true | false skips the camera/microphone check and joins immediately. |
chat | true | false hides in-room chat. |
brandColor | GurukulLive indigo | #RRGGBB accent for buttons (URL-encode # as %23). |
hideLeaveButton | false | true hides the Leave button — call leave() from your own UI. |
leaveRedirect | — | https URL to open after the user leaves (the top page is navigated when the user clicked Leave). Prefer handling the left event. |
The embedded room's interface is in English.
Allowed origins
GurukulLive only lets approved sites frame the room, and only talks to approved origins:
- Framing is controlled by the
Content-Security-Policy: frame-ancestorsheader on/embed/*. By default only GurukulLive itself may frame it. Self-hosted deployments list integrator sites inEMBED_DEFAULT_FRAME_ANCESTORS(space- or comma-separated origins,https://*.example.comwildcards allowed); per-organization lists come from the organization's embed settings when the deployment enables that lookup. Ask your GurukulLive administrator to add your origin. - Events and commands use
postMessagewith exact origins on both sides. The room posts only toparentOrigin, and only after checking it against the browser-reported parent page (and the organization's allowed origins when configured). It accepts commands only from that same window and origin. The SDK, in turn, accepts messages only from its own iframe and the room's origin.
SDK reference
VMeet.createFrame(container, options)
container is an element or a CSS selector. options: url (required), theme, prejoin, chat, brandColor, hideLeaveButton, leaveRedirect, width/height (CSS sizes, default 100%), title (accessible name of the iframe), commandTimeoutMs (default 10 000). Returns a VMeetFrame.
VMeetFrame
on(type, handler),once(type, handler),off(type, handler?)—on('*', (payload, type) => …)receives every event.whenReady()— promise of thereadypayload;isReady.- Commands return a promise of
{ ok, state?, error? }; they are queued untilready. destroy()— removes the iframe (the user leaves) and all listeners.iframe,origin— the element and the room's exact origin.
Events (room → your page)
| Event | Payload / when |
|---|---|
ready | { version, meeting, role, prejoin } — the room loaded and accepted your origin. Commands sent earlier are delivered now. |
joined | { meeting, role, participant, participants[], state } — connected to the class. |
left | { reason: 'user' | 'removed' | 'duplicate' | 'error' } — the user left, was removed, joined elsewhere, or the connection failed. |
participant-joined | { participant } — someone else joined. |
participant-left | { participant } — someone else left. |
recording-started | { recordingId } — cloud recording started (also sent if it was already running at join). |
recording-stopped | { recordingId } |
hand-raised | { participant, raised } — a hand went up or down (including your user’s own). |
meeting-ended | { meetingId } — the host ended the meeting for everyone. |
error | { code, message, fatal? } — e.g. join_token_expired, media_permission_denied, connection_failed, meeting_full (the room is full; offer a retry later). |
participant objects are { identity, userId, name, isHost, isLocal }; userId is theGurukulLive user id you get back from POST /public/users.
Commands (your page → room)
| Message type | SDK method | Effect |
|---|---|---|
toggle-mic | toggleMic() | Mute / unmute (works on the pre-join screen too). |
toggle-camera | toggleCamera() | Camera on / off. |
raise-hand | raiseHand(raised = true) · lowerHand() | Raise or lower the hand. |
set-display-name | setDisplayName(name) | Rename the user in the room (when the session allows renaming). |
leave | leave() | Leave the meeting (emits left). |
Raw protocol (without the SDK)
const ROOM_ORIGIN = 'https://gurukullive.example.com';
const frame = document.querySelector('iframe');
window.addEventListener('message', (event) => {
// Always check both the origin and the source window
if (event.origin !== ROOM_ORIGIN || event.source !== frame.contentWindow) return;
const msg = event.data; // { source: 'vmeet', type, payload, version }
if (!msg || msg.source !== 'vmeet') return;
if (msg.type === 'joined') console.log('joined', msg.payload.participant.name);
});
// Commands: optional requestId → the room answers { source: 'vmeet', type: 'ack', requestId, payload: { ok, state } }
frame.contentWindow.postMessage({ source: 'vmeet', type: 'toggle-mic', requestId: 'r1' }, ROOM_ORIGIN);Mobile apps and WebViews
The quickest path is opening the join link's url in an Android Custom Tab or iOS SFSafariViewController — no extra setup. To show the room inside your own screen, use a WebView with media enabled:
// AndroidManifest: CAMERA, RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, INTERNET — request CAMERA/RECORD_AUDIO at runtime first
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true // required: the room keeps per-tab state
mediaPlaybackRequiresUserGesture = false // let remote audio/video autoplay
}
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
// Grant only to your GurukulLive host
if (request.origin.host == "gurukullive.example.com") {
request.grant(arrayOf(
PermissionRequest.RESOURCE_VIDEO_CAPTURE,
PermissionRequest.RESOURCE_AUDIO_CAPTURE,
))
} else request.deny()
}
}
// Optional: receive room events (same JSON as postMessage events)
webView.addJavascriptInterface(object {
@JavascriptInterface fun postMessage(json: String) { handleRoomEvent(JSONObject(json)) }
}, "VMeetAndroid")
webView.loadUrl(joinLink.url) // or embedUrl
// Send a command
webView.evaluateJavascript("window.vmeetCommand && window.vmeetCommand('toggle-mic')", null)- Room events reach native code through
VMeetAndroid.postMessage, thevmeetWKScriptMessageHandler orReactNativeWebView.postMessage— whichever your app registers. Send commands withwindow.vmeetCommand(type, payload). - Browsers may block audio until the user taps the page; the room then shows a “Tap to enable meeting audio” button. Disabling the gesture requirement (above) avoids it.
- The room keeps the screen awake during the class (where the platform allows it) and respects notches and home indicators.
Embedding recordings
Ask for a signed player URL on your server — scope recordings:read — and iframe it. Links expire (default 1 hour, up to 24 hours); mint one per page view.
curl -X POST https://gurukullive.example.com/api/v1/public/recordings/REC_ID/playback \
-H "Authorization: Bearer $VMEET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"expiresInSeconds":3600,"viewerExternalId":"student-1001"}'<iframe src="https://gurukullive.example.com/embed/recording/REC_ID?token=…&theme=dark&start=120"
allow="autoplay; fullscreen; picture-in-picture" allowfullscreen
title="Class recording" style="border:0; width:100%; aspect-ratio:16/9"></iframe>Player parameters: token (from embedUrl), theme, autoplay=true (starts muted, as browsers require), start (seconds), parentOrigin. With parentOrigin (added by createFrame) the player emits ready, play, pause, ended and error. Multi-part class recordings play back to back.