Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to Integrate Plivo SDK in Laravel? #193

Open
hilmihidyt opened this issue Jan 29, 2025 · 0 comments
Open

How to Integrate Plivo SDK in Laravel? #193

hilmihidyt opened this issue Jan 29, 2025 · 0 comments

Comments

@hilmihidyt
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant