The project is in a healthy, maintained state
Allow to setup one-to-many P2P stream connections (WebRTC) between clients through Rails server (ActionCable) as the signaling server
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

 Project Readme

P2pStreamsChannel

Allow to setup one-to-many P2P stream connections (WebRTC) between clients through Rails server (ActionCable) as the signaling server.

Work Flow

===================================
Connect to Signaling Server (Rails)
===================================

host-user -------------------------------------> Rails server
host-user <-----views/chat_room----------------- Rails server
            (session: chat_room, peer_id: host_user)
host-user ---turbo-connect-stream--------------> Rails server/ActionCable

client-user -----------------------------------> Rails server
client-user <-----views/chat_room--------------- Rails server
        (session: chat_room, peer_id: client_user)
client-user ---turbo-connect-stream------------> Rails server/ActionCable

...
other clients
...


===========
Negotiation
===========

host-user ------SessionReady--> Rails Server --> client-user
client-user ----SessionReady--> Rails Server --> host-user
host-user --setupRTCPeerConnection --+
       <-----------------------------+  

host-user ------SdpOffer -----> Rails Server --> client-user
client-user --setupRTCPeerConnection --+
        <------------------------------+  

client-user ----SdpAnswer ----> Rails Server --> host-user

iceServers --ice-candidate----> host-user ----> client-user
iceServers --ice-candidate----> client-user --> host-user


=========
Connected
=========

After a client-user connected to the host-user, it'll be disconnected to the signaling server (in order to save memory).
Only the host-user keep connect to Rails server.
In case you want keep client connection, you could set params `keepCableConnection: true` to the p2p-frame.

client-user1 ----X disconnect from -----> Rails server Action cable
client-user2 ----X disconnect from -----> Rails server Action cable
...
host-user <-------keep connect to ------> Rails server Action cable

This is one-to-many p2p connections:
client-user1 --- send message 'hi' ---> host-user
host-user --- send message {user1: 'hi'} to --> others


============
Disconnected
============

client-user1 ---X disconnect ---> host-user
host-user ---> send client-user1 status ---> others

client-user1 ----reload --------> Rails server
host-user <--- start re-negotiating through Rails server ----> client-user1


host-user ---X disconnect ---> Rails server
all client-users will be disconnected
the first client-user re-connect to Rails server will become the new host
and start work-flow again

Installation

$ gem "p2p_streams_channel"
$ bundle isntall
$ rails g p2p_streams_channel:install

Usage

Render a p2p-frame-tag

# views/chat_rooms/_chat_room.html.erb
<%= p2p_frame_tag(
    session_id: dom_id(chat_room), 
    peer_id: dom_id(current_user),
    expires_in: 1.hour,	
    # config: {
        # ice_servers: [
		#     { urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"] },
	    # ],
        # heartbeat: {
        #     interval_mls: 100,
        #     idle_timeout_mls: 200 
        # },
        # keepCableConnection: false
    # }
) do %>
    <div data-controller="chat">
        # chat room views
    </div>
<% end %>

Create a Stimulus P2pController in which you will receive other p2p-connections status, data and send back your data to others.

$ rails g p2p_streams_channel:controller chat
# it will create js file `app/javascript/controllers/chat_controller.js`
// app/javascript/controllers/chat_controller.js
import { P2pController } from "p2p"
export default class extends P2pController {
    //
    // p2p callbacks
    //
    p2pNegotiating() {
        // your peer start to negotiate with the host through ActionCable
        console.log("NEGOTIATING ...")
        this.showConnecting()
    }

    p2pConnecting() {
        // your peer is connecting directly with the host peer
        console.log("CONNECTING ...")
    }

    p2pConnected() {
        // your peer's connected to the host peer
        // from now you could start send message to the other through the host peer
        console.log("CONNECTED ...")
        this.hideConnecting()
        this.showChatBox()
    }

    p2pDisconnected() {
        // your peer's disconnected from the host peer
        this.showConnecting()
    }

    p2pClosed() {}

    p2pError() {}

    // receiving message from the other through the host peer
    p2pReceivedMessage(message) {
        switch(message["type"]) {
            case "Data":
                // message["data"]: the text message
                const chatLine = document.createElement("div")
                chatLine.innerText = `${message["senderId"]}: ${message["data"]}`
                this.chatBoxTarget.append(chatLine)
                break
            case "Data.Connection.State":
                // message["data"]: the current connection state of other peers
                for (let [peer, state] of Object.entries(message["data"])) {
                    this.updatePeerState(peer, state)
                }
                break
            default:
                break
        }
    }

    //
    // send message to the others through the host peer:
    // for example:
    // send-button in message box will trigger this action
    send() {
        this.p2pSendMessage(this.messageBoxTarget.value)
        this.messageBoxTarget.value = ""
    }

    // others:
    // this.iamHost: your peer is the host or not
    // this.peerId: your peer id
    // this.hostPeerId: the host peer id
    //
}

Session Store

Session Store will cache p2p session (with session_id key you provide in p2p_frame_tag), this session contains peers infomation, especially the current host peer which will automatically negotiate with the new peers or re-connect peers.

Rails.cache is the default store. You could implement store by yourself, make sure your store can fetch, write, and read. Then set up in initializer:

# config/initializers/p2p_streams_channel.rb
P2pStreamsChannel.config do |config|
    config.store = YourStore.new
end

Development

run test:

$ rake spec

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/p2p_streams_channel.