What is WebRTC and How to Use It in Ruby on Rails (Step-by-Step Tutorial)

WebRTC Tutorial with Ruby on Rails – Step-by-Step Guide

WebRTC in Ruby on Rails

Table of Contents

What is WebRTC?

WebRTC (Web Real-Time Communication) is a free and open-source technology that allows two users to talk to each other using video, voice, or data directly in their browsers — without needing to install anything.

Think of it like Zoom or Google Meet, but built into your website. It works on most modern browsers and lets users:

  • Start a video or audio call directly in the browser
  • Share files with other users instantly
  • Stream media in real time (live streaming)

WebRTC is designed for peer-to-peer communication. That means once the connection is made, the data goes directly from one person’s device to the other — fast and private.

Why Use WebRTC in a Rails App?

By using WebRTC in your Rails application, you can add real-time communication features without relying on external video platforms or services.

Here’s why it’s useful in Rails:

  • No plugins: Users don’t have to install Zoom or Skype
  • Free: No cost for the actual video engine
  • Secure: Encrypted by default (HTTPS + DTLS/SRTP)
  • Works well with Rails ActionCable: Use ActionCable for signaling between users

Whether you’re building a remote doctor consultation app, live tutoring platform, or customer support chat, WebRTC gives you the real-time layer — and Rails gives you everything else.

How WebRTC Works – Core Concepts

WebRTC connects two people directly so they can talk via video, voice, or share data without using a middle server for the actual media. But to do that, some setup is needed behind the scenes.

Here are the key building blocks of how WebRTC works:

1. Media Stream (Camera & Mic)

WebRTC uses your device’s camera and microphone through the browser with JavaScript. The API to access them is getUserMedia().


    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    .then(stream => { /* send or play stream */ });
      

2. Peer Connection

A RTCPeerConnection is used to create the connection between two users. It manages everything about the media flow, including quality and network.

3. Signaling (Using Action Cable or WebSocket)

WebRTC can’t create the connection on its own. Browsers need to talk first and share “connection info” like:

  • Offer (how to connect)
  • Answer (I agree to connect this way)
  • ICE Candidates (network addresses)
This is called signaling. Rails ActionCable (WebSockets) is used for this exchange.

4. STUN Server

A STUN server helps each browser find its public IP address. This is needed when people are behind routers or firewalls.

5. TURN Server (Backup Plan)

If a direct connection fails (e.g., corporate firewalls), a TURN server is used to relay the media. It’s slower but works as a backup.

6. ICE Framework

ICE (Interactive Connectivity Establishment) is the protocol that tests different ways to connect (using STUN, TURN, local network, etc.) and picks the best one.

7. Data Channels

Besides audio and video, WebRTC can send any data between users using RTCDataChannel. Think of it like sending files or live messages.

Real-Life Example (Simple Flow)

  1. User A joins the app and clicks “Start Call”
  2. Their browser captures audio/video and creates an “offer”
  3. The offer is sent to User B via ActionCable (signaling)
  4. User B accepts and sends back an “answer”
  5. Both browsers exchange ICE candidates
  6. Once connected, video/audio goes directly from A ↔ B

Important: The server (Rails) helps with signaling but does NOT carry the media. That happens directly between users’ browsers.

Difference Between Action Cable and WebRTC

Although both Action Cable and WebRTC are used in real-time applications, they serve completely different purposes.

FeatureAction Cable (Rails)WebRTC
What it isA WebSocket framework built into Ruby on RailsA browser-based peer-to-peer media and data communication technology
Communication typeClient ⇄ ServerPeer ⇄ Peer (Direct browser-to-browser)
Used forReal-time messages, chats, notificationsVideo calls, audio calls, file sharing, screen sharing
Media supportDoes NOT support audio/video transferSupports real-time audio/video/media
LatencyLow latency, server in-betweenUltra low latency (peer-to-peer)
Needs a server?Yes (Rails server with ActionCable)Only for signaling (initial connection), then direct
Common use casesLive chat, notifications, game updates, dashboardsVideo calls, telemedicine, live education, screen share
Browser supportWorks anywhere with JS + WebSocketsModern browsers only (Chrome, Firefox, Safari, etc.)

How They Work Together

In most WebRTC apps built with Rails, you use Action Cable to exchange connection details (signaling), and then let WebRTC handle the real-time video/audio between users.

For example:
– Action Cable sends the “offer” and “answer” between users
– WebRTC builds the direct video/audio connection between browsers

WebRTC Architecture in Rails

