p2p_streams_channel 0.0.2 → 0.0.3
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 +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
|