plivo-php icon indicating copy to clipboard operation
plivo-php copied to clipboard

How to Integrate Plivo SDK in Laravel?

Open hilmihidyt opened this issue 4 weeks ago • 0 comments

Plivo SDK in Laravel I am integrating Plivo in Laravel to create a "click to call" function on my web app page. I want to provide features such as call, hangup, mute, and unmute on this page. I also want a feature that allows users to talk directly through the web. So, when the user clicks the "Call" button, the user can immediately speak to someone through the browser.

I have created codes like the following, but I get an error message: "Failed to load Plivo SDK: Plivo SDK failed to load".

show.blade.php

@extends('layouts.app')
@section('title', "Contact: {$contact->first_name} {$contact->last_name}")
@section('styles')
<script type="text/javascript" src="https://cdn.plivo.com/sdk/browser/v2/plivo.min.js"> </script>
<style>
    .call-interface {
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
    margin-bottom: 20px;
    }
    .call-status {
    font-size: 18px;
    margin-bottom: 15px;
    }
    .call-controls button {
    margin-right: 10px;
    margin-bottom: 10px;
    }
    .call-timer {
    font-size: 24px;
    font-weight: bold;
    margin: 15px 0;
    }
    .call-logs {
    margin-top: 30px;
    }
</style>
@endsection
@section('content')
<div class="col-md-4">
    <div class="card mb-3">
        <div class="card-header">
            <h5 class="card-title mb-0">
                Contact Details
            </h5>
        </div>
        <div class="card-body">
            <div class="mb-1">
                <strong>Name:</strong> {{ $contact->first_name }} {{ $contact->last_name }}
            </div>
            <div class="mb-1">
                <strong>Phone:</strong> {{ $contact->phone }}
            </div>
        </div>
        <div class="card-footer">
            <div class="row g-3 align-items-center">
                <div class="col-12">
                    <label for="note" class="form-label">Note</label>
                    <textarea name="note" class="form-control" id="note" rows="3">{!! $disposition ? $disposition->note : '' !!}</textarea>
                </div>
                <div class="col-auto">
                    <select name="disposition" class="form-select" id="disposition">
                        <option value="">Select disposition</option>
                        @foreach (config('data.disposition_status') as $key => $item)
                        <option value="{{ $key }}" {{ $disposition && $disposition->status === $key ? 'selected' : '' }}>{{ $item }}</option>
                        @endforeach
                    </select>
                </div>
                <div class="col-auto">
                    <button onclick="makeCall('{{ $contact->phone }}')" class="btn btn-dark">Call</button>
                </div>
            </div>
        </div>
    </div>