WebRTC handles the real-time media, but your Rails app plays a crucial role in setting up and managing the connection between users. This is where the architecture comes in.

🧠 Key Components

  • Client (Browser): Captures camera/microphone and initiates the call.
  • Rails Server: Handles authentication, authorization, and room/session setup.
  • ActionCable (WebSocket): Acts as a signaling server to exchange connection info between peers.
  • STUN/TURN Servers: Help browsers find the best way to connect (especially behind firewalls).
  • RTCPeerConnection: Manages the actual media/data connection between users.

📊 High-Level Flow (Rails + WebRTC)

  1. User A opens the app and starts a video call.
  2. User A’s browser uses getUserMedia() to access video/audio.
  3. User A creates a WebRTC offer and sends it via ActionCable to the Rails server.
  4. Rails server broadcasts this offer to User B using ActionCable (WebSocket).
  5. User B receives the offer, creates an answer, and sends it back through ActionCable.
  6. Both users exchange ICE candidates via ActionCable.
  7. Once the signaling is complete, a direct P2P WebRTC connection is established between both browsers.
  8. Media (video/audio) is streamed peer-to-peer without going through Rails.

📦 Folder Structure Example in Rails


    app/
    ├── controllers/
    │   └── calls_controller.rb      # Handles room auth, redirection
    ├── channels/
    │   └── video_call_channel.rb    # Signaling logic via ActionCable
    ├── views/
    │   └── calls/show.html.erb      # Loads JS to handle WebRTC
    ├── javascript/
    │   └── channels/video_call.js   # WebRTC logic (peer connection, ICE, streams)
      

🧩 Tech Stack Overview

ComponentToolPurpose
BackendRuby on RailsManages user flow, sessions
Realtime SignalingActionCable (WebSocket)Exchange offers/answers/ICE
Media CaptureJavaScript (getUserMedia)Access camera/mic
P2P ConnectionRTCPeerConnectionStream audio/video directly
Fallback RelayTURN ServerSend media if P2P fails

🎯 When to Use This Architecture

  • Telehealth platforms (doctor ↔ patient)
  • Online interview tools
  • Video customer support in apps
  • One-on-one or small group tutoring

WebRTC + Rails gives you full control over your real-time communication app without relying on third-party platforms. ActionCable makes it easy to plug signaling into your Rails system.

Key Terms & Vocabulary in WebRTC

When building or debugging WebRTC apps — especially in a Rails + JavaScript setup — you’ll come across several technical terms. Here’s a simple glossary of what each one means and why it matters.

TermMeaning / Usage
getUserMedia()A JavaScript method to access the user’s camera and microphone.
RTCPeerConnectionThe core object that manages audio/video/data exchange between two browsers.
RTCSessionDescriptionDescribes an offer or answer. Required to negotiate the WebRTC connection.
ICE (Interactive Connectivity Establishment)A method to find the best possible path to connect two peers across networks.
ICE CandidateA possible network address that can be used to connect the peers (like IP and port).
SignalingThe process of exchanging offers, answers, and ICE candidates between peers — done via WebSocket or ActionCable in Rails.
STUN ServerHelps a peer discover its public IP address and port behind NAT.
TURN ServerRelays media traffic when direct peer-to-peer connection isn’t possible.
SDP (Session Description Protocol)A text format used to describe multimedia sessions (codecs, ports, etc.) sent in offer/answer.
MediaStreamRepresents the stream of audio/video tracks from the user’s device.
trackA single audio or video source within a MediaStream (e.g., microphone or camera).
DataChannelOptional feature to send text or binary data peer-to-peer without a server.
onicecandidateAn event triggered when a new ICE candidate is found. Used to send it to the other peer.
ontrackAn event that receives the remote stream once the connection is established.

These terms form the building blocks of any WebRTC app. Understanding them helps you implement, debug, and extend features like video calls, screen sharing, or file transfer efficiently in your Rails projects.

Basic WebRTC Implementation (JavaScript Only)

This example covers just the frontend part — enough to:

  • Access the camera and microphone
  • Create a peer connection
  • Display your own video stream
  • Prepare for signaling (offer/answer/candidates) — but not send yet

🔧 HTML Setup

<video id="localVideo" autoplay muted playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
  

📜 JavaScript (Core WebRTC Setup)


const localVideo = document.getElementById("localVideo");
const remoteVideo = document.getElementById("remoteVideo");

let localStream;
let peerConnection;

// STUN server (public one by Google)
const iceConfig = {
  iceServers: [
    { urls: "stun:stun.l.google.com:19302" }
  ]
};

// Step 1: Get user media
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    localStream = stream;
    localVideo.srcObject = stream;
    initializePeerConnection();
  })
  .catch(error => {
    console.error("Error accessing media devices.", error);
  });

// Step 2: Create peer connection
function initializePeerConnection() {
  peerConnection = new RTCPeerConnection(iceConfig);

  // Add local tracks to peer connection
  localStream.getTracks().forEach(track => {
    peerConnection.addTrack(track, localStream);
  });

  // Receive remote stream
  peerConnection.ontrack = event => {
    remoteVideo.srcObject = event.streams[0];
  };

  // ICE candidate generation (to send to other peer via backend)
  peerConnection.onicecandidate = event => {
    if (event.candidate) {
      console.log("Generated ICE candidate:", event.candidate);
      // You would send this to the other user using signaling
    }
  };

  // (Optional) Connection state logging
  peerConnection.onconnectionstatechange = () => {
    console.log("Connection state:", peerConnection.connectionState);
  };
}
  

✅ What This Does

  • Grabs camera/mic using getUserMedia()
  • Creates a WebRTC connection using RTCPeerConnection
  • Attaches local media to the connection
  • Prepares to receive a remote stream (to be added later)

Next Step: To complete the connection, you’ll need signaling via backend (e.g., ActionCable in Rails) to exchange:

  • Offer
  • Answer
  • ICE candidates

Example: One-to-One Video Call in Rails

Let’s build a basic one-to-one video call feature using WebRTC and Rails. We’ll use JavaScript for capturing video/audio and ActionCable for signaling between two users.

🔧 Step-by-Step Implementation

1. Create a Call Room

Add a simple controller to manage the call room:

# app/controllers/calls_controller.rb
    class CallsController < ApplicationController
      def show
        @room_id = params[:id]
      end
    end
      

2. Create a Route

# config/routes.rb
    get '/call/:id', to: 'calls#show'
      

3. Setup ActionCable Channel

# app/channels/video_call_channel.rb
    class VideoCallChannel < ApplicationCable::Channel
      def subscribed
        stream_from "video_call_#{params[:room]}"
      end
    
      def receive(data)
        ActionCable.server.broadcast("video_call_#{params[:room]}", data)
      end
    end
      

4. Frontend HTML

<video id="localVideo" autoplay muted playsinline></video>
    <video id="remoteVideo" autoplay playsinline></video>
      

5. JavaScript: WebRTC + Signaling


    // app/javascript/channels/video_call.js
    import consumer from "./consumer";
    
    const peer = new RTCPeerConnection({
      iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
    });
    
    const room = window.location.pathname.split("/").pop();
    const channel = consumer.subscriptions.create(
      { channel: "VideoCallChannel", room },
      {
        received(data) {
          if (data.offer) {
            peer.setRemoteDescription(new RTCSessionDescription(data.offer));
            peer.createAnswer().then(answer => {
              peer.setLocalDescription(answer);
              channel.send({ answer });
            });
          } else if (data.answer) {
            peer.setRemoteDescription(new RTCSessionDescription(data.answer));
          } else if (data.candidate) {
            peer.addIceCandidate(new RTCIceCandidate(data.candidate));
          }
        }
      }
    );
    
    navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
      document.getElementById("localVideo").srcObject = stream;
      stream.getTracks().forEach(track => peer.addTrack(track, stream));
    });
    
    peer.onicecandidate = event => {
      if (event.candidate) {
        channel.send({ candidate: event.candidate });
      }
    };
    
    peer.ontrack = event => {
      document.getElementById("remoteVideo").srcObject = event.streams[0];
    };
    
    // Initial offer (if first user)
    peer.createOffer().then(offer => {
      peer.setLocalDescription(offer);
      channel.send({ offer });
    });
      

✅ How It Works

  • User A opens the room → creates an offer
  • User B joins the same room → responds with an answer
  • ICE candidates are exchanged to complete the connection
  • Once connected, browser streams video/audio directly peer-to-peer

📌 Notes

  • This example is one-to-one (room shared by 2 users)
  • Media never passes through the Rails server — only signaling does
  • You can expand this to multiple users with some changes

10 Real-World Use Cases of WebRTC

