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.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -14
  3. data/lib/p2p_streams_channel/engine.rb +0 -6
  4. data/lib/p2p_streams_channel/version.rb +1 -1
  5. data/lib/rails/generators/p2p_streams_channel/install_generator.rb +8 -2
  6. data/p2p_streams_channel.gemspec +2 -2
  7. data/spec/dummy/config/importmap.rb +2 -0
  8. data/spec/dummy/log/development.log +0 -0
  9. data/spec/dummy/vendor/javascript/p2p/index.js +6 -0
  10. data/spec/dummy/vendor/javascript/p2p/message.js +22 -0
  11. data/spec/dummy/vendor/javascript/p2p/p2p_connection.js +133 -0
  12. data/spec/dummy/vendor/javascript/p2p/p2p_controller.js +52 -0
  13. data/spec/dummy/vendor/javascript/p2p/p2p_frame.js +152 -0
  14. data/spec/dummy/vendor/javascript/p2p/p2p_peer.js +208 -0
  15. data/spec/dummy/vendor/javascript/p2p/package.json +14 -0
  16. metadata +29 -11
  17. /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/index.js +0 -0
  18. /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/message.js +0 -0
  19. /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_connection.js +0 -0
  20. /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_controller.js +0 -0
  21. /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_frame.js +0 -0
  22. /data/{app/assets/javascripts → lib/rails/generators/p2p_streams_channel/templates}/p2p/p2p_peer.js +0 -0
  23. /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: a8bda1bfc27255440985ebe30c675e2177ca08bf446b75d6ae9a7cfa5c619c59
4
- data.tar.gz: 4fd9269cf47645dff407538bb4a817c490623bed92b63f3a691c71c1ea8159c1
3
+ metadata.gz: 133464f081bff5e3415e6129f9ca134a3c09658647aa159ef0327b48670e9efc
4
+ data.tar.gz: 5b4cd920727bf06e0a64d6a999bd4e4e49ef7b4db19019972adba44c50800792
5
5
  SHA512:
6
- metadata.gz: 7df5626c1517a3a18a8d495a2ac2d22bdbd1e7fe3c3f04a87b0ff02eed8b33c9c9c3a3f683b14c86ad157960ce2bb8f058fdd841c6ece438115bc6c098f01020
7
- data.tar.gz: '082a674eb465701535db76a9d4772658f4bef2dc7b7246e2628ad1afd7af55731259c9041b0e58c9b13840b6f823cd07a4037f9ffa9b85c785285d3b78eaad29'
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, 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.
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
- Now you could implement your client side,
123
- for example: here is a simple chat controller:
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module P2pStreamsChannel
4
- VERSION = "0.0.2"
4
+ VERSION = "0.0.3"
5
5
  end
@@ -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 "#{File.expand_path("../../../../app/assets/javascripts/p2p/", __dir__)}", under: "p2p"\n)
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:#{File.expand_path("../../../../app/assets/javascripts/p2p/", __dir__)}"
23
+ run "yarn add p2p@file:vendor/javascript/p2p"
18
24
  end
19
25
 
20
26
  def create_initializer
@@ -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 = "rails p2p turbo streams channel"
11
- spec.description = "rails p2p turbo streams channel"
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,6 @@
1
+ import "p2p/p2p_frame"
2
+ import P2pController from "p2p/p2p_controller"
3
+
4
+ export {
5
+ P2pController
6
+ }
@@ -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.2
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-03-28 00:00:00.000000000 Z
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: rails p2p turbo streams channel
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: rails p2p turbo streams channel
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