You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关于使用Twilio WebRTC实现JS端一对一外呼视频通话的可行性及简易示例问询

Can Twilio Be Used for One-to-One Outbound Video Calls?

Yes, absolutely! You can build one-to-one outbound video calls using Twilio's Programmable Voice (to initiate the outbound call) and Programmable Video (to handle the real-time video connection). Let me walk you through how it works and share a simplified JavaScript implementation to get you up and running.

How It Works

  1. Initiate the Outbound Call: Use Twilio Programmable Voice to dial the recipient's phone number.
  2. Redirect to Video: When the recipient answers, Twilio serves them a TwiML response that sends them to a web endpoint hosting a video-enabled page.
  3. Join a Shared Video Room: Both the caller and recipient connect to the same Twilio Video room using secure access tokens, enabling bidirectional video and audio.

Prerequisites

Before diving in, make sure you have:

  • A Twilio account (sign up if you don’t have one)
  • A Twilio phone number capable of making voice calls
  • Your Twilio credentials: Account SID, Auth Token, and an API Key pair (create these in the Twilio Console)
  • ngrok (to expose your local server to the internet—Twilio needs to reach your endpoints)
  • Node.js and npm installed

Example Implementation

We’ll split this into a backend (Node.js/Express) to handle call logic and token generation, and frontend JavaScript to manage the video connection.

Backend Code (Node.js/Express)

First, install the required packages:

npm install express twilio

Create your server file (e.g., server.js):

const express = require('express');
const twilio = require('twilio');
const app = express();
app.use(express.urlencoded({ extended: true }));

// Replace these with your actual Twilio credentials
const accountSid = 'YOUR_TWILIO_ACCOUNT_SID';
const authToken = 'YOUR_TWILIO_AUTH_TOKEN';
const apiKeySid = 'YOUR_TWILIO_API_KEY_SID';
const apiKeySecret = 'YOUR_TWILIO_API_KEY_SECRET';
const twilioPhoneNumber = '+1234567890'; // Your Twilio number

const client = twilio(accountSid, authToken);

// Endpoint to trigger the outbound call
app.post('/start-call', async (req, res) => {
  const recipientNumber = req.body.recipient;

  try {
    const call = await client.calls.create({
      to: recipientNumber,
      from: twilioPhoneNumber,
      url: 'https://your-ngrok-subdomain.ngrok.io/handle-call', // Replace with your ngrok URL
    });
    res.json({ success: true, callSid: call.sid });
  } catch (err) {
    res.status(500).json({ success: false, error: err.message });
  }
});

// TwiML endpoint to handle the answered call
app.post('/handle-call', (req, res) => {
  const twiml = new twilio.twiml.VoiceResponse();
  twiml.say('Hi there! We\'re connecting you to a video call now. Please open the link that appears on your screen.');
  // Redirect to the video room page
  twiml.redirect({ method: 'POST' }, 'https://your-ngrok-subdomain.ngrok.io/join-video');
  res.type('text/xml');
  res.send(twiml.toString());
});