WebRTC is not just for video calls — it’s a powerful technology used in many real-world applications across industries. Here are ten practical examples:

  1. Telemedicine
    Doctors and patients can talk face-to-face from anywhere using secure, real-time video sessions — reducing in-person visits and improving remote care.
  2. Online Education & Tutoring
    Teachers and students connect through virtual classrooms with live video, screen sharing, and even whiteboarding tools.
  3. Customer Support Video Chat
    Businesses offer live face-to-face video support on their website, improving trust and resolving issues faster.
  4. Remote Interviews
    Companies conduct job interviews via WebRTC, often using custom-built Rails apps that include time tracking and candidate evaluations.
  5. Social Media Video Calls
    Platforms like Facebook and Snapchat use WebRTC to enable real-time communication between friends — from video calls to filters.
  6. Gaming Voice Chat
    In multiplayer online games, WebRTC provides low-latency voice communication for team coordination.
  7. Secure Corporate Video Conferencing
    Enterprises build private, in-house video chat systems using WebRTC for internal meetings, reducing reliance on third-party apps like Zoom.
  8. Live Auctions or Sales Events
    Bidders can watch and participate in live auctions through video and voice, enhancing engagement in real time.
  9. Real-Time Collaboration Tools
    Apps like Miro and Figma use WebRTC for live collaboration, screen sharing, and feedback during real-time design or coding sessions.
  10. Peer-to-Peer File Sharing
    WebRTC’s data channels allow users to share large files without uploading to a central server — directly browser to browser.

These use cases show just how flexible WebRTC is — whether you’re building a healthcare platform or adding a simple chat feature to your Rails app, WebRTC helps you go real-time with ease.

Security & Privacy Considerations in WebRTC

WebRTC is designed with security in mind from the start — but that doesn’t mean you can skip implementation best practices. Below are key security and privacy points every developer should know when using WebRTC in a Rails application.

🔒 Built-in WebRTC Security Features

  • Encryption by Default: All WebRTC streams (video, audio, and data) are encrypted using DTLS (Datagram Transport Layer Security) and SRTP (Secure Real-time Transport Protocol).
  • Secure Transport: WebRTC only works on HTTPS pages or localhost (to prevent man-in-the-middle attacks).
  • No Plugin Risks: Since it runs natively in the browser, there are no plugin-based vulnerabilities like Flash or Java applets.
  • Permission-Based Media Access: The browser asks users for explicit permission before accessing their microphone or camera.

🚨 Security Considerations for Rails Developers

  • Use HTTPS Everywhere: WebRTC will not work on non-secure HTTP. Ensure your Rails app uses SSL.
  • Validate User Sessions: Use Rails authentication (Devise or similar) to ensure only authorized users can access call rooms.
  • Protect Signaling Channel (ActionCable): Secure WebSocket communication using authentication and encrypted channels.
  • Don’t Trust Client Data: Even though media is peer-to-peer, always verify any data or metadata coming through WebSocket signaling.
  • Tokenize Rooms or Calls: Use temporary, expiring room tokens so users can’t guess room IDs (e.g., `/call/abc123xyz` instead of `/call/1`).

🕵️ Privacy Best Practices

  • Ask Only What You Need: If you don’t need audio, request only video — respect the user’s privacy.
  • Use Muted/Hidden Streams Until Consent: Load the camera but don’t show or transmit video until the user confirms.
  • Allow Leaving or Disabling Mic/Camera: Let users control their devices anytime during the call.
  • Don’t Record Without Consent: Always show a visual cue if recording is taking place, and ask for user permission.

🛡️ Extra Protection (for Production)

  • Implement TURN servers with access control (to avoid abuse)
  • Use firewalls and rate-limiting on ActionCable/WebSocket endpoints
  • Use UUIDs and secure random tokens for all room/session identifiers
  • Log connection attempts and disconnections for auditing

With WebRTC, you’re streaming content directly between users — which is great for speed and privacy — but signaling and access control still depend on your Rails backend. Always secure both sides.

WebRTC Interview Questions & Answers

