2017年10月10日 星期二

PHP 使用 Twilio 在網頁瀏覽器撥打電話

使用瀏覽器的 WebRTC 進行語音對話,所以瀏覽器須支援 WebRTC。
WebRTC 瀏覽器支援狀況:http://caniuse.com/#search=webrtc
注意:
  • Chrome 47以上版本,若不是 https 加密連線網址,將不允許呼叫 getUserMedia() 使用麥克風、視訊。 瀏覽器 console 會有錯誤訊息:twilio.js 1.3 requires WebRTC/ORTC browser support
  • 需有麥克風,否則 JavaScript SDK (twilio.min.js) 會出現找不到麥克風裝置錯誤訊息


步驟:
  1. PHP Server 端用到的東西,可先參考另一邊文章 「PHP 使用 Twilio 發送簡訊範例」:
    A.下載 Twilio 提供的 PHP SDK
    B.取得 ACCOUNT SIDAUTH TOKEN (https://www.twilio.com/console)
  2. Clinet端瀏覽器則會用到 JavaScript SDK (twilio.min.js)
    我之前使用時,GA(Generally Available)版本為 1.3,所以以下都用1.3版當作範例。
    1.3版網址: https://www.twilio.com/docs/api/client/twilio-js-13
    官方說明為直接外部引用,也可選擇將 twilio.min.js 直接下載回來再使用。
    <script src="//media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js" type="text/javascript">
    官網說明:The twilio.js Library: Twilio in the Browser - Twilio
  3. 登入 Twilio 管理介面,到「Home / Phone Numbers / Tools / TwiML Apps
    新增一個 TwiML App 設定,然後記下 「TwiML App 的  SID
    「FRIENDLY NAME」:自己任意定義的名稱
    「Voice 的 REQUEST URL」:這個很重要,是要給 Twilio 訪問的網址,請填一個之後要開放給 Twilio 訪問的網址。
  4. 下載官方 PHP + JavaScript 範例測試
    GitHub:https://github.com/TwilioDevEd/client-quickstart-php
    下載:https://github.com/TwilioDevEd/client-quickstart-php/archive/master.zip
    解壓縮,並將步驟1 Twilio 提供的 PHP SDK,放到 vendor/ 資料夾下,因範例檔引用路徑是 include('./vendor/autoload.php')。
    最後上傳到可執行PHP的對外Server。
    官方 PHP + JavaScript 範例裡面有幾個檔:index.html、quickstart.js、config.example.php、token.php、randos.php、voice.php,說明如後。
  5. 「index.html」,引用 twilio.min.js (twilio 的 JavaScript SDK工具包)、quickstart.js (JavaScript 使用範例,可隨自己需求修改)
    <!DOCTYPE html>
    <html>
        <head>
            <title>Twilio Client Quickstart</title>
            <link rel="stylesheet" href="site.css">
        </head>
        <body>
            <div id="controls">
                <div id="info">
                    <p class="instructions">Twilio Client</p>
                    <div id="client-name"></div>
                </div>
                <div id="call-controls">
                    <p class="instructions">Make a Call:</p>
                    <input id="phone-number" type="text" placeholder="Enter a phone # or client name" />
                    <button id="button-call">Call</button>
                    <button id="button-hangup">Hangup</button>
                </div>
                <div id="log"></div>
            </div>
    
            <script type="text/javascript" src="//media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js"></script>
            <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
            <script src="quickstart.js"></script>
        </body>
    </html>
  6. 「quickstart.js」,twilio.min.js 使用範例。
    其中這一段會將參數資料傳送給 Twilio,之後 Twilio 訪問第3步驟設定的網址時,也會將這些參數資料一併回傳,所以自訂此處的參數資料,可做一些想達到的效果,例如接收要撥的電話號碼、或是進行自己定義的驗證規則。
    ...
    // get the phone number to connect the call to
    var params = {
        To: document.getElementById('phone-number').value
    };
    
    console.log('Calling ' + params.To + '...');
    Twilio.Device.connect(params);
    ...

    quickstart.js完整內容:
    $(function() {
        log('Requesting Capability Token...');
        $.getJSON('./token.php')
                .done(function(data) {
                    log('Got a token.');
                    console.log('Token: ' + data.token);
    
                    // Setup Twilio.Device
                    Twilio.Device.setup(data.token);
    
                    Twilio.Device.ready(function(device) {
                        log('Twilio.Device Ready!');
                        document.getElementById('call-controls').style.display = 'block';
                    });
    
                    Twilio.Device.error(function(error) {
                        log('Twilio.Device Error: ' + error.message);
                    });
    
                    Twilio.Device.connect(function(conn) {
                        log('Successfully established call!');
                        document.getElementById('button-call').style.display = 'none';
                        document.getElementById('button-hangup').style.display = 'inline';
                    });
    
                    Twilio.Device.disconnect(function(conn) {
                        log('Call ended.');
                        document.getElementById('button-call').style.display = 'inline';
                        document.getElementById('button-hangup').style.display = 'none';
                    });
    
                    Twilio.Device.incoming(function(conn) {
                        log('Incoming connection from ' + conn.parameters.From);
                        var archEnemyPhoneNumber = '+12099517118';
    
                        if (conn.parameters.From === archEnemyPhoneNumber) {
                            conn.reject();
                            log('It\'s your nemesis. Rejected call.');
                        } else {
                            // accept the incoming connection and start two-way audio
                            conn.accept();
                        }
                    });
    
                    setClientNameUI(data.identity);
                })
                .fail(function() {
                    log('Could not get a token from server!');
                });
    
        // Bind button to make call
        document.getElementById('button-call').onclick = function() {
            // get the phone number to connect the call to
            var params = {
                To: document.getElementById('phone-number').value
            };
    
            console.log('Calling ' + params.To + '...');
            Twilio.Device.connect(params);
        };
    
        // Bind button to hangup call
        document.getElementById('button-hangup').onclick = function() {
            log('Hanging up...');
            Twilio.Device.disconnectAll();
        };
    
    });
    
    // Activity log
    function log(message) {
        var logDiv = document.getElementById('log');
        logDiv.innerHTML += '<p>&gt;&nbsp;' + message + '</p>';
        logDiv.scrollTop = logDiv.scrollHeight;
    }
    
    // Set the client name in the UI
    function setClientNameUI(clientName) {
        var div = document.getElementById('client-name');
        div.innerHTML = 'Your client name: <strong>' + clientName +
                '</strong>';
    }
  7. 「config.example.php」,設定檔。
    config.example.php 改名 config.php
    修改 config.php
    $TWILIO_ACCOUNT_SID = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 前面取得的 ACCOUNT SID
    $TWILIO_AUTH_TOKEN = 'your_auth_token'; //前面取得的 AUTH TOKEN
    $TWILIO_TWIML_APP_SID = 'APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 前面取得的 TwiML App SID
    $TWILIO_CALLER_ID = '+1XXXYYYZZZZ'; // 向 Twilio 買的有效的號碼(有語音功能)
    
  8. 「token.php」, JS 一開始會訪問「token.php」,「token.php」會產生給 JS 用的 token
    範例中只有 token是必須的,另一個 identity 可有可無(會出現在 Twilio 的撥號記錄)。
    <?php
    
    include('./vendor/autoload.php');
    include('./config.php');
    include('./randos.php');
    
    use Twilio\Jwt\ClientToken;
    
    // choose a random username for the connecting user
    $identity = randomUsername();
    
    $capability = new ClientToken($TWILIO_ACCOUNT_SID, $TWILIO_AUTH_TOKEN);
    $capability->allowClientOutgoing($TWILIO_TWIML_APP_SID);
    $capability->allowClientIncoming($identity);
    $token = $capability->generateToken();
    
    // return serialized token and the user's randomly generated ID
    header('Content-Type: application/json');
    echo json_encode(array(
        'identity' => $identity,
        'token' => $token,
    ));
    
  9. 「randos.php」,這個檔只是用來產生前面「token.php」的隨機identity,正式應該用不到。
    <?php
    
    // Generate a random username for the connecting client
    function randomUsername() {
        $ADJECTIVES = array(
            'Abrasive', 'Brash', 'Callous', 'Daft', 'Eccentric', 'Fiesty', 'Golden',
            'Holy', 'Ignominious', 'Joltin', 'Killer', 'Luscious', 'Mushy', 'Nasty',
            'OldSchool', 'Pompous', 'Quiet', 'Rowdy', 'Sneaky', 'Tawdry',
            'Unique', 'Vivacious', 'Wicked', 'Xenophobic', 'Yawning', 'Zesty',
        );
    
        $FIRST_NAMES = array(
            'Anna', 'Bobby', 'Cameron', 'Danny', 'Emmett', 'Frida', 'Gracie', 'Hannah',
            'Isaac', 'Jenova', 'Kendra', 'Lando', 'Mufasa', 'Nate', 'Owen', 'Penny',
            'Quincy', 'Roddy', 'Samantha', 'Tammy', 'Ulysses', 'Victoria', 'Wendy',
            'Xander', 'Yolanda', 'Zelda',
        );
    
        $LAST_NAMES = array(
            'Anchorage', 'Berlin', 'Cucamonga', 'Davenport', 'Essex', 'Fresno',
            'Gunsight', 'Hanover', 'Indianapolis', 'Jamestown', 'Kane', 'Liberty',
            'Minneapolis', 'Nevis', 'Oakland', 'Portland', 'Quantico', 'Raleigh',
            'SaintPaul', 'Tulsa', 'Utica', 'Vail', 'Warsaw', 'XiaoJin', 'Yale',
            'Zimmerman',
        );
    
        // Choose random components of username and return it
        $adj = $ADJECTIVES[array_rand($ADJECTIVES)];
        $fn = $FIRST_NAMES[array_rand($FIRST_NAMES)];
        $ln = $LAST_NAMES[array_rand($LAST_NAMES)];
    
        return $adj . $fn . $ln;
    }
    
  10. 「voice.php」,回應 Twilio,Twilio 會根據回應的內容做對應的動作。
    所以回應內容前,也可自己做一些驗證,例如 Twilio 傳送過來的 ACCOUNT SID、TwiML App SID 是否正確,或自己額外傳送的資料是否正常。
    voice.php 的網址須為「Voice 的 REQUEST URL」填的網址。
    <?php
    
    include('./vendor/autoload.php');
    include('./config.php');
    
    #$response = new \Twilio\Twiml; //SDK 5.4.2
    $response = new \Twilio\TwiML\VoiceResponse();; //SDK 6.40.1
    
    // get the phone number from the page request parameters, if given
    if (isset($_REQUEST['To']) && strlen($_REQUEST['To']) > 0) {
        $number = htmlspecialchars($_REQUEST['To']);
        #$dial = $response->dial(array('callerId' => $TWILIO_CALLER_ID)); //SDK 5.4.2
        $dial = $response->dial("", array('callerId' => $TWILIO_CALLER_ID)); //SDK 6.40.1
    
        // wrap the phone number or client name in the appropriate TwiML verb
        // by checking if the number given has only digits and format symbols
        if (preg_match("/^[\d\+\-\(\) ]+$/", $number)) {
            $dial->number($number); //播電話給$number這個號碼
        } else {
            $dial->client($number);
        }
    } else {
        $response->say("Thanks for calling!");//會用語音講"Thanks for calling"
    }
    
    header('Content-Type: text/xml');
    echo $response;
    
    //因要給Twilio 訪問,所以是公開的網址,因此也可以自行加些驗證判斷,不合法就回應404
    //header("HTTP/1.0 404 Not Found");
    

    最後結果產生的 XML 格式如下,
    用買的電話號碼 $TWILIO_CALLER_ID(+447*********),撥電話給 $number(+886*********)
    <?xml version="1.0" encoding="UTF-8"?>
    <Response><Dial callerId="+447*********"><Number>+886*********</Number></Dial></Response>
  11. Twilio 發送紀錄:https://www.twilio.com/console/voice/dashboard
    發送失敗debug:https://www.twilio.com/console/dev-tools/debugger


參考:

其他:
Create a Real-Time Video Chat Room with WebRTC & Twilio — SitePoint
https://docs.microsoft.com/zh-tw/azure/partner-twilio-php-make-phone-call
http://twimlets.com/message?Message=test
https://cloud.google.com/appengine/docs/flexible/nodejs/using-sms-and-voice-services-via-twilio
https://www.npmjs.com/package/twilio-js


沒有留言:

張貼留言