p2p_streams_channel 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/README.md +210 -0
- data/Rakefile +8 -0
- data/app/assets/javascripts/p2p/index.js +6 -0
- data/app/assets/javascripts/p2p/message.js +22 -0
- data/app/assets/javascripts/p2p/p2p_connection.js +133 -0
- data/app/assets/javascripts/p2p/p2p_controller.js +52 -0
- data/app/assets/javascripts/p2p/p2p_frame.js +152 -0
- data/app/assets/javascripts/p2p/p2p_peer.js +208 -0
- data/app/assets/javascripts/p2p/package.json +14 -0
- data/app/channels/signaling_channel.rb +29 -0
- data/app/helpers/p2p_streams_channel/tag_helper.rb +16 -0
- data/lib/p2p_streams_channel/cache.rb +22 -0
- data/lib/p2p_streams_channel/engine.rb +20 -0
- data/lib/p2p_streams_channel/negotiation.rb +25 -0
- data/lib/p2p_streams_channel/session.rb +57 -0
- data/lib/p2p_streams_channel/session_handler.rb +32 -0
- data/lib/p2p_streams_channel/session_state.rb +33 -0
- data/lib/p2p_streams_channel/version.rb +5 -0
- data/lib/p2p_streams_channel.rb +29 -0
- data/lib/rails/generators/p2p_streams_channel/controller_generator.rb +12 -0
- data/lib/rails/generators/p2p_streams_channel/install_generator.rb +24 -0
- data/lib/rails/generators/p2p_streams_channel/templates/controller.js +51 -0
- data/lib/rails/generators/p2p_streams_channel/templates/initializer.rb +4 -0
- data/p2p_streams_channel.gemspec +38 -0
- data/sig/p2p_streams_channel.rbs +4 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +4 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/p2p_controller.rb +7 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/helpers/p2p_helper.rb +2 -0
- data/spec/dummy/app/javascript/application.js +4 -0
- data/spec/dummy/app/javascript/controllers/application.js +9 -0
- data/spec/dummy/app/javascript/controllers/chat_controller.js +46 -0
- data/spec/dummy/app/javascript/controllers/index.js +11 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +16 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/app/views/p2p/index.html.erb +6 -0
- data/spec/dummy/app/views/p2p/room_chat.html.erb +19 -0
- data/spec/dummy/bin/importmap +4 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config/application.rb +22 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +12 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +68 -0
- data/spec/dummy/config/environments/production.rb +87 -0
- data/spec/dummy/config/environments/test.rb +60 -0
- data/spec/dummy/config/importmap.rb +7 -0
- data/spec/dummy/config/initializers/content_security_policy.rb +25 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +8 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/p2p_streams_channel.rb +4 -0
- data/spec/dummy/config/initializers/permissions_policy.rb +11 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/puma.rb +43 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/config.ru +6 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/test.log +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/tmp/development_secret.txt +1 -0
- data/spec/p2p_peer_status_spec.rb +89 -0
- data/spec/p2p_send_data_spec.rb +52 -0
- data/spec/p2p_streams_channel_spec.rb +7 -0
- data/spec/rails_helper.rb +8 -0
- data/spec/session_spec.rb +29 -0
- data/spec/spec_helper.rb +16 -0
- metadata +259 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a8bda1bfc27255440985ebe30c675e2177ca08bf446b75d6ae9a7cfa5c619c59
|
|
4
|
+
data.tar.gz: 4fd9269cf47645dff407538bb4a817c490623bed92b63f3a691c71c1ea8159c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7df5626c1517a3a18a8d495a2ac2d22bdbd1e7fe3c3f04a87b0ff02eed8b33c9c9c3a3f683b14c86ad157960ce2bb8f058fdd841c6ece438115bc6c098f01020
|
|
7
|
+
data.tar.gz: '082a674eb465701535db76a9d4772658f4bef2dc7b7246e2628ad1afd7af55731259c9041b0e58c9b13840b6f823cd07a4037f9ffa9b85c785285d3b78eaad29'
|
data/.rspec
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# P2pStreamsChannel
|
|
2
|
+
|
|
3
|
+
Allow to setup one-to-many P2P stream connections ([WebRTC](https://webrtc.org/)) between clients through Rails server (ActionCable) as the signaling server.
|
|
4
|
+
|
|
5
|
+
## Work Flow
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
===================================
|
|
10
|
+
Connect to Signaling Server (Rails)
|
|
11
|
+
===================================
|
|
12
|
+
|
|
13
|
+
host-user -------------------------------------> Rails server
|
|
14
|
+
host-user <-----views/chat_room----------------- Rails server
|
|
15
|
+
(session: chat_room, peer_id: host_user)
|
|
16
|
+
host-user ---turbo-connect-stream--------------> Rails server/ActionCable
|
|
17
|
+
|
|
18
|
+
client-user -----------------------------------> Rails server
|
|
19
|
+
client-user <-----views/chat_room--------------- Rails server
|
|
20
|
+
(session: chat_room, peer_id: client_user)
|
|
21
|
+
client-user ---turbo-connect-stream------------> Rails server/ActionCable
|
|
22
|
+
|
|
23
|
+
...
|
|
24
|
+
other clients
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
===========
|
|
29
|
+
Negotiation
|
|
30
|
+
===========
|
|
31
|
+
|
|
32
|
+
host-user ------SessionReady--> Rails Server --> client-user
|
|
33
|
+
client-user ----SessionReady--> Rails Server --> host-user
|
|
34
|
+
host-user --setupRTCPeerConnection --+
|
|
35
|
+
<-----------------------------+
|
|
36
|
+
|
|
37
|
+
host-user ------SdpOffer -----> Rails Server --> client-user
|
|
38
|
+
client-user --setupRTCPeerConnection --+
|
|
39
|
+
<------------------------------+
|
|
40
|
+
|
|
41
|
+
client-user ----SdpAnswer ----> Rails Server --> host-user
|
|
42
|
+
|
|
43
|
+
iceServers --ice-candidate----> host-user ----> client-user
|
|
44
|
+
iceServers --ice-candidate----> client-user --> host-user
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
=========
|
|
48
|
+
Connected
|
|
49
|
+
=========
|
|
50
|
+
|
|
51
|
+
After client-user connected to the host-user, all of them will disconnected to the signaling server (in order to save memory and other things), only the host-user keep connect to Rails server.
|
|
52
|
+
In case you want keep client connection, you could set params `keepCableConnection: true` to the p2p-frame.
|
|
53
|
+
|
|
54
|
+
client-user1 ----X disconnect from -----> Rails server Action cable
|
|
55
|
+
client-user2 ----X disconnect from -----> Rails server Action cable
|
|
56
|
+
...
|
|
57
|
+
host-user <-------keep connect to ------> Rails server Action cable
|
|
58
|
+
|
|
59
|
+
This is one-to-many p2p connections:
|
|
60
|
+
client-user1 --- send message 'hi' ---> host-user
|
|
61
|
+
host-user --- send message {user1: 'hi'} to --> others
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
============
|
|
65
|
+
Disconnected
|
|
66
|
+
============
|
|
67
|
+
|
|
68
|
+
client-user1 ---X disconnect ---> host-user
|
|
69
|
+
host-user ---> send client-user1 status ---> others
|
|
70
|
+
|
|
71
|
+
client-user1 ----reload --------> Rails server
|
|
72
|
+
host-user <--- start re-negotiating through Rails server ----> client-user1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
host-user ---X disconnect ---> Rails server
|
|
76
|
+
all client-users will be disconnected
|
|
77
|
+
the first client-user re-connect to Rails server will become the new host
|
|
78
|
+
and start work-flow again
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
$ gem "p2p_streams_channel"
|
|
86
|
+
$ bundle isntall
|
|
87
|
+
$ rails g p2p_streams_channel:install
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Usage
|
|
91
|
+
|
|
92
|
+
Create a Stimulus P2pController in which you will receive other p2p-connections status, data send by other connected connections, and send your data to others.
|
|
93
|
+
```ruby
|
|
94
|
+
$ rails g p2p_streams_channel:controller chat
|
|
95
|
+
# it will create js file `app/javascript/controllers/chat_controller.js`
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Render a p2p-frame-tag
|
|
99
|
+
```ruby
|
|
100
|
+
# views/chat_rooms/_chat_room.html.erb
|
|
101
|
+
<%= p2p_frame_tag(
|
|
102
|
+
session_id: dom_id(chat_room),
|
|
103
|
+
peer_id: dom_id(current_user),
|
|
104
|
+
# config: {
|
|
105
|
+
# ice_servers: [
|
|
106
|
+
# { urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"] },
|
|
107
|
+
# ],
|
|
108
|
+
# heartbeat: {
|
|
109
|
+
# interval_mls: 100,
|
|
110
|
+
# idle_timeout_mls: 200
|
|
111
|
+
# },
|
|
112
|
+
# keepCableConnection: false
|
|
113
|
+
# }
|
|
114
|
+
) do %>
|
|
115
|
+
<div data-controller="chat">
|
|
116
|
+
# chat room views
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
Now you could implement your client side,
|
|
123
|
+
for example: here is a simple chat controller:
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
// app/javascript/controllers/chat_controller.js
|
|
128
|
+
import { P2pController } from "p2p"
|
|
129
|
+
export default class extends P2pController {
|
|
130
|
+
//
|
|
131
|
+
// p2p callbacks
|
|
132
|
+
//
|
|
133
|
+
p2pNegotiating() {
|
|
134
|
+
// your peer start to negotiate with the host through ActionCable
|
|
135
|
+
console.log("NEGOTIATING ...")
|
|
136
|
+
this.showConnecting()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
p2pConnecting() {
|
|
140
|
+
// your peer is connecting directly with the host peer
|
|
141
|
+
console.log("CONNECTING ...")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
p2pConnected() {
|
|
145
|
+
// your peer's connected to the host peer
|
|
146
|
+
// from now you could start send message to the other through the host peer
|
|
147
|
+
console.log("CONNECTED ...")
|
|
148
|
+
this.hideConnecting()
|
|
149
|
+
this.showChatBox()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
p2pDisconnected() {
|
|
153
|
+
// your peer's disconnected from the host peer
|
|
154
|
+
this.showConnecting()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
p2pClosed() {}
|
|
158
|
+
|
|
159
|
+
p2pError() {}
|
|
160
|
+
|
|
161
|
+
// receiving message from the other through the host peer
|
|
162
|
+
p2pReceivedMessage(message) {
|
|
163
|
+
switch(message["type"]) {
|
|
164
|
+
case "Data":
|
|
165
|
+
// message["data"]: the text message
|
|
166
|
+
const chatLine = document.createElement("div")
|
|
167
|
+
chatLine.innerText = `${message["senderId"]}: ${message["data"]}`
|
|
168
|
+
this.chatBoxTarget.append(chatLine)
|
|
169
|
+
break
|
|
170
|
+
case "Data.Connection.State":
|
|
171
|
+
// message["data"]: the current connection state of other peers
|
|
172
|
+
for (let [peer, state] of Object.entries(message["data"])) {
|
|
173
|
+
this.updatePeerState(peer, state)
|
|
174
|
+
}
|
|
175
|
+
break
|
|
176
|
+
default:
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//
|
|
182
|
+
// send message to the others through the host peer:
|
|
183
|
+
// for example:
|
|
184
|
+
// send-button in message box will trigger this action
|
|
185
|
+
send() {
|
|
186
|
+
this.p2pSendMessage(this.messageBoxTarget.value)
|
|
187
|
+
this.messageBoxTarget.value = ""
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// others:
|
|
191
|
+
// this.iamHost: your peer is the host or not
|
|
192
|
+
// this.peerId: your peer id
|
|
193
|
+
// this.hostPeerId: the host peer id
|
|
194
|
+
//
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
run test:
|
|
201
|
+
```ruby
|
|
202
|
+
$ cd spec/dummy
|
|
203
|
+
$ rails g p2p_streams_channel:install
|
|
204
|
+
$ cd ../..
|
|
205
|
+
$ rake spec
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Contributing
|
|
209
|
+
|
|
210
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/p2p_streams_channel.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const ConnectionState = {
|
|
2
|
+
SessionJoin: "SessionJoin",
|
|
3
|
+
SessionReady: "SessionReady",
|
|
4
|
+
SdpOffer: "SdpOffer",
|
|
5
|
+
SdpAnswer: "SdpAnswer",
|
|
6
|
+
IceCandidate: "IceCandidate",
|
|
7
|
+
Error: "Error",
|
|
8
|
+
New: "new",
|
|
9
|
+
Negotiating: "negotiating",
|
|
10
|
+
Connecting: "connecting",
|
|
11
|
+
Connected: "connected",
|
|
12
|
+
DisConnected: "disconnected",
|
|
13
|
+
Closed: "closed",
|
|
14
|
+
Failed: "failed",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const MessageType = {
|
|
18
|
+
Connection: "Connection",
|
|
19
|
+
Heartbeat: "Heartbeat",
|
|
20
|
+
Data: "Data",
|
|
21
|
+
DataConnectionState: "Data.Connection.State",
|
|
22
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { ConnectionState, MessageType } from "p2p/message"
|
|
2
|
+
|
|
3
|
+
const ICE_CONFIG = {
|
|
4
|
+
iceServers: [
|
|
5
|
+
{
|
|
6
|
+
urls: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"]
|
|
7
|
+
},
|
|
8
|
+
],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default class P2pConnection {
|
|
12
|
+
constructor(peer, clientId, hostId, iamHost, iceConfig, heartbeatConfig) { // TODO: add heartbeatInterval to config
|
|
13
|
+
this.peer = peer
|
|
14
|
+
this.clientId = clientId
|
|
15
|
+
this.hostId = hostId
|
|
16
|
+
this.iamHost = iamHost
|
|
17
|
+
this.state = ConnectionState.New
|
|
18
|
+
this.lastTimeUpdate = 0
|
|
19
|
+
this.iceConfig = iceConfig || ICE_CONFIG
|
|
20
|
+
this.heartbeatConfig = heartbeatConfig
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setupRTCPeerConnection() {
|
|
24
|
+
// console.log("connection start ...")
|
|
25
|
+
this.rtcPeerConnection = new RTCPeerConnection(this.iceConfig)
|
|
26
|
+
|
|
27
|
+
this.rtcPeerConnection.onicecandidate = event => {
|
|
28
|
+
// console.log(event)
|
|
29
|
+
if (event.candidate) {
|
|
30
|
+
let ice = {}
|
|
31
|
+
ice[ConnectionState.IceCandidate] = event.candidate
|
|
32
|
+
this.peer.signal(ConnectionState.IceCandidate, ice)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.rtcPeerConnection.oniceconnectionstatechange = event => {
|
|
36
|
+
// console.log(event)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.rtcPeerConnection.onconnectionstatechange = (ev) => {
|
|
40
|
+
// console.log(`onconnectionstatechange ${this.rtcPeerConnection.connectionState}`)
|
|
41
|
+
this.state = this.rtcPeerConnection.connectionState
|
|
42
|
+
if (this.state == ConnectionState.DisConnected || this.state == ConnectionState.Closed) {
|
|
43
|
+
this.close()
|
|
44
|
+
}
|
|
45
|
+
this.peer.updateP2pConnectionState(this)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.sendDataChannel = this.rtcPeerConnection.createDataChannel("sendChannel")
|
|
49
|
+
this.sendDataChannelOpen = false
|
|
50
|
+
this.sendDataChannel.onopen = this.handleSendChannelStatusChange.bind(this)
|
|
51
|
+
this.sendDataChannel.onclose = this.handleSendChannelStatusChange.bind(this)
|
|
52
|
+
|
|
53
|
+
this.rtcPeerConnection.ondatachannel = event => {
|
|
54
|
+
// console.log("ondatachannel p2p ...")
|
|
55
|
+
this.receiveDataChannel = event.channel
|
|
56
|
+
this.receiveDataChannel.onmessage = this.receiveP2pMessage.bind(this)
|
|
57
|
+
this.receiveDataChannel.onopen = this.handleReceiveChannelStatusChange.bind(this)
|
|
58
|
+
this.receiveDataChannel.onclose = this.handleReceiveChannelStatusChange.bind(this)
|
|
59
|
+
|
|
60
|
+
this.peer.updateP2pConnectionState(this)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.rtcPeerConnection
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
receiveP2pMessage(event) {
|
|
67
|
+
// console.log(`p2p received msg: ${event.data}`)
|
|
68
|
+
const msg = JSON.parse(event.data)
|
|
69
|
+
switch (msg.type) {
|
|
70
|
+
case MessageType.Heartbeat:
|
|
71
|
+
this.state = ConnectionState.Connected
|
|
72
|
+
this.lastTimeUpdate = Date.now()
|
|
73
|
+
break
|
|
74
|
+
default:
|
|
75
|
+
this.peer.receivedP2pMessage(msg)
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
sendP2pMessage(message, type = MessageType.Data, senderId = null) {
|
|
81
|
+
if (this.sendDataChannel && this.sendDataChannelOpen) {
|
|
82
|
+
const msgJson = JSON.stringify({
|
|
83
|
+
type: type,
|
|
84
|
+
senderId: senderId || this.peer.peerId,
|
|
85
|
+
data: message
|
|
86
|
+
})
|
|
87
|
+
this.sendDataChannel.send(msgJson)
|
|
88
|
+
} else {
|
|
89
|
+
// console.warn("the send data channel is not available!")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
handleSendChannelStatusChange(event) {
|
|
94
|
+
// console.log(event)
|
|
95
|
+
if (this.sendDataChannel) {
|
|
96
|
+
this.sendDataChannelOpen = this.sendDataChannel.readyState == "open"
|
|
97
|
+
if (this.sendDataChannelOpen && this.heartbeatConfig) {
|
|
98
|
+
this.scheduleHeartbeat()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
handleReceiveChannelStatusChange(event) {
|
|
104
|
+
// console.log(event)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
scheduleHeartbeat() {
|
|
108
|
+
this.heartbeat = setTimeout(() => {
|
|
109
|
+
this.sendHeartbeat()
|
|
110
|
+
}, this.heartbeatConfig.interval_mls)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sendHeartbeat() {
|
|
114
|
+
if (this.lastTimeUpdate > 0 && Date.now() - this.lastTimeUpdate > this.heartbeatConfig.idle_timeout_mls) {
|
|
115
|
+
// console.log("HEART-BEAT DETECT DISCONNECTED ............")
|
|
116
|
+
this.state = ConnectionState.DisConnected
|
|
117
|
+
this.peer.updateP2pConnectionState(this)
|
|
118
|
+
} else {
|
|
119
|
+
this.sendP2pMessage("ping", MessageType.Heartbeat)
|
|
120
|
+
this.scheduleHeartbeat()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stopHeartbeat() {
|
|
125
|
+
// console.log(`stop heartbeat ${this.hostId} <-> ${this.clientId}`)
|
|
126
|
+
clearTimeout(this.heartbeat)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
close() {
|
|
130
|
+
// console.log(`close the connection ${this.hostId} <-> ${this.clientId}`)
|
|
131
|
+
this.stopHeartbeat()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
connect() {
|
|
5
|
+
this.p2pSetup()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
p2pSetup() {
|
|
9
|
+
this.p2pFrame = this.element.closest("p2p-frame")
|
|
10
|
+
if (this.p2pFrame) {
|
|
11
|
+
this.p2pFrame.setP2pListener(this)
|
|
12
|
+
} else {
|
|
13
|
+
throw new Error("Couldn't find p2p-frame!")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get peerId() {
|
|
18
|
+
this.p2pFrame.peer?.peerId
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get hostPeerId() {
|
|
22
|
+
this.p2pFrame.peer?.hostPeerId
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get iamHost() {
|
|
26
|
+
this.p2pFrame.peer?.iamHost
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// p2p callbacks
|
|
30
|
+
|
|
31
|
+
p2pNegotiating() {}
|
|
32
|
+
|
|
33
|
+
p2pConnecting() {}
|
|
34
|
+
|
|
35
|
+
p2pConnected() {}
|
|
36
|
+
|
|
37
|
+
p2pDisconnected() {}
|
|
38
|
+
|
|
39
|
+
p2pClosed() {}
|
|
40
|
+
|
|
41
|
+
p2pError() {}
|
|
42
|
+
|
|
43
|
+
// send/received p2p message
|
|
44
|
+
|
|
45
|
+
p2pSendMessage(message) {
|
|
46
|
+
if (this.p2pFrame) {
|
|
47
|
+
this.p2pFrame.sendP2pMessage(message)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
p2pReceivedMessage(message) {}
|
|
52
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Turbo, cable } from "@hotwired/turbo-rails"
|
|
2
|
+
import P2pPeer from "p2p/p2p_peer"
|
|
3
|
+
import { MessageType } from "p2p/message"
|
|
4
|
+
|
|
5
|
+
class P2pFrameElement extends HTMLElement {
|
|
6
|
+
constructor() {
|
|
7
|
+
super()
|
|
8
|
+
this.listeners ||= []
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// called each time the element is added to the document.
|
|
12
|
+
async connectedCallback() {
|
|
13
|
+
Turbo.connectStreamSource(this)
|
|
14
|
+
this.subscription = await cable.subscribeTo(this.channel, {
|
|
15
|
+
received: this.receiveSignal.bind(this),
|
|
16
|
+
connected: this.subscriptionConnected.bind(this),
|
|
17
|
+
disconnected: this.subscriptionDisconnected.bind(this)
|
|
18
|
+
}).catch(err => console.log(err))
|
|
19
|
+
|
|
20
|
+
this.peer = new P2pPeer(this.sessionId, this.peerId, this, this.subscription, this.iceConfig, this.heartbeatConfig)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// called each time the element is removed from the document.
|
|
24
|
+
disconnectedCallback() {
|
|
25
|
+
// console.log("p2p-frame disconnected")
|
|
26
|
+
this.unsubscribeSignalChannel()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
subscriptionConnected() {
|
|
30
|
+
// console.log("subscriptionConnected")
|
|
31
|
+
this.peer.setup()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
subscriptionDisconnected() {
|
|
35
|
+
// console.log("subscriptionDisconnected")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
receiveSignal(message) {
|
|
39
|
+
// console.log("receive signal")
|
|
40
|
+
// console.log(message)
|
|
41
|
+
if (message.type == MessageType.Connection) {
|
|
42
|
+
this.peer.negotiate(message)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setP2pListener(listener) {
|
|
47
|
+
this.listeners ||= []
|
|
48
|
+
this.listeners.push(listener)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
dispatchP2pMessage(message) {
|
|
52
|
+
this.listeners.forEach(listener => {
|
|
53
|
+
listener.p2pReceivedMessage(message)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sendP2pMessage(msg) {
|
|
58
|
+
this.peer.sendP2pMessage(msg)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
p2pNegotiating() {
|
|
62
|
+
this.listeners.forEach(listener => {
|
|
63
|
+
listener.p2pNegotiating()
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
p2pConnecting() {
|
|
68
|
+
this.listeners.forEach(listener => {
|
|
69
|
+
listener.p2pConnecting()
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
p2pConnected() {
|
|
74
|
+
this.listeners.forEach(listener => {
|
|
75
|
+
listener.p2pConnected()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// only host-peer retain connect to the signal server
|
|
79
|
+
if (!this.peer?.iamHost && !this.keepCableConnection) {
|
|
80
|
+
// console.log("im not host so unsubscribe")
|
|
81
|
+
this.unsubscribeSignalChannel()
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
p2pDisconnected() {
|
|
86
|
+
this.listeners.forEach(listener => {
|
|
87
|
+
listener.p2pDisconnected()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
p2pClosed() {
|
|
92
|
+
this.listeners.forEach(listener => {
|
|
93
|
+
listener.p2pClosed()
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
p2pError() {
|
|
98
|
+
this.listeners.forEach(listener => {
|
|
99
|
+
listener.p2pError()
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async unsubscribeSignalChannel() { // TODO: MAKE SURE `SignalingChannel stopped streaming`
|
|
104
|
+
if (this.subscription) this.subscription.unsubscribe()
|
|
105
|
+
Turbo.disconnectStreamSource(this)
|
|
106
|
+
let consumer = await cable.getConsumer()
|
|
107
|
+
if (consumer) consumer.disconnect()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get sessionId() {
|
|
111
|
+
return this.getAttribute("session-id")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get peerId() {
|
|
115
|
+
return this.getAttribute("peer-id")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get params() {
|
|
119
|
+
return JSON.parse(this.getAttribute("params"))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get config() {
|
|
123
|
+
return this.params["config"] || {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get iceConfig() {
|
|
127
|
+
return {
|
|
128
|
+
iceServers: this.config["ice_servers"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get heartbeatConfig() {
|
|
133
|
+
return this.config["heartbeat"]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get keepCableConnection() {
|
|
137
|
+
return this.config["keep_cable_connection"]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get channel() {
|
|
141
|
+
const channel = this.getAttribute("channel")
|
|
142
|
+
const signed_stream_name = this.getAttribute("signed-stream-name")
|
|
143
|
+
return {
|
|
144
|
+
channel: channel,
|
|
145
|
+
signed_stream_name: signed_stream_name,
|
|
146
|
+
session_id: this.sessionId,
|
|
147
|
+
peer_id: this.peerId,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
customElements.define("p2p-frame", P2pFrameElement)
|