Here are 10 commonly asked WebRTC interview questions with easy-to-understand answers — useful for developers working with real-time features in Rails or any web application.

  1. What is WebRTC?
    A: WebRTC (Web Real-Time Communication) is a browser-based API that enables audio, video, and data communication between peers without needing plugins or external software. It’s used for video calls, screen sharing, and live data transfer.
  2. How does WebRTC establish a connection?
    A: WebRTC uses a signaling mechanism to exchange connection details (offer, answer, and ICE candidates). This is usually handled via WebSockets or tools like Rails ActionCable.
  3. What are STUN and TURN servers?
    A: STUN servers help a device find its public IP address. TURN servers act as a relay when a direct connection between peers fails (e.g., due to firewalls or NAT).
  4. Is WebRTC media encrypted?
    A: Yes. WebRTC uses DTLS and SRTP to encrypt all media and data streams, ensuring secure communication between peers.
  5. What is the role of the signaling server in WebRTC?
    A: The signaling server (e.g., using Rails ActionCable) is used to exchange metadata between peers before they connect. It does not transmit media — only messages for connection setup.
  6. Can WebRTC be used without a backend server?
    A: No. While the media is peer-to-peer, a backend is needed for signaling (e.g., exchanging offers and answers) and authentication.
  7. How does ICE help in WebRTC?
    A: ICE (Interactive Connectivity Establishment) tries different network paths (using STUN/TURN) to find the best way to connect two peers.
  8. What are RTCDataChannels?
    A: RTCDataChannels are part of WebRTC and allow direct, peer-to-peer data transfer — useful for file sharing, chat messages, or real-time syncing.
  9. How does WebRTC integrate with a Rails app?
    A: WebRTC handles media on the frontend. Rails (with ActionCable) can act as the signaling server to help browsers exchange offers, answers, and ICE candidates.
  10. What are the common challenges in WebRTC development?
    A: Handling NAT/firewalls, ensuring compatibility across browsers, securing signaling and media access, and setting up TURN/STUN servers are common challenges.

Tools and Libraries for WebRTC

WebRTC is built into modern browsers, but for real-world apps — especially with Rails — you’ll need extra tools to handle signaling, UI, and server-side logic. Here’s a list of essential tools and libraries:

🧰 Core Tools

  • WebRTC API (Browser): Built-in API used via JavaScript. It includes:
    • getUserMedia() – Access camera and microphone
    • RTCPeerConnection – Connect users directly
    • RTCDataChannel – Share data between peers
  • STUN/TURN Servers: Required for NAT traversal and relay. Examples:
    • Coturn – Free, open-source TURN/STUN server
    • Google STUN Server – stun:stun.l.google.com:19302

🔧 JavaScript Libraries

  • SimpleWebRTC: Easiest way to get started with WebRTC calls. Great for beginners.
    GitHub Repo
  • PeerJS: Simplifies WebRTC peer connections with fallback support.
    https://peerjs.com
  • Adapter.js: A shim library to ensure WebRTC works across different browsers consistently.
    GitHub Repo

🧠 Rails-Specific Tools

  • ActionCable (Rails 5+): Built-in WebSocket system in Rails used for signaling between peers.
  • Redis: Optional, but often used to scale ActionCable (Pub/Sub backend).
  • Devise: Used to manage user sessions and authentication before allowing access to calls.

🖥️ UI Tools (Optional)

  • Bootstrap/Tailwind CSS: To style video call interfaces with responsive layouts.
  • FontAwesome: For adding call buttons, mute, hang-up, and mic icons.

Using these tools together, you can build a fully functional, secure, and scalable video calling app with WebRTC inside a Ruby on Rails project.

Performance Tips and Best Practices for WebRTC

WebRTC is powerful, but real-time communication can be resource-heavy and unpredictable across networks. Here are proven performance tips and best practices to keep your WebRTC + Rails app smooth, secure, and scalable.

📈 Performance Optimization Tips

  • Use TURN servers only when needed: TURN relays are slower and costly. Prefer STUN for faster direct connections.
  • Limit media resolution: Use { video: { width: 640, height: 480 } } to reduce bandwidth and CPU load unless HD is required.
  • Handle network changes: Listen to iceconnectionstatechange and reconnect if needed (mobile users switch Wi-Fi/cellular).
  • Disable unnecessary tracks: Stop or mute unused video/audio tracks to save CPU and bandwidth.
  • Use MediaRecorder wisely: Recording streams can spike CPU. Record only when necessary, and compress offline if possible.

🔐 Security Best Practices

  • Enforce HTTPS: WebRTC will not run without it (except on localhost).
  • Secure signaling (ActionCable): Authenticate users before letting them join or broadcast in rooms.
  • Use short-lived tokens for rooms: Don’t use predictable IDs — generate UUIDs or signed room tokens.
  • Restrict media access: Only request camera/mic when needed and show clear visual cues when streaming starts.

⚙️ Server & App-Level Practices

  • Use Redis for ActionCable: Helps scale your signaling layer in production.
  • Broadcast efficiently: Don’t rebroadcast full stream data — WebRTC handles that. Use ActionCable only for signaling.
  • Track connection states: Log connection start, ICE failures, disconnections for debugging and analytics.
  • Clean up after disconnects: Remove stale peers or close tracks when users leave the room.

