Allow to setup one-to-many P2P stream connections (WebRTC) between clients through Rails server (ActionCable) as the signaling server.
===================================
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
$ gem "p2p_streams_channel"
$ bundle isntall
$ rails g p2p_streams_channel:install
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 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
run test:
$ rake spec
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/p2p_streams_channel.