// Endpoint to generate video room access token and serve the video page
app.post('/join-video', async (req, res) => {
  const participantIdentity = req.body.From || 'caller-user'; // Use caller number or custom identity
  const roomName = `private-call-${Date.now()}`; // Unique room name per call

  // Create the video room
  await client.video.v1.rooms.create({
    uniqueName: roomName,
    type: 'group', // Supports up to 50 participants, perfect for one-to-one
  });

  // Generate an access token for the participant
  const AccessToken = twilio.jwt.AccessToken;
  const VideoGrant = AccessToken.VideoGrant;

  const token = new AccessToken(accountSid, apiKeySid, apiKeySecret);
  token.identity = participantIdentity;

  const videoGrant = new VideoGrant({ room: roomName });
  token.addGrant(videoGrant);

  // Serve the HTML page with video SDK and connection logic
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Twilio Video Call</title>
        <script src="https://sdk.twilio.com/js/video/releases/2.27.0/twilio-video.min.js"></script>
        <style>
          .video-grid { display: flex; gap: 20px; margin: 20px; }
          video { width: 400px; height: 300px; border: 2px solid #333; border-radius: 8px; }
        </style>
      </head>
      <body>
        <h1>Connected to Video Call</h1>
        <div class="video-grid">
          <div>
            <h3>Your Video</h3>
            <div id="localVideo"></div>
          </div>
          <div>
            <h3>Participant Video</h3>
            <div id="remoteVideo"></div>
          </div>
        </div>
        <script>
          const token = "${token.toJwt()}";
          const roomName = "${roomName}";

          // Connect to the video room
          Twilio.Video.connect(token, {
            room: roomName,
            audio: true,
            video: { width: 400, height: 300 }
          }).then(room => {
            console.log('Joined room:', room.name);

            // Show local video
            const localDiv = document.getElementById('localVideo');
            room.localParticipant.tracks.forEach(track => {
              if (track.kind === 'video') localDiv.appendChild(track.attach());
            });

            // Handle remote participant joining
            room.on('participantConnected', participant => {
              console.log('Participant joined:', participant.identity);
              // Show their video when it's available
              participant.on('trackPublished', publication => {
                publication.on('subscribed', track => {
                  document.getElementById('remoteVideo').appendChild(track.attach());
                });
              });
            });

            // Handle participant leaving
            room.on('participantDisconnected', participant => {
              console.log('Participant left:', participant.identity);
              document.getElementById('remoteVideo').innerHTML = '<p>Participant has left the call</p>';
            });
          }).catch(err => {
            console.error('Failed to connect:', err);
            alert('Failed to join video call. Please try again.');
          });
        </script>
      </body>
    </html>
  `);
});

// Serve the caller initiation page
app.get('/caller', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>Start Video Call</title>
    </head>
    <body>
      <h1>Initiate Outbound Video Call</h1>
      <input type="text" id="recipient" placeholder="Enter recipient number (e.g., +1234567890)">
      <button onclick="startCall()">Start Call & Join Video</button>
      <div class="video-grid" style="display: none; margin: 20px;">
        <div id="localVideo"></div>
        <div id="remoteVideo"></div>
      </div>

      <script>
        async function startCall() {
          const recipient = document.getElementById('recipient').value;
          // Trigger the outbound call via our backend
          const callResponse = await fetch('/start-call', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: \`recipient=\${encodeURIComponent(recipient)}\`
          });
          const data = await callResponse.json();
          if (data.success) {
            alert('Call initiated! Joining video room...');
            document.querySelector('.video-grid').style.display = 'flex';
            // Join the video room as the caller
            const tokenResponse = await fetch('/join-video', {
              method: 'POST',
              headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
              body: 'From=caller-user'
            });
            const html = await tokenResponse.text();
            // Extract token from the response
            const tokenMatch = html.match(/const token = "(.*?)";/);
            if (tokenMatch) {
              const token = tokenMatch[1];
              const roomMatch = html.match(/const roomName = "(.*?)";/);
              const roomName = roomMatch[1];
              // Connect to the room
              Twilio.Video.connect(token, {
                room: roomName,
                audio: true,
                video: true
              }).then(room => {
                room.localParticipant.tracks.forEach(track => {
                  document.getElementById('localVideo').appendChild(track.attach());
                });
                room.on('participantConnected', participant => {
                  participant.on('trackPublished', pub => {
                    pub.on('subscribed', track => {
                      document.getElementById('remoteVideo').appendChild(track.attach());
                    });
                  });
                });
              });
            }
          } else {
            alert('Failed to start call: ' + data.error);
          }
        }
      </script>
    </body>
    </html>
  `);
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

How to Test

  1. Run your server: node server.js
  2. Start ngrok to expose your local port: ngrok http 3000
  3. Replace all instances of https://your-ngrok-subdomain.ngrok.io in the code with your actual ngrok URL
  4. Open http://localhost:3000/caller in your browser, enter the recipient's phone number, and click "Start Call & Join Video"
  5. When the recipient answers, they’ll be redirected to the video page in their browser (mobile browsers work too!)
  6. Both parties should now see each other's video and hear audio

Key Notes

  • Credentials: Never hardcode your Twilio credentials in production—use environment variables instead.
  • Room Management: The example generates a unique room name per call, which is better than a fixed name for scalability.
  • Error Handling: Add more robust error handling for edge cases (e.g., missed calls, network drops) in a production app.
  • Mobile Support: For a smoother mobile experience, consider building native apps using Twilio's iOS/Android Video SDKs.

内容的提问来源于stack exchange,提问作者dan_0

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 18:57:46