🎨 UX Best Practices

  • Show connection status: Display “connecting…”, “waiting…”, “connected”, and error states.
  • Let users control devices: Add buttons to mute audio, turn off video, or switch camera (mobile).
  • Fallback notice: If the user’s browser doesn’t support WebRTC, show a message or offer to download an app.

By following these best practices, your WebRTC application built with Rails will run smoother, consume fewer resources, and provide a better experience for users — even across different networks and devices.

Alternatives to WebRTC

TechnologyProsCons
Zoom SDKEnterprise support, stableExpensive, less customizable
Twilio VideoQuick to integrate, good docsPaid service
AgoraGlobal infrastructure, great APIsRequires API key, cost involved
Daily.coWebRTC-based, easyUsage-limited on free plan

Real World Case Study: Telehealth App

Problem: A clinic needed a secure, real-time video consultation system embedded in their existing Rails app.

Solution: WebRTC was used for peer-to-peer video, and ActionCable handled signaling. The server ensured authentication before allowing access to call rooms.

Outcome: Reduced wait times and remote patient care, no additional licensing costs for third-party video SDKs.

🚀 Complete WebRTC Implementation Tutorial

This comprehensive tutorial walks through the actual implementation of one-to-one video calls in a Rails app using WebRTC and Action Cable. Every code snippet comes from a working project.

📋 What We’re Building

A Rails app where users can:

  • See who’s online in a WhatsApp-style user list
  • Click to start video calls with other users
  • Accept/decline incoming calls
  • Have real-time video/audio conversations

🛠️ Tech Stack

  • Rails 7.1.3 + PostgreSQL
  • Devise for authentication
  • Action Cable for real-time signaling
  • WebRTC for peer-to-peer video/audio
  • Stimulus for frontend logic

📊 Project Setup

Step 1: Create Call Model

# Generate the Call model with all necessary fields
rails generate model Call caller:references callee:references status:string call_type:string room:string
rails db:migrate
      

Step 2: Add Online Status to Users

# Add online status to track which users are currently active
rails generate migration AddOnlineToUsers online:boolean
rails db:migrate
      

🏗️ Models & Associations

User Model (app/models/user.rb)

class User < ApplicationRecord
  # Devise authentication modules
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  # Call associations - a user can be either caller or callee
  has_many :outgoing_calls, class_name: 'Call', foreign_key: :caller_id
  has_many :incoming_calls, class_name: 'Call', foreign_key: :callee_id
end
      

Call Model (app/models/call.rb)

class Call < ApplicationRecord
  belongs_to :caller, class_name: 'User'
  belongs_to :callee, class_name: 'User'

  validates :status, presence: true
  validates :call_type, presence: true
  validates :room, presence: true, uniqueness: true
end
      

📡 Action Cable Channels

UserChannel (app/channels/user_channel.rb)

class UserChannel < ApplicationCable::Channel
  def subscribed
    stream_from "user_#{current_user.id}"
  end
end
      

CallChannel (app/channels/call_channel.rb)

class CallChannel < ApplicationCable::Channel
  def subscribed
    stream_from "call_#{params[:room]}"
  end

  def receive(data)
    ActionCable.server.broadcast("call_#{params[:room]}", data)
  end
end
      

🎮 Controllers & Routes

Routes (config/routes.rb)

Rails.application.routes.draw do
  devise_for :users
  get 'dashboard', to: 'dashboard#index'
  
  resources :calls, only: [:create] do
    member do
      get :room
      post :accept
      post :decline
    end
  end
end
      

Calls Controller (app/controllers/calls_controller.rb)

class CallsController < ApplicationController
  before_action :authenticate_user!

  def create
    callee = User.find(params[:user_id])
    call = Call.create!(
      caller: current_user,
      callee: callee,
      status: "pending",
      call_type: params[:type],
      room: SecureRandom.uuid
    )
    
    ActionCable.server.broadcast("user_#{callee.id}", {
      type: "incoming_call",
      call_id: call.id,
      caller_name: current_user.email,
      call_type: call.call_type,
      room: call.room
    })
    
    render json: { call_id: call.id, room: call.room }, status: :created
  end

  def accept
    call = Call.find(params[:id])
    call.update!(status: "accepted")
    
    [call.caller_id, call.callee_id].each do |user_id|
      ActionCable.server.broadcast("user_#{user_id}", {
        type: "call_accepted",
        call_id: call.id,
        room: call.room
      })
    end
    head :ok
  end

  def room
    @call = Call.find(params[:id])
    unless [@call.caller_id, @call.callee_id].include?(current_user.id)
      redirect_to dashboard_path, alert: 'You are not a participant in this call.'
      return
    end
  end
