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.
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