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
@@ -0,0 +1,208 @@
|
|
1
|
+
import { ConnectionState, MessageType } from "p2p/message"
|
2
|
+
import P2pConnection from "p2p/p2p_connection"
|
3
|
+
|
4
|
+
export default class P2pPeer {
|
5
|
+
constructor(sessionId, peerId, container, signaling, iceConfig, heartbeatConfig) {
|
6
|
+
this.sessionId = sessionId
|
7
|
+
this.container = container
|
8
|
+
this.signaling = signaling
|
9
|
+
this.iceConfig = iceConfig
|
10
|
+
this.heartbeatConfig = heartbeatConfig
|
11
|
+
this.peerId = peerId
|
12
|
+
this.hostPeerId = null
|
13
|
+
this.iamHost = false
|
14
|
+
this.state = null
|
15
|
+
}
|
16
|
+
|
17
|
+
setup() {
|
18
|
+
this.connections = new Map()
|
19
|
+
this.signal(ConnectionState.SessionJoin, {})
|
20
|
+
this.dispatchP2pConnectionState({state: ConnectionState.Negotiating})
|
21
|
+
}
|
22
|
+
|
23
|
+
signal(state, data) {
|
24
|
+
let msg = {
|
25
|
+
"type": MessageType.Connection,
|
26
|
+
"session_id": this.sessionId,
|
27
|
+
"peer_id": this.peerId,
|
28
|
+
"state": state,
|
29
|
+
...data
|
30
|
+
}
|
31
|
+
this.signaling.send(msg)
|
32
|
+
}
|
33
|
+
|
34
|
+
negotiate(msg) {
|
35
|
+
switch (msg.state) {
|
36
|
+
case ConnectionState.SessionJoin:
|
37
|
+
break
|
38
|
+
case ConnectionState.SessionReady:
|
39
|
+
if (msg.host_peer_id == this.peerId) { // iam host
|
40
|
+
this.iamHost = true
|
41
|
+
this.hostPeerId = this.peerId
|
42
|
+
if (msg.peer_id == this.peerId) {
|
43
|
+
this.updateP2pConnectionState()
|
44
|
+
return
|
45
|
+
}
|
46
|
+
|
47
|
+
const connection = new P2pConnection(this, msg.peer_id, this.peerId, this.iamHost, this.iceConfig, this.heartbeatConfig)
|
48
|
+
this.connections.set(msg.peer_id, connection)
|
49
|
+
|
50
|
+
const rtcPeerConnection = connection.setupRTCPeerConnection()
|
51
|
+
if (!rtcPeerConnection) {
|
52
|
+
// TODO: failed case
|
53
|
+
return
|
54
|
+
}
|
55
|
+
rtcPeerConnection.createOffer()
|
56
|
+
.then(offer => {
|
57
|
+
return rtcPeerConnection.setLocalDescription(offer)
|
58
|
+
})
|
59
|
+
.then(() => {
|
60
|
+
let offer = {"host_peer_id": msg.host_peer_id}
|
61
|
+
offer[ConnectionState.SdpOffer] = JSON.stringify(rtcPeerConnection.localDescription)
|
62
|
+
this.signal(ConnectionState.SdpOffer, offer)
|
63
|
+
})
|
64
|
+
.catch(err => console.log(err))
|
65
|
+
}
|
66
|
+
|
67
|
+
this.state = ConnectionState.SessionReady
|
68
|
+
|
69
|
+
break
|
70
|
+
case ConnectionState.SdpOffer:
|
71
|
+
if (msg.host_peer_id != this.peerId && this.state != ConnectionState.SdpOffer) { // iam not host
|
72
|
+
this.hostPeerId = msg.host_peer_id
|
73
|
+
const connection = new P2pConnection(this, this.peerId, msg.host_peer_id, this.iamHost, this.iceConfig, this.heartbeatConfig)
|
74
|
+
this.connections.set(this.peerId, connection)
|
75
|
+
|
76
|
+
const rtcPeerConnection = connection.setupRTCPeerConnection()
|
77
|
+
|
78
|
+
let offer = JSON.parse(msg[ConnectionState.SdpOffer])
|
79
|
+
// console.log(`${this.peerId} get Offer`)
|
80
|
+
// console.log(offer)
|
81
|
+
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(offer))
|
82
|
+
.then(() => {
|
83
|
+
rtcPeerConnection.createAnswer()
|
84
|
+
.then(answer => {
|
85
|
+
return rtcPeerConnection.setLocalDescription(answer)
|
86
|
+
})
|
87
|
+
.then(() => {
|
88
|
+
let answer = {"host_peer_id": msg.host_peer_id}
|
89
|
+
answer[ConnectionState.SdpAnswer] = JSON.stringify(rtcPeerConnection.localDescription)
|
90
|
+
this.signal(ConnectionState.SdpAnswer, answer)
|
91
|
+
})
|
92
|
+
.catch(err => console.log(err))
|
93
|
+
})
|
94
|
+
}
|
95
|
+
break
|
96
|
+
case ConnectionState.SdpAnswer:
|
97
|
+
if (msg.host_peer_id == this.peerId) { // iam host
|
98
|
+
// console.log(` ${this.peerId} get Answer`)
|
99
|
+
const clientConnection = this.connections.get(msg.peer_id)
|
100
|
+
if (!clientConnection) return;
|
101
|
+
|
102
|
+
const rtcPeerConnection = clientConnection.rtcPeerConnection
|
103
|
+
let answer = JSON.parse(msg[ConnectionState.SdpAnswer])
|
104
|
+
|
105
|
+
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(answer))
|
106
|
+
.catch(err => console.log(err))
|
107
|
+
}
|
108
|
+
break
|
109
|
+
case ConnectionState.IceCandidate:
|
110
|
+
if (msg[ConnectionState.IceCandidate]) {
|
111
|
+
this.connections.forEach((connection, peerId) => {
|
112
|
+
connection.rtcPeerConnection.addIceCandidate(new RTCIceCandidate(msg[ConnectionState.IceCandidate]))
|
113
|
+
.catch(err => console.log(err))
|
114
|
+
})
|
115
|
+
}
|
116
|
+
break
|
117
|
+
case ConnectionState.Error:
|
118
|
+
// console.log("Connection Error")
|
119
|
+
break
|
120
|
+
default:
|
121
|
+
break
|
122
|
+
}
|
123
|
+
}
|
124
|
+
|
125
|
+
dispatchP2pMessage(message, type, senderId) {
|
126
|
+
this.connections.forEach((connection, peerId) => {
|
127
|
+
connection.sendP2pMessage(message, type, senderId)
|
128
|
+
})
|
129
|
+
}
|
130
|
+
|
131
|
+
sendP2pMessage(message) {
|
132
|
+
if (this.iamHost) {
|
133
|
+
this.container.dispatchP2pMessage({
|
134
|
+
type: MessageType.Data,
|
135
|
+
senderId: this.peerId,
|
136
|
+
data: message
|
137
|
+
})
|
138
|
+
}
|
139
|
+
|
140
|
+
this.connections.forEach((connection, peerId) => {
|
141
|
+
connection.sendP2pMessage(message, MessageType.Data, this.peerId)
|
142
|
+
})
|
143
|
+
}
|
144
|
+
|
145
|
+
receivedP2pMessage(message) {
|
146
|
+
switch (message.type) {
|
147
|
+
case MessageType.Data:
|
148
|
+
case MessageType.DataConnectionState:
|
149
|
+
if (this.iamHost) {
|
150
|
+
//broadcast to all connections
|
151
|
+
this.dispatchP2pMessage(message.data, message.type, message.senderId)
|
152
|
+
}
|
153
|
+
|
154
|
+
// dispatch msg to all sub views
|
155
|
+
this.container.dispatchP2pMessage(message)
|
156
|
+
break
|
157
|
+
default:
|
158
|
+
break
|
159
|
+
}
|
160
|
+
}
|
161
|
+
|
162
|
+
updateP2pConnectionState(connection = null) {
|
163
|
+
if (this.iamHost) {
|
164
|
+
this.connectionStatus ||= {}
|
165
|
+
this.connections.forEach((connection, peerId) => {
|
166
|
+
this.connectionStatus[peerId] = connection.state
|
167
|
+
})
|
168
|
+
this.connectionStatus[this.hostPeerId] = ConnectionState.Connected
|
169
|
+
|
170
|
+
this.container.dispatchP2pMessage({
|
171
|
+
type: MessageType.DataConnectionState,
|
172
|
+
senderId: this.peerId,
|
173
|
+
data: this.connectionStatus
|
174
|
+
})
|
175
|
+
|
176
|
+
this.dispatchP2pMessage(this.connectionStatus, MessageType.DataConnectionState, this.hostPeerId)
|
177
|
+
}
|
178
|
+
|
179
|
+
if (connection) {
|
180
|
+
this.dispatchP2pConnectionState(connection)
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
dispatchP2pConnectionState(connection) {
|
185
|
+
switch (connection.state) {
|
186
|
+
case ConnectionState.Negotiating:
|
187
|
+
this.container.p2pNegotiating()
|
188
|
+
break
|
189
|
+
case ConnectionState.Connecting:
|
190
|
+
this.container.p2pConnecting()
|
191
|
+
break
|
192
|
+
case ConnectionState.Connected:
|
193
|
+
this.container.p2pConnected()
|
194
|
+
break
|
195
|
+
case ConnectionState.DisConnected:
|
196
|
+
this.container.p2pDisconnected()
|
197
|
+
break
|
198
|
+
case ConnectionState.Closed:
|
199
|
+
this.container.p2pClosed()
|
200
|
+
break
|
201
|
+
case ConnectionState.Failed:
|
202
|
+
this.container.p2pError()
|
203
|
+
break
|
204
|
+
default:
|
205
|
+
break
|
206
|
+
}
|
207
|
+
}
|
208
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
"name": "p2p",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"files": ["./*.*"],
|
5
|
+
"main": "./index.js",
|
6
|
+
"dependencies": {
|
7
|
+
"@hotwired/stimulus": "^3.2.2",
|
8
|
+
"@hotwired/turbo-rails": "^8.0.4",
|
9
|
+
"esbuild": "^0.20.2"
|
10
|
+
},
|
11
|
+
"scripts": {
|
12
|
+
"build": "esbuild app/assets/javascripts/*.* --bundle --minify --format=esm --outdir=app/assets/builds --public-path=/assets"
|
13
|
+
}
|
14
|
+
}
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SignalingChannel < Turbo::StreamsChannel
|
4
|
+
def subscribed
|
5
|
+
super
|
6
|
+
|
7
|
+
# TODO: user_id, session_id, is_host
|
8
|
+
@session_id = params["session_id"]
|
9
|
+
@peer_id = params["peer_id"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def receive(data)
|
13
|
+
puts "Signaling Server peer #{@peer_id} receive #{data} #{params}"
|
14
|
+
send_back_msg = P2pStreamsChannel.resolve(data)
|
15
|
+
if send_back_msg.present?
|
16
|
+
SignalingChannel.sync send_back_msg, to: P2pStreamsChannel.fetch_session(data["session_id"])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def unsubscribed
|
21
|
+
super
|
22
|
+
|
23
|
+
P2pStreamsChannel.disconnect_if_host_peer(@session_id, @peer_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.sync(data, to:)
|
27
|
+
ActionCable.server.broadcast stream_name_from(to), data
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module P2pStreamsChannel::TagHelper
|
4
|
+
def p2p_frame_tag(session_id:, peer_id:, **params, &block)
|
5
|
+
session = P2pStreamsChannel.fetch_session(session_id, **params)
|
6
|
+
signed_stream_name = Turbo::StreamsChannel.signed_stream_name(session)
|
7
|
+
content = capture(&block)
|
8
|
+
|
9
|
+
%(
|
10
|
+
<p2p-frame channel="SignalingChannel" signed-stream-name=#{signed_stream_name}
|
11
|
+
session-id=#{session.id} peer-id=#{peer_id.to_json} params=#{params.to_json}>
|
12
|
+
#{content}
|
13
|
+
</p2p-frame>
|
14
|
+
).html_safe
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./session_state"
|
4
|
+
|
5
|
+
module P2pStreamsChannel
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# TODO:
|
9
|
+
# params[start_time]
|
10
|
+
# params[end_time]
|
11
|
+
# params[max_number_of_peers]
|
12
|
+
#
|
13
|
+
def fetch_session(session_id, **params)
|
14
|
+
P2pStreamsChannel.store.fetch(session_id, expires_in: params[:expires_in]) do
|
15
|
+
P2pStreamsChannel::Session.new(session_id, secret_key: params[:secret_key])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def save_session(session)
|
20
|
+
P2pStreamsChannel.store.write(session.id, session)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module P2pStreamsChannel
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
config.autoload_once_paths = %W( #{root}/app/channels )
|
6
|
+
|
7
|
+
initializer "p2p_streams_channel.assets" do
|
8
|
+
if Rails.application.config.respond_to?(:assets)
|
9
|
+
Rails.application.config.assets.precompile += Dir["#{root}/app/assets/javascripts/*/*"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
config.autoload_once_paths = %W( #{root}/app/helpers )
|
14
|
+
initializer "p2p_streams_channel.helpers" do
|
15
|
+
ActiveSupport.on_load(:action_controller_base) do
|
16
|
+
helper P2pStreamsChannel::Engine.helpers
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module P2pStreamsChannel
|
4
|
+
class Negotiation
|
5
|
+
def join(peer_id, session_state)
|
6
|
+
session_state.add_peer(peer_id)
|
7
|
+
session_state.host_ready? ? ready_response(peer_id, session_state) : nil
|
8
|
+
rescue => e
|
9
|
+
error_response(e, session_state)
|
10
|
+
end
|
11
|
+
|
12
|
+
def ready_response(peer_id, session_state)
|
13
|
+
{
|
14
|
+
"type": P2pStreamsChannel::CONNECTION_MESSAGE_TYPE,
|
15
|
+
"state": P2pStreamsChannel::STATE_SESSION_READY,
|
16
|
+
"host_peer_id": session_state.host_peer_id,
|
17
|
+
"peer_id": peer_id
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def error_response(err, session_state)
|
22
|
+
# TODO:
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./negotiation"
|
4
|
+
|
5
|
+
module P2pStreamsChannel
|
6
|
+
# TODO: support optional :db -> save session to db
|
7
|
+
class Session
|
8
|
+
attr_reader :id, :secret_key, :negotiation, :session_state
|
9
|
+
|
10
|
+
def initialize(id, secret_key: "")
|
11
|
+
@id = id
|
12
|
+
@secret_key = secret_key
|
13
|
+
@negotiation = Negotiation.new
|
14
|
+
@session_state = SessionState.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def signature
|
18
|
+
{id: @id}
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_param
|
22
|
+
signature.to_param
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_json
|
26
|
+
signature.to_json
|
27
|
+
end
|
28
|
+
|
29
|
+
def join_peer(peer_id)
|
30
|
+
@negotiation.join(peer_id, session_state).tap do |response|
|
31
|
+
save!
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def disconnect_peer(peer_id)
|
36
|
+
if session_state.host_peer_id == peer_id
|
37
|
+
reset_state
|
38
|
+
else
|
39
|
+
session_state.remove_peer(peer_id)
|
40
|
+
save!
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def is_host_peer?(peer_id)
|
45
|
+
session_state.host_peer_id == peer_id
|
46
|
+
end
|
47
|
+
|
48
|
+
def reset_state
|
49
|
+
@session_state = SessionState.new
|
50
|
+
save!
|
51
|
+
end
|
52
|
+
|
53
|
+
def save!
|
54
|
+
P2pStreamsChannel.save_session(self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module P2pStreamsChannel
|
2
|
+
module_function
|
3
|
+
|
4
|
+
CONNECTION_MESSAGE_TYPE = "Connection".freeze
|
5
|
+
|
6
|
+
def resolve(message)
|
7
|
+
case message["type"]
|
8
|
+
when CONNECTION_MESSAGE_TYPE
|
9
|
+
handle_session(message)
|
10
|
+
else
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def handle_session(data)
|
15
|
+
case data["state"]
|
16
|
+
when P2pStreamsChannel::STATE_SESSION_JOIN
|
17
|
+
if session = P2pStreamsChannel.fetch_session(data["session_id"])
|
18
|
+
session.join_peer(data["peer_id"])
|
19
|
+
end
|
20
|
+
else
|
21
|
+
data
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def disconnect_if_host_peer(session_id, peer_id)
|
26
|
+
if session = P2pStreamsChannel.fetch_session(session_id)
|
27
|
+
if session.is_host_peer?(peer_id)
|
28
|
+
session.disconnect_peer(peer_id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module P2pStreamsChannel
|
4
|
+
STATE_SESSION_JOIN = "SessionJoin".freeze
|
5
|
+
STATE_SESSION_READY = "SessionReady".freeze
|
6
|
+
STATE_CONNECTED = "connected".freeze
|
7
|
+
|
8
|
+
class SessionState
|
9
|
+
attr_reader :peers, :host_peer_id
|
10
|
+
|
11
|
+
def initialize(host_peer_id = nil)
|
12
|
+
@peers = {}
|
13
|
+
@host_peer_id = host_peer_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_peer(peer_id)
|
17
|
+
@peers[peer_id] = true
|
18
|
+
@host_peer_id = peer_id if @host_peer_id.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def remove_peer(peer_id)
|
22
|
+
@peers[peer_id] = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def num_of_connected_peers
|
26
|
+
@peers.count { |peer, connected| connected == true }
|
27
|
+
end
|
28
|
+
|
29
|
+
def host_ready?
|
30
|
+
@peers[@host_peer_id] == true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "p2p_streams_channel/version"
|
4
|
+
require_relative "p2p_streams_channel/engine"
|
5
|
+
require_relative "p2p_streams_channel/cache"
|
6
|
+
require_relative "p2p_streams_channel/session"
|
7
|
+
require_relative "p2p_streams_channel/session_handler"
|
8
|
+
|
9
|
+
require_relative "rails/generators/p2p_streams_channel/install_generator"
|
10
|
+
|
11
|
+
module P2pStreamsChannel
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
class Configuration
|
15
|
+
attr_accessor :store
|
16
|
+
end
|
17
|
+
|
18
|
+
mattr_reader :configs, default: Configuration.new
|
19
|
+
|
20
|
+
def config
|
21
|
+
yield(configs)
|
22
|
+
end
|
23
|
+
module_function :config
|
24
|
+
|
25
|
+
def store
|
26
|
+
configs.store || Rails.cache
|
27
|
+
end
|
28
|
+
module_function :store
|
29
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
|
4
|
+
module P2pStreamsChannel
|
5
|
+
class ControllerGenerator < ::Rails::Generators::NamedBase
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def p2p_controller_js
|
9
|
+
copy_file "controller.js", "app/javascript/controllers/#{name}_controller.js"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "rails/generators"
|
3
|
+
|
4
|
+
module P2pStreamsChannel
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def importmap
|
9
|
+
return unless (importmap_path = Rails.root.join("config/importmap.rb")).exist?
|
10
|
+
|
11
|
+
append_to_file importmap_path, %(\npin_all_from "#{File.expand_path("../../../../app/assets/javascripts/p2p/", __dir__)}", under: "p2p"\n)
|
12
|
+
end
|
13
|
+
|
14
|
+
def node
|
15
|
+
return unless Rails.root.join("package.json").exist?
|
16
|
+
|
17
|
+
run "yarn add p2p@file:#{File.expand_path("../../../../app/assets/javascripts/p2p/", __dir__)}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_initializer
|
21
|
+
copy_file "initializer.rb", "config/initializers/p2p_streams_channel.rb"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import { P2pController } from "p2p"
|
2
|
+
|
3
|
+
export default class extends P2pController {
|
4
|
+
//
|
5
|
+
// p2p callbacks
|
6
|
+
//
|
7
|
+
p2pNegotiating() {
|
8
|
+
// your peer start to negotiate with the host through ActionCable
|
9
|
+
}
|
10
|
+
|
11
|
+
p2pConnecting() {
|
12
|
+
// your peer is connecting directly with the host peer
|
13
|
+
}
|
14
|
+
|
15
|
+
p2pConnected() {
|
16
|
+
// your peer's connected to the host peer
|
17
|
+
// from now you could start send message to the other through the host peer
|
18
|
+
}
|
19
|
+
|
20
|
+
p2pDisconnected() {
|
21
|
+
// your peer's disconnected from the host peer
|
22
|
+
}
|
23
|
+
|
24
|
+
p2pClosed() {}
|
25
|
+
|
26
|
+
p2pError() {}
|
27
|
+
|
28
|
+
// receiving message from the other through the host peer
|
29
|
+
p2pReceivedMessage(message) {
|
30
|
+
switch(message["type"]) {
|
31
|
+
case "Data":
|
32
|
+
// message["data"]: the raw text(or whatever)
|
33
|
+
break
|
34
|
+
case "Data.Connection.State":
|
35
|
+
// message["data"]: the current connection state of other peers
|
36
|
+
// for (let [peer, state] of Object.entries(message["data"])) {}
|
37
|
+
break
|
38
|
+
default:
|
39
|
+
break
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
//
|
44
|
+
// send message to the others through the host peer:
|
45
|
+
// this.p2pSendMessage('hi')
|
46
|
+
//
|
47
|
+
// this.iamHost: your peer is the host or not
|
48
|
+
// this.peerId: your peer id
|
49
|
+
// this.hostPeerId: the host peer id
|
50
|
+
//
|
51
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "lib/p2p_streams_channel/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "p2p_streams_channel"
|
6
|
+
spec.version = P2pStreamsChannel::VERSION
|
7
|
+
spec.authors = ["theforestvn88"]
|
8
|
+
spec.email = ["theforestvn88@gmail.com"]
|
9
|
+
|
10
|
+
spec.summary = "rails p2p turbo streams channel"
|
11
|
+
spec.description = "rails p2p turbo streams channel"
|
12
|
+
spec.homepage = "https://github.com/theforestvn88/p2p_streams_channel"
|
13
|
+
spec.required_ruby_version = ">= 2.6.0"
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/theforestvn88/p2p_streams_channel"
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/theforestvn88/p2p_streams_channel"
|
18
|
+
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
20
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
23
|
+
(File.expand_path(f) == __FILE__) ||
|
24
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_dependency "rails"
|
32
|
+
spec.add_dependency 'turbo-rails'
|
33
|
+
spec.add_dependency 'stimulus-rails'
|
34
|
+
|
35
|
+
spec.add_development_dependency 'rake'
|
36
|
+
spec.add_development_dependency 'rspec-rails'
|
37
|
+
spec.test_files = Dir["spec/**/*"]
|
38
|
+
end
|
data/spec/dummy/Rakefile
ADDED