end
      

🎯 Frontend: Stimulus Controllers

User Call Controller (app/javascript/controllers/user_call_controller.js)

import { Controller } from "@hotwired/stimulus"
import consumer from "channels/consumer"

export default class extends Controller {
  connect() {
    this.currentUserId = this.element.dataset.currentUserId
    this.subscribeToUserChannel()
    this.setupCallButtons()
  }

  subscribeToUserChannel() {
    this.userChannel = consumer.subscriptions.create(
      { channel: "UserChannel" },
      {
        received: (data) => this.handleUserNotification(data)
      }
    )
  }

  handleUserNotification(data) {
    if (data.type === "incoming_call") {
      this.showIncomingCallModal(data)
    } else if (data.type === "call_accepted") {
      window.location.href = `/calls/${data.call_id}/room`
    }
  }

  startCallRequest(userId, type) {
    fetch('/calls', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
      },
      body: JSON.stringify({ user_id: userId, type: type })
    })
    .then(res => res.json())
    .then(data => {
      this.pendingCall = data
      this.showWaitingModal()
    })
  }
}
      

🔗 WebRTC Integration

Call Controller (app/javascript/controllers/call_controller.js)

import { Controller } from "@hotwired/stimulus"
import consumer from "channels/consumer"

export default class extends Controller {
  static targets = ["localVideo", "remoteVideo", "endCallBtn"]

  connect() {
    this.isCaller = localStorage.getItem('isCaller') === 'true'
    localStorage.removeItem('isCaller')
    
    const pathMatch = window.location.pathname.match(/\/calls\/(\d+)\/room/)
    if (pathMatch) {
      this.room = pathMatch[1]
      this.startWebRTCCall(this.room)
    }
  }

  async startWebRTCCall(room) {
    this.room = room
    this.localStream = null
    this.remoteStream = null
    this.peerConnection = null
    this.signaling = null
    
    this.setupSignaling()
    await this.startLocalStream()
    
    if (this.isCaller) {
      this.createOffer()
    }
  }

  setupSignaling() {
    this.signaling = consumer.subscriptions.create(
      { channel: "CallChannel", room: this.room },
      {
        received: (data) => this.handleSignalingData(data)
      }
    )
  }

  async startLocalStream() {
    this.localStream = await navigator.mediaDevices.getUserMedia({ 
      video: true, 
      audio: true 
    })
    this.localVideoTarget.srcObject = this.localStream
  }

  createPeerConnection() {
    this.peerConnection = new RTCPeerConnection({
      iceServers: [
        { urls: "stun:stun.l.google.com:19302" }
      ]
    })
    
    this.localStream.getTracks().forEach(track => {
      this.peerConnection.addTrack(track, this.localStream)
    })
    
    this.peerConnection.ontrack = (event) => {
      if (!this.remoteStream) {
        this.remoteStream = new MediaStream()
        this.remoteVideoTarget.srcObject = this.remoteStream
      }
      event.streams[0].getTracks().forEach(track => {
        this.remoteStream.addTrack(track)
      })
    }
    
    this.peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        this.sendSignal({ type: "candidate", candidate: event.candidate })
      }
    }
  }

  async createOffer() {
    this.createPeerConnection()
    const offer = await this.peerConnection.createOffer()
    await this.peerConnection.setLocalDescription(offer)
    this.sendSignal({ type: "offer", sdp: offer })
  }

  async handleSignalingData(data) {
    if (data.type === "offer" && !this.isCaller) {
      this.createPeerConnection()
      await this.peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp))
      const answer = await this.peerConnection.createAnswer()
      await this.peerConnection.setLocalDescription(answer)
      this.sendSignal({ type: "answer", sdp: answer })
    } else if (data.type === "answer" && this.isCaller) {
      await this.peerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp))
    } else if (data.type === "candidate") {
      if (this.peerConnection && this.peerConnection.remoteDescription) {
        await this.peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate))
      }
    }
  }

  sendSignal(data) {
    if (this.signaling) {
      this.signaling.send(data)
    }
  }

  endCall() {
    this.sendSignal({ type: "call_ended" })
    this.endCallCleanup(true)
  }

  endCallCleanup(redirect = true) {
    if (this.peerConnection) {
      this.peerConnection.close()
      this.peerConnection = null
    }
    
    if (this.localStream) {
      this.localStream.getTracks().forEach(track => track.stop())
      this.localStream = null
    }
    
    if (redirect) {
      window.location.href = "/dashboard"
    }
  }
}
      

