Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Open Tok Bundle Laravel Package

joos/open-tok-bundle

Symfony bundle integrating the OpenTok (TokBox/Vonage Video) PHP SDK, providing services to create video sessions and generate client tokens.

View on GitHub
Deep Wiki
Context7

Getting Started

First, calibrate expectations: this is a Symfony2 bundle, not a Laravel package. There is no ServiceProvider, no facade, no config/*.php, no publishable assets. composer require joos/open-tok-bundle in a Laravel app will either fail resolution (it pins the legacy opentok/opentok-php-sdk, which drags Guzzle 3/5 against your Guzzle 7) or pull you into a dependency-downgrade project. Treat the repo as reference material, not a dependency.

What to read in the repo, in order — it's small:

  1. DependencyInjection/JoosOpenTokExtension.php — the config keys (api_key, api_secret) and how the SDK client is constructed. This is the entire "logic" of the package.
  2. Resources/config/services.yml — the service id it registers and its constructor arguments. This tells you the exact shape of the object you're injecting.
  3. composer.json — the pinned SDK and PHP constraints. This is where you confirm it's unusable on PHP 8+/Laravel 10+.
  4. JoosOpenTokBundle.php — boilerplate, skim it.

Total reading time: fifteen minutes. That is the honest scope of the package.

What you actually build instead. The bundle's whole value proposition — "credentials from config, client as an injectable service" — is ~100 lines of Laravel glue. The direct successor SDK is vonage/client (TokBox was acquired by Vonage in 2020; OpenTok is now Vonage Video API; opentok/opentok-php-sdk is deprecated).

composer require vonage/client
// config/video.php
return [
    'application_id' => env('VONAGE_APPLICATION_ID'),
    'private_key'    => env('VONAGE_PRIVATE_KEY'), // path or raw JWT key
    'default_ttl'    => (int) env('VIDEO_TOKEN_TTL', 3600),
];
// app/Providers/VideoServiceProvider.php
public function register(): void
{
    $this->app->singleton(VideoSessionProvider::class, fn ($app) => new VonageVideoSessionProvider(
        $app['config']->get('video'),
    ));
}

First use case to get working end-to-end — this is the one the bundle exists to enable, and it is the whole MVP:

  1. POST /api/sessions → server creates a session, returns sessionId.
  2. POST /api/sessions/{id}/token → server mints a per-participant token bound to that session, returns it.
  3. Client-side Vonage JS SDK initializes with applicationId + sessionId + token and connects.

That round trip proves credentials, networking, and the SDK wiring. Do it in a throwaway branch or a php artisan tinker scratchpad before writing any controllers.

Do not start by writing the Vue/React component. The client SDK is the easy part; the server lifecycle and webhook handling are where time disappears.

Implementation Patterns

Interface-first adapter (the single most valuable decision)

The bundle's implicit lesson is that the vendor call surface is tiny. Encode that behind your own contract so a Vonage price change or a Twilio/LiveKit/Daily evaluation is a new class, not a rewrite of every call site.

interface VideoSessionProvider
{
    public function createSession(array $opts = []): SessionRef;   // mediaMode, archiveMode
    public function issueToken(SessionRef $s, array $opts = []): Token; // role, expireTime, data
    public function startArchive(SessionRef $s, array $opts = []): ArchiveRef;
    public function stopArchive(ArchiveRef $a): void;
}

Bind VonageVideoSessionProvider in the provider above. Feature code (ConsultationController, TutoringSession) depends only on the interface.

Controller pattern — session + token in one request

For most product flows you don't need two endpoints. A join endpoint that lazily creates the session (first caller) and mints a token (every caller) is the cleanest shape:

public function join(Meeting $meeting, VideoSessionProvider $video): JsonResponse
{
    if (! $meeting->video_session_id) {
        $session = $video->createSession([
            'mediaMode'   => 'routed',   // 'relayed' for 1:1, 'routed' for 3+
            'archiveMode' => 'manual',  // 'always' if every call must be recorded
        ]);
        $meeting->update(['video_session_id' => $session->id]);
    }

    $token = $video->issueToken($meeting->sessionRef(), [
        'role'       => $this->roleFor($meeting, auth()->user()), // publisher|subscriber|moderator
        'expireTime' => now()->addHour()->timestamp,
        'data'       => json_encode(['user_id' => auth()->id(), 'name' => auth()->user()->name]),
    ]);

    return response()->json([
        'applicationId' => config('video.application_id'),
        'sessionId'     => $meeting->video_session_id,
        'token'         => (string) $token,
    ]);
}

Store the session ID on your domain model (meetings.video_session_id) rather than in cache — it's durable state you'll need for archiving, moderation, and support tickets.

Role mapping as a policy

role is where authorization lives. Map your domain roles to Vonage roles explicitly, and never let the client request a role:

  • subscriber — observer / auditor / supervisor (receives streams, cannot publish).
  • publisher — normal participant.
  • moderator — can force-disconnect, kick, and (with archiveMode) trigger recording.

Put this in a small VideoRolePolicy class. It's the one piece of "business logic" in the whole integration, and the bundle gives you nothing here.

Webhook ingestion — the part the bundle does not help with

Session/stream/archive lifecycle events arrive out of band. This is where real operational weight sits.

Route::post('/webhooks/vonage', VonageWebhookController::class)
    ->withoutMiddleware(['csrf']);
public function __invoke(Request $request)
{
    abort_unless($this->verifySignature($request), 403);

    $payload = $request->json()->all();

    // Dedupe before dispatch — vendor retries aggressively.
    if (! Cache::add('vonage:event:'.$payload['id'], true, now()->addDay())) {
        return response()->noContent(); // 204 is fine; they stop retrying
    }

    match ($payload['type']) {
        'session.created'  => SessionCreated::dispatch($payload),
        'archive.updated'  => ArchiveUpdated::dispatch($payload),
        'connection.created' => ParticipantJoined::dispatch($payload),
        default            => null,
    };

    return response()->noContent(); // fast 200/204, queue the work
}

Rules that matter: verify the signature, dedupe by event ID, respond in <1s, do the work in a queued job, and make every handler idempotent. Never call the vendor API synchronously inside a webhook handler — you'll stack timeouts during incident storms.

Archiving as an explicit state machine

archiveMode: 'always' is seductive and expensive. Prefer 'manual' plus an explicit start/stop:

$archive = $video->startArchive($session, ['name' => "meeting-{$meeting->id}", 'hasAudio' => true, 'hasVideo' => true]);
$meeting->update(['archive_id' => $archive->id, 'archive_status' => 'started']);

Then reconcile archive_status from the archive.updated webhook (started → stopped → uploaded → available), not from the return value of stopArchive(). The stop call returns before the composite is ready; only the webhook tells you when there's a URL you can hand to a user.

Local development

Vonage requires HTTPS and a real network path for media. php artisan serve on http://localhost will connect but you'll fight device permissions and cross-origin issues. Use valet share / ngrok / expose and test from two physical devices or two browser profiles — not two tabs on the same machine with the same mic.

Gotchas and Tips

The framework mismatch is fatal, not cosmetic. Don't try to bridge Symfony's container into Laravel to "reuse" the bundle. A Symfony DI container inside Laravel is more code than the glue you'd be avoiding, and you'd own the bridge forever. Port the shape; don't adopt the code.

Guzzle is the landmine. The legacy opentok/opentok-php-sdk pins Guzzle 3/5. If you have any package needing Guzzle 7 (Laravel HTTP client, Flysystem S3, Stripe, AWS SDK, most mail transports), Composer will either refuse or force a downgrade that silently breaks unrelated features. Run composer require vonage/client --dry-run against your real composer.lock before committing to anything.

The vendor was renamed, twice. TokBox → Vonage (2020) → OpenTok is "Vonage Video API." Class names, config keys, and docs all shifted. Search results will mix eras; the opentok SDK examples you find on Stack Overflow mostly won't run against vonage/client. Budget one migration every 18–36 months on this vendor line — the rename is the precedent.

Credentials are asymmetric now. The old bundle used api_key + api_secret. The current API is application_id + a private key used to sign a short-lived JWT. Do not commit the key; inject it from a secrets manager. Rotate it on a schedule and make sure your adapter reads it at resolution time, not at boot, so rotations don't require a deploy.

Never ship the secret to the client. Only applicationId, sessionId, and a per-user token go over the wire. A leaked project secret lets anyone mint moderator tokens for any session in your account. This is the single highest-severity mistake in the integration.

Token expiry vs. session lifetime. Tokens are bearer credentials with an expireTime. Set it to call-duration-plus-buffer (60–90 min is typical), not 24 hours. Expired tokens fail silently client-side — the user sees a spinner, not an error, and your logs show nothing. Log token issuance with user ID, session ID, and expiry server-side so support can diagnose "can't join" tickets.

data is a string, not an array. The token metadata field is opaque and length-limited; json_encode it yourself and parse client-side. Don't put anything in it you wouldn't put in a URL — it's readable by the client.

Session ID ≠ token ≠ archive ID. Three distinct identifiers with different lifetimes and scopes. Name your columns explicitly (video_session_id, video_archive_id) — the inevitable bug is passing a session ID where a token is expected, which fails as an unhelpful generic connection error.

mediaMode: relayed is cheaper and 1:1-only. It falls back or breaks at 3+ participants. Pick routed up front if group calls are on the roadmap — switching later means the session ID changes and in-flight meetings drop.

Webhooks retry; your handler must be idempotent. Vonage retries on non-2xx and even on slow 2xx. Dedupe by event ID in cache/Redis, and make handlers safe to run twice. This is the number-one source of "duplicate participant" and "archive started twice" bugs.

Don't hold the vendor client in a request-scoped binding if you queue. The JWT is time-bound; a long-lived singleton in a queue worker will eventually sign with an expired token. Construct per job, or make the adapter re-sign on each call.

Extension points are yours to define. The bundle offers no event system, no logging, no retry policy. Wrap every vendor call in a small decorator that logs latency and failure to your existing observability stack — vendor API errors are opaque and you will need your own breadcrumbs:

class LoggingVideoProvider implements VideoSessionProvider { /* delegate + measure */ }

Test with fakes, not the network. Because you own the interface, bind a FakeVideoSessionProvider in tests that returns deterministic session/token strings. Your test suite should never touch Vonage. Keep one nightly/smoke test against the real API to catch SDK drift.

Watch the commercial limits, not the code. Concurrent-session caps, participant-minute pricing, and archive storage are plan-level constraints the SDK won't surface until you hit them. Check the account tier before launch, and log session duration server-side so you can reconcile usage against the invoice — that's the number that will surprise you.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package