p2p_streams_channel 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -14
- data/lib/p2p_streams_channel/engine.rb +0 -6
- data/lib/p2p_streams_channel/version.rb +1 -1
- data/lib/rails/generators/p2p_streams_channel/install_generator.rb +8 -2
- data/p2p_streams_channel.gemspec +2 -2
- data/spec/dummy/config/importmap.rb +2 -0
- data/spec/dummy/log/development.log +0 -0
- data/spec/dummy/vendor/javascript/p2p/index.js +6 -0
- data/spec/dummy/vendor/javascript/p2p/message.js +22 -0
- data/spec/dummy/vendor/javascript/p2p/p2p_connection.js +133 -0
- data/spec/dummy/vendor/javascript/p2p/p2p_controller.js +52 -0
- data/spec/dummy/vendor/javascript/p2p/p2p_frame.js +152 -0
- data/spec/dummy/vendor/javascript/p2p/p2p_peer.js +208 -0
- data/spec/dummy/vendor/javascript/p2p/package.json +14 -0
- metadata +29 -11
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/index.js +0 -0
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/message.js +0 -0
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_connection.js +0 -0
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_controller.js +0 -0
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_frame.js +0 -0
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_peer.js +0 -0
- /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/package.json +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 133464f081bff5e3415e6129f9ca134a3c09658647aa159ef0327b48670e9efc
|
4
|
+
data.tar.gz: 5b4cd920727bf06e0a64d6a999bd4e4e49ef7b4db19019972adba44c50800792
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d0a29b91e5f9f76c1c3da318242bf79e9b36f36e6ff058d196858382be308684719782a8d69c008af0408c1048e08b9c9d5ec8e72d64a090b254faae8d0486cd
|
7
|
+
data.tar.gz: 72b6b4c8d91d4a6e0a01411018e7363a4c6ae1a0347dca41918d0320ba18666f86bd2982fa01037a963af0e24256f56fed5f491b6de4a6fe8b2bb3fa25b604bf
|
data/README.md
CHANGED
@@ -48,7 +48,8 @@ iceServers --ice-candidate----> client-user --> host-user
|
|
48
48
|
Connected
|
49
49
|
=========
|
50
50
|
|
51
|
-
After client-user connected to the host-user,
|
51
|
+
After a client-user connected to the host-user, it'll be disconnected to the signaling server (in order to save memory).
|
52
|
+
Only the host-user keep connect to Rails server.
|
52
53
|
In case you want keep client connection, you could set params `keepCableConnection: true` to the p2p-frame.
|
53
54
|
|
54
55
|
client-user1 ----X disconnect from -----> Rails server Action cable
|
@@ -89,12 +90,6 @@ $ rails g p2p_streams_channel:install
|
|
89
90
|
|
90
91
|
## Usage
|
91
92
|
|
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
93
|
Render a p2p-frame-tag
|
99
94
|
```ruby
|
100
95
|
# views/chat_rooms/_chat_room.html.erb
|
@@ -118,10 +113,11 @@ Render a p2p-frame-tag
|
|
118
113
|
<% end %>
|
119
114
|
```
|
120
115
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
116
|
+
Create a Stimulus P2pController in which you will receive other p2p-connections status, data and send back your data to others.
|
117
|
+
```ruby
|
118
|
+
$ rails g p2p_streams_channel:controller chat
|
119
|
+
# it will create js file `app/javascript/controllers/chat_controller.js`
|
120
|
+
```
|
125
121
|
|
126
122
|
```js
|
127
123
|
// app/javascript/controllers/chat_controller.js
|
@@ -199,9 +195,6 @@ export default class extends P2pController {
|
|
199
195
|
|
200
196
|
run test:
|
201
197
|
```ruby
|
202
|
-
$ cd spec/dummy
|
203
|
-
$ rails g p2p_streams_channel:install
|
204
|
-
$ cd ../..
|
205
198
|
$ rake spec
|
206
199
|
```
|
207
200
|
|
@@ -3,12 +3,6 @@
|
|
3
3
|
module P2pStreamsChannel
|
4
4
|
class Engine < ::Rails::Engine
|
5
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
6
|
|
13
7
|
config.autoload_once_paths = %W( #{root}/app/helpers )
|
14
8
|
initializer "p2p_streams_channel.helpers" do
|
@@ -5,16 +5,22 @@ module P2pStreamsChannel
|
|
5
5
|
class InstallGenerator < ::Rails::Generators::Base
|
6
6
|
source_root File.expand_path("../templates", __FILE__)
|
7
7
|
|
8
|
+
def copy_p2p
|
9
|
+
empty_directory "vendor/javascript/p2p"
|
10
|
+
directory "p2p", "vendor/javascript/p2p"
|
11
|
+
end
|
12
|
+
|
8
13
|
def importmap
|
9
14
|
return unless (importmap_path = Rails.root.join("config/importmap.rb")).exist?
|
10
15
|
|
11
|
-
append_to_file importmap_path, %(\npin_all_from "
|
16
|
+
append_to_file importmap_path, %(\npin_all_from "vendor/javascript/p2p", under: "p2p"\n)
|
17
|
+
append_to_file Rails.root.join("app/assets/config/manifest.js"), %(\n//= link_tree ../../../vendor/javascript .js\n)
|
12
18
|
end
|
13
19
|
|
14
20
|
def node
|
15
21
|
return unless Rails.root.join("package.json").exist?
|
16
22
|
|
17
|
-
run "yarn add p2p@file
|
23
|
+
run "yarn add p2p@file:vendor/javascript/p2p"
|
18
24
|
end
|
19
25
|
|
20
26
|
def create_initializer
|
data/p2p_streams_channel.gemspec
CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.authors = ["theforestvn88"]
|
8
8
|
spec.email = ["theforestvn88@gmail.com"]
|
9
9
|
|
10
|
-
spec.summary = "
|
11
|
-
spec.description = "
|
10
|
+
spec.summary = "Allow to setup one-to-many P2P stream connections (WebRTC) between clients through Rails server (ActionCable) as the signaling server"
|
11
|
+
spec.description = "Allow to setup one-to-many P2P stream connections (WebRTC) between clients through Rails server (ActionCable) as the signaling server"
|
12
12
|
spec.homepage = "https://github.com/theforestvn88/p2p_streams_channel"
|
13
13
|
spec.required_ruby_version = ">= 2.6.0"
|
14
14
|
|
@@ -5,3 +5,5 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
|
5
5
|
pin "@hotwired/stimulus", to: "stimulus.min.js"
|
6
6
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
|
7
7
|
pin_all_from "app/javascript/controllers", under: "controllers"
|
8
|
+
|
9
|
+
pin_all_from "vendor/javascript/p2p", under: "p2p"
|
File without changes
|
@@ -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)
|
@@ -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
|
+
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: p2p_streams_channel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- theforestvn88
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-04-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -80,7 +80,8 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
-
description:
|
83
|
+
description: Allow to setup one-to-many P2P stream connections (WebRTC) between clients
|
84
|
+
through Rails server (ActionCable) as the signaling server
|
84
85
|
email:
|
85
86
|
- theforestvn88@gmail.com
|
86
87
|
executables: []
|
@@ -90,13 +91,6 @@ files:
|
|
90
91
|
- ".rspec"
|
91
92
|
- README.md
|
92
93
|
- Rakefile
|
93
|
-
- app/assets/javascripts/p2p/index.js
|
94
|
-
- app/assets/javascripts/p2p/message.js
|
95
|
-
- app/assets/javascripts/p2p/p2p_connection.js
|
96
|
-
- app/assets/javascripts/p2p/p2p_controller.js
|
97
|
-
- app/assets/javascripts/p2p/p2p_frame.js
|
98
|
-
- app/assets/javascripts/p2p/p2p_peer.js
|
99
|
-
- app/assets/javascripts/p2p/package.json
|
100
94
|
- app/channels/signaling_channel.rb
|
101
95
|
- app/helpers/p2p_streams_channel/tag_helper.rb
|
102
96
|
- lib/p2p_streams_channel.rb
|
@@ -111,6 +105,13 @@ files:
|
|
111
105
|
- lib/rails/generators/p2p_streams_channel/install_generator.rb
|
112
106
|
- lib/rails/generators/p2p_streams_channel/templates/controller.js
|
113
107
|
- lib/rails/generators/p2p_streams_channel/templates/initializer.rb
|
108
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/index.js
|
109
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/message.js
|
110
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/p2p_connection.js
|
111
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/p2p_controller.js
|
112
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/p2p_frame.js
|
113
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/p2p_peer.js
|
114
|
+
- lib/rails/generators/p2p_streams_channel/templates/p2p/package.json
|
114
115
|
- p2p_streams_channel.gemspec
|
115
116
|
- sig/p2p_streams_channel.rbs
|
116
117
|
- spec/dummy/Rakefile
|
@@ -158,6 +159,7 @@ files:
|
|
158
159
|
- spec/dummy/config/routes.rb
|
159
160
|
- spec/dummy/config/storage.yml
|
160
161
|
- spec/dummy/db/test.sqlite3
|
162
|
+
- spec/dummy/log/development.log
|
161
163
|
- spec/dummy/log/test.log
|
162
164
|
- spec/dummy/public/404.html
|
163
165
|
- spec/dummy/public/422.html
|
@@ -166,6 +168,13 @@ files:
|
|
166
168
|
- spec/dummy/public/apple-touch-icon.png
|
167
169
|
- spec/dummy/public/favicon.ico
|
168
170
|
- spec/dummy/tmp/development_secret.txt
|
171
|
+
- spec/dummy/vendor/javascript/p2p/index.js
|
172
|
+
- spec/dummy/vendor/javascript/p2p/message.js
|
173
|
+
- spec/dummy/vendor/javascript/p2p/p2p_connection.js
|
174
|
+
- spec/dummy/vendor/javascript/p2p/p2p_controller.js
|
175
|
+
- spec/dummy/vendor/javascript/p2p/p2p_frame.js
|
176
|
+
- spec/dummy/vendor/javascript/p2p/p2p_peer.js
|
177
|
+
- spec/dummy/vendor/javascript/p2p/package.json
|
169
178
|
- spec/p2p_peer_status_spec.rb
|
170
179
|
- spec/p2p_send_data_spec.rb
|
171
180
|
- spec/p2p_streams_channel_spec.rb
|
@@ -196,7 +205,8 @@ requirements: []
|
|
196
205
|
rubygems_version: 3.5.4
|
197
206
|
signing_key:
|
198
207
|
specification_version: 4
|
199
|
-
summary:
|
208
|
+
summary: Allow to setup one-to-many P2P stream connections (WebRTC) between clients
|
209
|
+
through Rails server (ActionCable) as the signaling server
|
200
210
|
test_files:
|
201
211
|
- spec/dummy/Rakefile
|
202
212
|
- spec/dummy/app/assets/config/manifest.js
|
@@ -243,6 +253,7 @@ test_files:
|
|
243
253
|
- spec/dummy/config/storage.yml
|
244
254
|
- spec/dummy/config.ru
|
245
255
|
- spec/dummy/db/test.sqlite3
|
256
|
+
- spec/dummy/log/development.log
|
246
257
|
- spec/dummy/log/test.log
|
247
258
|
- spec/dummy/public/404.html
|
248
259
|
- spec/dummy/public/422.html
|
@@ -251,6 +262,13 @@ test_files:
|
|
251
262
|
- spec/dummy/public/apple-touch-icon.png
|
252
263
|
- spec/dummy/public/favicon.ico
|
253
264
|
- spec/dummy/tmp/development_secret.txt
|
265
|
+
- spec/dummy/vendor/javascript/p2p/index.js
|
266
|
+
- spec/dummy/vendor/javascript/p2p/message.js
|
267
|
+
- spec/dummy/vendor/javascript/p2p/p2p_connection.js
|
268
|
+
- spec/dummy/vendor/javascript/p2p/p2p_controller.js
|
269
|
+
- spec/dummy/vendor/javascript/p2p/p2p_frame.js
|
270
|
+
- spec/dummy/vendor/javascript/p2p/p2p_peer.js
|
271
|
+
- spec/dummy/vendor/javascript/p2p/package.json
|
254
272
|
- spec/p2p_peer_status_spec.rb
|
255
273
|
- spec/p2p_send_data_spec.rb
|
256
274
|
- spec/p2p_streams_channel_spec.rb
|
/data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/index.js
RENAMED
File without changes
|
/data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/message.js
RENAMED
File without changes
|
File without changes
|
File without changes
|
/data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_frame.js
RENAMED
File without changes
|
/data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_peer.js
RENAMED
File without changes
|
/data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/package.json
RENAMED
File without changes
|