</div>
<div class="col-md-8">
    <div class="card">
        <div class="card-header">
            <h3>Call Interface</h3>
        </div>
        <div class="card-body">
            <div class="call-interface">
                <div id="call-status" class="call-status alert alert-info">
                    Ready
                </div>
                <div class="call-timer" id="call-timer">
                    00:00:00
                </div>
                <div class="call-controls">
                    <button id="makeCall" class="btn btn-success" disabled>
                    <i class="fas fa-phone"></i> Call
                    </button>
                    <button id="hangupCall" class="btn btn-danger" disabled>
                    <i class="fas fa-phone-slash"></i> Hangup
                    </button>
                    <button id="muteCall" class="btn btn-warning" disabled>
                    <i class="fas fa-microphone-slash"></i> Mute
                    </button>
                    <button id="unmuteCall" class="btn btn-info" disabled>
                    <i class="fas fa-microphone"></i> Unmute
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
@push('scripts')
<script>
    let client;
    let currentCall = null;
    let callTimer = null;
    let callStartTime = null;
    
    async function initializePlivoClient() {
        try {
            console.log('Initializing Plivo client...');
            
            if (typeof window.plivoBrowserSdk === 'undefined') {
                throw new Error('Plivo Browser SDK not loaded');
            }
    
            const response = await fetch('{{ route("plivo.token") }}');
            const data = await response.json();
            
            if (data.error) {
                throw new Error(data.error);
            }
            
            console.log('Token received, initializing client...');
    
            try {
                const options = {
                    debug: "DEBUG",
                    permOnClick: true,
                    enableTracking: true,
                    audioConstraints: {
                        optional: [
                            { echoCancellation: true },
                            { noiseSuppression: true }
                        ]
                    }
                };
    
                // Create client using the correct SDK reference
                client = new window.plivoBrowserSdk.Client(options);
                
                console.log('Client created, attempting login...');
                
                // Login
                await client.login(data.username, data.password);
                
                console.log('Login successful');
                document.getElementById('makeCall').disabled = false;
                document.getElementById('call-status').innerHTML = 'Ready to call';
                document.getElementById('call-status').className = 'alert alert-success';
                
                setupEventListeners();
                
            } catch (error) {
                throw new Error(`Client initialization failed: ${error.message}`);
            }
    
        } catch (error) {
            console.error('Failed to initialize Plivo client:', error);
            document.getElementById('call-status').innerHTML = 'Failed to initialize phone: ' + error.message;
            document.getElementById('call-status').className = 'alert alert-danger';
        }
    }
    
    // Wait for SDK to load
    function waitForPlivoSDK() {
        return new Promise((resolve, reject) => {
            let attempts = 0;
            const maxAttempts = 20;
            
            const checkSDK = setInterval(() => {
                if (typeof window.plivoBrowserSdk !== 'undefined') {
                    clearInterval(checkSDK);
                    resolve();
                } else {
                    attempts++;
                    if (attempts >= maxAttempts) {
                        clearInterval(checkSDK);
                        reject(new Error('Plivo SDK failed to load'));
                    }
                }
            }, 250);
        });
    }
    
    // Initialize when document is ready
    document.addEventListener('DOMContentLoaded', async () => {
        try {
            await waitForPlivoSDK();
            await initializePlivoClient();
        } catch (error) {
            console.error('Initialization failed:', error);
            document.getElementById('call-status').innerHTML = 'Failed to load Plivo SDK: ' + error.message;
            document.getElementById('call-status').className = 'alert alert-danger';
        }
    });
    
    function setupEventListeners() {
        if (!client) {
            console.error('Client not initialized');
            return;
        }
    
        client.on('onLogin', () => {
            console.log('Successfully logged in to Plivo');
            document.getElementById('call-status').innerHTML = 'Ready to make calls';
            document.getElementById('call-status').className = 'alert alert-success';
        });
    
        client.on('onLoginFailed', (error) => {
            console.error('Login failed:', error);
        });
    
        client.on('onCallRemoteRinging', () => {
            document.getElementById('call-status').innerHTML = 'Ringing...';
            document.getElementById('call-status').className = 'alert alert-warning';
        });
    
        client.on('onCallAnswered', () => {
            document.getElementById('call-status').innerHTML = 'Call in progress';
            document.getElementById('call-status').className = 'alert alert-success';
            document.getElementById('hangupCall').disabled = false;
            document.getElementById('muteCall').disabled = false;
            startCallTimer();
        });
    
        client.on('onCallTerminated', () => {
            resetCallInterface();
            logCall('completed');
        });
    
        client.on('onCallFailed', (error) => {
            console.error('Call failed:', error);
            resetCallInterface();
            logCall('failed');
        });
    }
    
    function startCallTimer() {
        callStartTime = new Date();
        callTimer = setInterval(() => {
            const now = new Date();
            const diff = new Date(now - callStartTime);
            const hours = diff.getUTCHours().toString().padStart(2, '0');
            const minutes = diff.getUTCMinutes().toString().padStart(2, '0');
            const seconds = diff.getUTCSeconds().toString().padStart(2, '0');
            document.getElementById('call-timer').innerHTML = `${hours}:${minutes}:${seconds}`;
        }, 1000);
    }
    
    function resetCallInterface() {
        currentCall = null;
        document.getElementById('call-status').innerHTML = 'Ready';
        document.getElementById('call-status').className = 'alert alert-info';
        document.getElementById('hangupCall').disabled = true;
        document.getElementById('muteCall').disabled = true;
        document.getElementById('unmuteCall').disabled = true;
        document.getElementById('call-timer').innerHTML = '00:00:00';
        
        if (callTimer) {
            clearInterval(callTimer);
            callTimer = null;
        }
        callStartTime = null;
    }
    
    async function logCall(status) {
        try {
            await fetch('/api/call-logs', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
                },
                body: JSON.stringify({
                    contact_id: '{{ $contact->id }}',
                    status: status,
                    start_time: callStartTime,
                    end_time: new Date(),
                    duration: callStartTime ? Math.round((new Date() - callStartTime) / 1000) : 0
                })
            });
        } catch (error) {
            console.error('Failed to log call:', error);
        }
    }
    
    // Initialize when page loads
    document.addEventListener('DOMContentLoaded', initializePlivoClient);
    
    // Call button event listener
    document.getElementById('makeCall').addEventListener('click', async () => {
        try {
            currentCall = await client.call('{{ $contact->phone }}');
            document.getElementById('call-status').innerHTML = 'Calling...';
            document.getElementById('call-status').className = 'alert alert-warning';
        } catch (error) {
            console.error('Error making call:', error);
            document.getElementById('call-status').innerHTML = 'Call failed: ' + error.message;
            document.getElementById('call-status').className = 'alert alert-danger';
        }
    });
    
    // Hangup button event listener
    document.getElementById('hangupCall').addEventListener('click', () => {
        if (currentCall) {
            currentCall.hangup();
        }
    });
    
    // Mute button event listener
    document.getElementById('muteCall').addEventListener('click', () => {
        if (currentCall) {
            currentCall.mute();
            document.getElementById('muteCall').disabled = true;
            document.getElementById('unmuteCall').disabled = false;
        }
    });
    
    // Unmute button event listener
    document.getElementById('unmuteCall').addEventListener('click', () => {
        if (currentCall) {
            currentCall.unmute();
            document.getElementById('muteCall').disabled = false;
            document.getElementById('unmuteCall').disabled = true;
        }
    });
