p2p_streams_channel 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|