🎨 UI Implementation

Dashboard View (app/views/dashboard/index.html.erb)

<div class="container py-5" data-controller="user-call" data-current-user-id="<%= current_user.id %>">
  <div class="user-list-panel">
    <div class="user-list-header">Chats</div>
    <div class="user-list-scroll">
      
      <% if @online_users.any? %>
        <div class="user-list-group-label">Online</div>
        <% @online_users.each do |user| %>
          <div class="user-list-row" data-user-id="<%= user.id %>">
            <%= image_tag('https://ui-avatars.com/api/?name=' + URI.encode_www_form_component(user.email), class: "user-list-avatar") %>
            <span class="user-list-email"><%= user.email %></span>
            <div class="user-list-actions">
              <%= button_to "Video", calls_path(type: 'video', user_id: user.id), method: :post, class: "user-list-call-btn" %>
              <%= button_to "Audio", calls_path(type: 'audio', user_id: user.id), method: :post, class: "user-list-call-btn" %>
            </div>
          </div>
        <% end %>
      <% end %>
    </div>
  </div>
</div>
      

Call Room View (app/views/calls/room.html.erb)

<div class="call-room-container" data-controller="call" data-turbo="false">
  <div id="callStatus" class="call-status">Connecting...</div>
  
  <div class="call-videos">
    <video id="remoteVideo" class="call-video-main" autoplay playsinline data-call-target="remoteVideo"></video>
    <video id="localVideo" class="call-video-small" autoplay playsinline muted data-call-target="localVideo"></video>
  </div>
  
  <div class="call-controls">
    <button id="endCallBtn" class="btn btn-danger btn-lg" data-action="click->call#endCall">
      End Call
    </button>
  </div>
</div>
      

🔄 Complete Flow Walkthrough

Step-by-Step Call Process:

  1. User A clicks “Video Call” on User B
    • user_call_controller.js sends POST to /calls
    • CallsController#create creates Call record
    • Action Cable broadcasts to User B’s channel
  2. User B receives incoming call
    • UserChannel receives notification
    • Modal appears with accept/decline buttons
  3. User B accepts call
    • POST to /calls/:id/accept
    • Both users get “call_accepted” notification
    • Both redirect to /calls/:id/room
  4. WebRTC Connection Setup
    • Both users join CallChannel with room ID
    • Caller creates offer, sends via Action Cable
    • Callee receives offer, creates answer, sends back
    • Both exchange ICE candidates
    • Direct peer-to-peer connection established
  5. Video/Audio Streams
    • Local streams attached to video elements
    • Remote streams received via ontrack event
    • Real-time video/audio communication

All code shown is from the actual working project and can be used as a reference for building similar features!

📚 External Resources & References

Here are additional resources, demo projects, and references to help you learn more about WebRTC and implement it in your Rails applications.

🚀 Demo Projects & Examples

  • WebRTC Calls Demo Project
    A complete Rails application demonstrating WebRTC video calls with Action Cable signaling. Features user authentication, real-time call management, and peer-to-peer video communication.
  • SimpleWebRTC
    The easiest way to get started with WebRTC calls. Great for beginners and prototyping.
  • WebRTC Samples
    Official WebRTC samples and demos from the WebRTC team, covering various use cases and implementations.

📖 Official Documentation

🛠️ Development Tools & Libraries

  • PeerJS
    Simplifies WebRTC peer connections with fallback support and easy-to-use API.
  • WebRTC Adapter
    A shim library to ensure WebRTC works across different browsers consistently.
  • Coturn TURN Server
    Free, open-source TURN/STUN server for NAT traversal and media relay.
  • mediasoup
    Advanced WebRTC SFU (Selective Forwarding Unit) for scalable video conferencing.

🎓 Learning Resources

Conclusion

WebRTC brings powerful, real-time communication to Rails applications. Combined with ActionCable, it enables fully custom video and audio experiences without external services. Perfect for modern apps needing privacy and full-stack control.

Learn more about Rails

45 thoughts on “What is WebRTC and How to Use It in Ruby on Rails (Step-by-Step Tutorial)”

Comments are closed.

Scroll to Top