</script>
@endpush

PlivoController.php

<?php

namespace App\Http\Controllers;

use App\Models\Call;
use Plivo\RestClient;
use Illuminate\Support\Str;
use Illuminate\Http\Request;

class PlivoController extends Controller
{
    protected $client;

    public function __construct()
    {
        $this->client = new RestClient(
            config('services.plivo.auth_id'),
            config('services.plivo.auth_token')
        );
    }

    public function getToken()
    {
        try {
            $endpointId = config('services.plivo.endpoint_id');
            $username = 'user_' . time();

            $token = $this->generateToken($username);

            return response()->json([
                'username' => $username,
                'password' => $token
            ]);

        } catch (\Exception $e) {
            \Log::error('Plivo Token Error', [
                'error' => $e->getMessage()
            ]);

            return response()->json([
                'error' => $e->getMessage()
            ], 500);
        }
    }

    private function generateToken($username)
    {
        $authId = config('services.plivo.auth_id');
        $authToken = config('services.plivo.auth_token');

        $iat = time();
        $exp = $iat + 3600; // Token berlaku 1 jam

        $payload = [
            "iss" => $authId,
            "sub" => $username,
            "iat" => $iat,
            "exp" => $exp,
            "jti" => Str::uuid()->toString()
        ];

        $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
        
        $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
        $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode(json_encode($payload)));
        
        $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $authToken, true);
        $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
        
        return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
    }

    public function handleCallEvent(Request $request)
    {
        \Log::info('Call Event Received', $request->all());
        return response()->json(['status' => 'success']);
    }
}

web.php

Route::get('/plivo/token', [PlivoController::class, 'getToken'])->name('plivo.token');
Route::post('/plivo/call-event', [PlivoController::class, 'handleCallEvent'])->name('plivo.call-event');

What is the correct way to integrate Plivo in Laravel?

hilmihidyt avatar Jan 29 '25 08:01 hilmihidyt