webrtc-ruby 0.1.0
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 +7 -0
- data/.dockerignore +19 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +12 -0
- data/Dockerfile +49 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/Rakefile +42 -0
- data/examples/signaling_server/server.rb +200 -0
- data/examples/simple_data_channel.rb +81 -0
- data/examples/video_call.rb +152 -0
- data/ext/webrtc_ruby/CMakeLists.txt +84 -0
- data/ext/webrtc_ruby/Makefile +31 -0
- data/ext/webrtc_ruby/webrtc_ruby.c +757 -0
- data/ext/webrtc_ruby/webrtc_ruby.h +169 -0
- data/lib/webrtc/configuration.rb +99 -0
- data/lib/webrtc/data_channel.rb +154 -0
- data/lib/webrtc/dtls_transport.rb +54 -0
- data/lib/webrtc/dtmf_sender.rb +81 -0
- data/lib/webrtc/errors.rb +10 -0
- data/lib/webrtc/ffi/library.rb +100 -0
- data/lib/webrtc/ice_candidate.rb +62 -0
- data/lib/webrtc/ice_transport.rb +95 -0
- data/lib/webrtc/media_stream.rb +67 -0
- data/lib/webrtc/media_stream_track.rb +83 -0
- data/lib/webrtc/peer_connection.rb +346 -0
- data/lib/webrtc/promise.rb +59 -0
- data/lib/webrtc/rtp_receiver.rb +51 -0
- data/lib/webrtc/rtp_sender.rb +85 -0
- data/lib/webrtc/rtp_transceiver.rb +34 -0
- data/lib/webrtc/sctp_transport.rb +31 -0
- data/lib/webrtc/session_description.rb +64 -0
- data/lib/webrtc/stats_report.rb +199 -0
- data/lib/webrtc/version.rb +5 -0
- data/lib/webrtc.rb +38 -0
- data/webrtc-ruby.gemspec +33 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 67e8d8f0e59e83bb3a7d49ec981fbb179f4a417e827c9cc2109ab0e204aa20de
|
|
4
|
+
data.tar.gz: f726ca624518f56a58cf7846e9eb36c7030d13c9f6052fd681165585279f66de
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2505178e6228f9c0fe66d854355a61601d3fa225c1ee3c4618dc2fa74abccd750f666e581430cda79a310182550271dcfb7a1591093d70cc55bd4708390990c0
|
|
7
|
+
data.tar.gz: ec7bfeeb58a2a1371cffcce47c45063f518e727f251759fdaf775a76f05258508fed7c4eb6ae1438aa1722e261b0ce648a78fc863a6cc44d7bef6558e1a1bb02
|
data/.dockerignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.1.0 - 2025-12-31
|
|
11
|
+
|
|
12
|
+
- Initial release
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
FROM ruby:3.3-slim
|
|
2
|
+
|
|
3
|
+
# Install build dependencies
|
|
4
|
+
RUN apt-get update && apt-get install -y \
|
|
5
|
+
build-essential \
|
|
6
|
+
cmake \
|
|
7
|
+
git \
|
|
8
|
+
libssl-dev \
|
|
9
|
+
pkg-config \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
|
|
14
|
+
# Copy gemfiles first for better caching
|
|
15
|
+
COPY Gemfile Gemfile.lock* webrtc-ruby.gemspec ./
|
|
16
|
+
COPY lib/webrtc/version.rb ./lib/webrtc/
|
|
17
|
+
|
|
18
|
+
# Install Ruby dependencies
|
|
19
|
+
RUN bundle install
|
|
20
|
+
|
|
21
|
+
# Clone and build libdatachannel
|
|
22
|
+
RUN git clone --depth 1 --recurse-submodules https://github.com/paullouisageneau/libdatachannel.git /tmp/libdatachannel \
|
|
23
|
+
&& cd /tmp/libdatachannel \
|
|
24
|
+
&& mkdir build && cd build \
|
|
25
|
+
&& cmake -DUSE_GNUTLS=0 -DUSE_NICE=0 -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 -DCMAKE_BUILD_TYPE=Release .. \
|
|
26
|
+
&& make -j$(nproc) \
|
|
27
|
+
&& mkdir -p /app/vendor/libdatachannel/build \
|
|
28
|
+
&& mkdir -p /app/vendor/libdatachannel/include \
|
|
29
|
+
&& cp -r /tmp/libdatachannel/include/* /app/vendor/libdatachannel/include/ \
|
|
30
|
+
&& cp /tmp/libdatachannel/build/libdatachannel.so* /app/vendor/libdatachannel/build/ \
|
|
31
|
+
&& rm -rf /tmp/libdatachannel
|
|
32
|
+
|
|
33
|
+
# Copy extension source
|
|
34
|
+
COPY ext/ ./ext/
|
|
35
|
+
|
|
36
|
+
# Build webrtc_ruby extension
|
|
37
|
+
RUN cd ext/webrtc_ruby \
|
|
38
|
+
&& mkdir -p build && cd build \
|
|
39
|
+
&& cmake .. \
|
|
40
|
+
&& make -j$(nproc)
|
|
41
|
+
|
|
42
|
+
# Copy the rest of the application
|
|
43
|
+
COPY . .
|
|
44
|
+
|
|
45
|
+
# Set library path
|
|
46
|
+
ENV LD_LIBRARY_PATH=/app/ext/webrtc_ruby/build:/app/vendor/libdatachannel/build
|
|
47
|
+
|
|
48
|
+
# Run tests by default
|
|
49
|
+
CMD ["bundle", "exec", "rspec", "--format", "documentation"]
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# WebRTC Ruby
|
|
2
|
+
|
|
3
|
+
WebRTC bindings for Ruby, providing real-time audio, video, and data channel capabilities.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Full WebRTC API following W3C specifications
|
|
8
|
+
- Peer-to-peer connections with ICE/STUN/TURN support
|
|
9
|
+
- Data channels for bidirectional data transfer
|
|
10
|
+
- Media stream and track handling
|
|
11
|
+
- DTMF tone sending
|
|
12
|
+
- Connection statistics
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Ruby 3.1+
|
|
17
|
+
- CMake
|
|
18
|
+
- OpenSSL development libraries
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Add this line to your application's Gemfile:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem 'webrtc-ruby'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
And then execute:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Building Native Extension
|
|
35
|
+
|
|
36
|
+
The gem requires building a native extension that wraps [libdatachannel](https://github.com/paullouisageneau/libdatachannel).
|
|
37
|
+
|
|
38
|
+
#### macOS
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
brew install cmake openssl
|
|
42
|
+
|
|
43
|
+
# Clone and build libdatachannel
|
|
44
|
+
git clone --depth 1 --recurse-submodules https://github.com/paullouisageneau/libdatachannel.git vendor/libdatachannel
|
|
45
|
+
cd vendor/libdatachannel
|
|
46
|
+
mkdir build && cd build
|
|
47
|
+
cmake -DUSE_GNUTLS=0 -DUSE_NICE=0 -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 ..
|
|
48
|
+
make -j$(sysctl -n hw.ncpu)
|
|
49
|
+
cd ../../..
|
|
50
|
+
|
|
51
|
+
# Build the extension
|
|
52
|
+
cd ext/webrtc_ruby
|
|
53
|
+
mkdir build && cd build
|
|
54
|
+
cmake ..
|
|
55
|
+
make
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Linux (Ubuntu/Debian)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sudo apt install build-essential cmake git libssl-dev pkg-config
|
|
62
|
+
|
|
63
|
+
# Clone and build libdatachannel
|
|
64
|
+
git clone --depth 1 --recurse-submodules https://github.com/paullouisageneau/libdatachannel.git vendor/libdatachannel
|
|
65
|
+
cd vendor/libdatachannel
|
|
66
|
+
mkdir build && cd build
|
|
67
|
+
cmake -DUSE_GNUTLS=0 -DUSE_NICE=0 -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 ..
|
|
68
|
+
make -j$(nproc)
|
|
69
|
+
cd ../../..
|
|
70
|
+
|
|
71
|
+
# Build the extension
|
|
72
|
+
cd ext/webrtc_ruby
|
|
73
|
+
mkdir build && cd build
|
|
74
|
+
cmake ..
|
|
75
|
+
make
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Docker
|
|
79
|
+
|
|
80
|
+
You can also use Docker to build and test:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
docker build -t webrtc-ruby .
|
|
84
|
+
docker run --rm webrtc-ruby
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Basic Data Channel Example
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
require 'webrtc'
|
|
93
|
+
|
|
94
|
+
# Initialize WebRTC
|
|
95
|
+
WebRTC.init
|
|
96
|
+
|
|
97
|
+
# Create peer connections
|
|
98
|
+
pc1 = WebRTC::RTCPeerConnection.new
|
|
99
|
+
pc2 = WebRTC::RTCPeerConnection.new
|
|
100
|
+
|
|
101
|
+
# Collect ICE candidates
|
|
102
|
+
pc1_candidates = []
|
|
103
|
+
pc2_candidates = []
|
|
104
|
+
|
|
105
|
+
pc1.on_ice_candidate { |candidate| pc1_candidates << candidate if candidate }
|
|
106
|
+
pc2.on_ice_candidate { |candidate| pc2_candidates << candidate if candidate }
|
|
107
|
+
|
|
108
|
+
# Handle incoming data channel on pc2
|
|
109
|
+
pc2.on_data_channel do |dc|
|
|
110
|
+
puts "Received data channel: #{dc.label}"
|
|
111
|
+
dc.on_message { |event| puts "Received: #{event.data}" }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Create data channel on pc1
|
|
115
|
+
dc1 = pc1.create_data_channel('chat')
|
|
116
|
+
dc1.on_open { puts "Data channel opened!" }
|
|
117
|
+
dc1.on_message { |event| puts "Received: #{event.data}" }
|
|
118
|
+
|
|
119
|
+
# Signaling: Create and exchange offer/answer
|
|
120
|
+
offer = pc1.create_offer.await
|
|
121
|
+
pc1.set_local_description(offer).await
|
|
122
|
+
pc2.set_remote_description(offer).await
|
|
123
|
+
|
|
124
|
+
answer = pc2.create_answer.await
|
|
125
|
+
pc2.set_local_description(answer).await
|
|
126
|
+
pc1.set_remote_description(answer).await
|
|
127
|
+
|
|
128
|
+
# Exchange ICE candidates
|
|
129
|
+
pc1_candidates.each { |c| pc2.add_ice_candidate(c).await rescue nil }
|
|
130
|
+
pc2_candidates.each { |c| pc1.add_ice_candidate(c).await rescue nil }
|
|
131
|
+
|
|
132
|
+
# Cleanup
|
|
133
|
+
dc1.destroy
|
|
134
|
+
pc1.close
|
|
135
|
+
pc2.close
|
|
136
|
+
WebRTC.cleanup
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Configuration with ICE Servers
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
config = {
|
|
143
|
+
ice_servers: [
|
|
144
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
145
|
+
{ urls: 'turn:turn.example.com', username: 'user', credential: 'pass' }
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pc = WebRTC::RTCPeerConnection.new(config)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Data Channel Options
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# Ordered, reliable channel (default)
|
|
156
|
+
dc = pc.create_data_channel('reliable')
|
|
157
|
+
|
|
158
|
+
# Unordered channel with max retransmits
|
|
159
|
+
dc = pc.create_data_channel('unreliable', ordered: false, max_retransmits: 3)
|
|
160
|
+
|
|
161
|
+
# Channel with max packet lifetime
|
|
162
|
+
dc = pc.create_data_channel('timed', max_packet_life_time: 3000)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Connection State Monitoring
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
pc.on_connection_state_change do |state|
|
|
169
|
+
puts "Connection state: #{state}" # :new, :connecting, :connected, :disconnected, :failed, :closed
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
pc.on_ice_gathering_state_change do |state|
|
|
173
|
+
puts "ICE gathering: #{state}" # :new, :gathering, :complete
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
pc.on_ice_connection_state_change do |state|
|
|
177
|
+
puts "ICE connection: #{state}" # :new, :checking, :connected, :completed, :failed, :disconnected, :closed
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Getting Statistics
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
stats = pc.get_stats.await
|
|
185
|
+
|
|
186
|
+
stats.each do |id, stat|
|
|
187
|
+
case stat
|
|
188
|
+
when WebRTC::RTCPeerConnectionStats
|
|
189
|
+
puts "Data channels opened: #{stat.data_channels_opened}"
|
|
190
|
+
when WebRTC::RTCTransportStats
|
|
191
|
+
puts "Bytes sent: #{stat.bytes_sent}"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## API Reference
|
|
197
|
+
|
|
198
|
+
### Core Classes
|
|
199
|
+
|
|
200
|
+
- `WebRTC::RTCPeerConnection` - Main peer connection class
|
|
201
|
+
- `WebRTC::RTCSessionDescription` - SDP session description
|
|
202
|
+
- `WebRTC::RTCIceCandidate` - ICE candidate
|
|
203
|
+
- `WebRTC::RTCDataChannel` - Data channel for sending/receiving data
|
|
204
|
+
|
|
205
|
+
### Media Classes
|
|
206
|
+
|
|
207
|
+
- `WebRTC::MediaStream` - Collection of media tracks
|
|
208
|
+
- `WebRTC::MediaStreamTrack` - Individual audio or video track
|
|
209
|
+
- `WebRTC::RTCRtpSender` - Sends media to remote peer
|
|
210
|
+
- `WebRTC::RTCRtpReceiver` - Receives media from remote peer
|
|
211
|
+
- `WebRTC::RTCRtpTransceiver` - Combines sender and receiver
|
|
212
|
+
|
|
213
|
+
### Transport Classes
|
|
214
|
+
|
|
215
|
+
- `WebRTC::RTCDtlsTransport` - DTLS transport layer
|
|
216
|
+
- `WebRTC::RTCIceTransport` - ICE transport layer
|
|
217
|
+
- `WebRTC::RTCSctpTransport` - SCTP transport for data channels
|
|
218
|
+
|
|
219
|
+
### Other Classes
|
|
220
|
+
|
|
221
|
+
- `WebRTC::RTCDTMFSender` - Send DTMF tones
|
|
222
|
+
- `WebRTC::RTCStatsReport` - Connection statistics
|
|
223
|
+
|
|
224
|
+
## Development
|
|
225
|
+
|
|
226
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
227
|
+
|
|
228
|
+
### Running Tests
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# Build the native extension first
|
|
232
|
+
cd ext/webrtc_ruby/build && cmake .. && make && cd ../../..
|
|
233
|
+
|
|
234
|
+
# Run tests
|
|
235
|
+
DYLD_LIBRARY_PATH="ext/webrtc_ruby/build:vendor/libdatachannel/build" bundle exec rspec
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Running Examples
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
DYLD_LIBRARY_PATH="ext/webrtc_ruby/build:vendor/libdatachannel/build" bundle exec ruby examples/simple_data_channel.rb
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Acknowledgments
|
|
245
|
+
|
|
246
|
+
This project is built on top of:
|
|
247
|
+
|
|
248
|
+
- [libdatachannel](https://github.com/paullouisageneau/libdatachannel) - C/C++ WebRTC network library by Paul-Louis Ageneau
|
|
249
|
+
- [Ruby FFI](https://github.com/ffi/ffi) - Foreign Function Interface for Ruby
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
254
|
+
|
|
255
|
+
## Contributing
|
|
256
|
+
|
|
257
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/webrtc-ruby.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rspec/core/rake_task'
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
namespace :native do
|
|
9
|
+
desc 'Build native library'
|
|
10
|
+
task :build do
|
|
11
|
+
ext_dir = File.join(__dir__, 'ext', 'webrtc_ruby')
|
|
12
|
+
build_dir = File.join(ext_dir, 'build')
|
|
13
|
+
|
|
14
|
+
Dir.mkdir(build_dir) unless Dir.exist?(build_dir)
|
|
15
|
+
|
|
16
|
+
case RbConfig::CONFIG['host_os']
|
|
17
|
+
when /darwin/
|
|
18
|
+
lib_name = 'libwebrtc_ruby.dylib'
|
|
19
|
+
flags = '-dynamiclib'
|
|
20
|
+
when /mswin|mingw/
|
|
21
|
+
lib_name = 'webrtc_ruby.dll'
|
|
22
|
+
flags = '-shared'
|
|
23
|
+
else
|
|
24
|
+
lib_name = 'libwebrtc_ruby.so'
|
|
25
|
+
flags = '-shared'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
cmd = "cc -Wall -Wextra -fPIC -O2 -DWEBRTC_RUBY_BUILDING #{flags} " \
|
|
29
|
+
"-o #{File.join(build_dir, lib_name)} " \
|
|
30
|
+
"#{File.join(ext_dir, 'webrtc_ruby.c')}"
|
|
31
|
+
|
|
32
|
+
system(cmd) || abort('Native build failed')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc 'Clean native build'
|
|
36
|
+
task :clean do
|
|
37
|
+
build_dir = File.join(__dir__, 'ext', 'webrtc_ruby', 'build')
|
|
38
|
+
FileUtils.rm_rf(build_dir)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
task default: %i[native:build spec]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# WebRTC Signaling Server Example
|
|
5
|
+
#
|
|
6
|
+
# This is a conceptual example showing how to implement
|
|
7
|
+
# a WebRTC signaling server using WebSockets.
|
|
8
|
+
#
|
|
9
|
+
# In a real application, you would use a WebSocket library
|
|
10
|
+
# like 'faye-websocket' or 'websocket-eventmachine-server'.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# gem install faye-websocket puma
|
|
14
|
+
# ruby server.rb
|
|
15
|
+
|
|
16
|
+
require 'json'
|
|
17
|
+
|
|
18
|
+
# Simulated signaling server logic
|
|
19
|
+
class SignalingServer
|
|
20
|
+
def initialize
|
|
21
|
+
@rooms = {}
|
|
22
|
+
@clients = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Handle incoming WebSocket message
|
|
26
|
+
def handle_message(client_id, message)
|
|
27
|
+
data = JSON.parse(message)
|
|
28
|
+
room_id = data['room']
|
|
29
|
+
|
|
30
|
+
case data['type']
|
|
31
|
+
when 'join'
|
|
32
|
+
join_room(client_id, room_id)
|
|
33
|
+
|
|
34
|
+
when 'offer'
|
|
35
|
+
# Forward offer to other peer in room
|
|
36
|
+
forward_to_peer(client_id, room_id, {
|
|
37
|
+
type: 'offer',
|
|
38
|
+
sdp: data['sdp']
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
when 'answer'
|
|
42
|
+
# Forward answer to other peer in room
|
|
43
|
+
forward_to_peer(client_id, room_id, {
|
|
44
|
+
type: 'answer',
|
|
45
|
+
sdp: data['sdp']
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
when 'ice-candidate'
|
|
49
|
+
# Forward ICE candidate to other peer
|
|
50
|
+
forward_to_peer(client_id, room_id, {
|
|
51
|
+
type: 'ice-candidate',
|
|
52
|
+
candidate: data['candidate'],
|
|
53
|
+
sdpMid: data['sdpMid'],
|
|
54
|
+
sdpMLineIndex: data['sdpMLineIndex']
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
when 'leave'
|
|
58
|
+
leave_room(client_id, room_id)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def join_room(client_id, room_id)
|
|
65
|
+
@rooms[room_id] ||= []
|
|
66
|
+
@rooms[room_id] << client_id
|
|
67
|
+
@clients[client_id] = room_id
|
|
68
|
+
|
|
69
|
+
# Notify if room has 2 peers (ready to connect)
|
|
70
|
+
return unless @rooms[room_id].size == 2
|
|
71
|
+
|
|
72
|
+
# Tell the first peer to create an offer
|
|
73
|
+
first_peer = @rooms[room_id].first
|
|
74
|
+
send_to_client(first_peer, { type: 'ready' })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def leave_room(client_id, room_id)
|
|
78
|
+
@rooms[room_id]&.delete(client_id)
|
|
79
|
+
@clients.delete(client_id)
|
|
80
|
+
|
|
81
|
+
# Notify remaining peer
|
|
82
|
+
@rooms[room_id]&.each do |peer_id|
|
|
83
|
+
send_to_client(peer_id, { type: 'peer-left' })
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def forward_to_peer(from_client_id, room_id, message)
|
|
88
|
+
@rooms[room_id]&.each do |peer_id|
|
|
89
|
+
next if peer_id == from_client_id
|
|
90
|
+
|
|
91
|
+
send_to_client(peer_id, message)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def send_to_client(client_id, message)
|
|
96
|
+
# In a real implementation, this would send via WebSocket
|
|
97
|
+
puts "Sending to #{client_id}: #{message.to_json}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Example client-side pseudocode (would run in browser or Ruby client):
|
|
102
|
+
#
|
|
103
|
+
# class WebRTCClient
|
|
104
|
+
# def initialize(signaling_url, room_id)
|
|
105
|
+
# @room_id = room_id
|
|
106
|
+
# @pc = WebRTC::RTCPeerConnection.new
|
|
107
|
+
#
|
|
108
|
+
# # Connect to signaling server
|
|
109
|
+
# @ws = WebSocket.new(signaling_url)
|
|
110
|
+
#
|
|
111
|
+
# # Set up ICE candidate handling
|
|
112
|
+
# @pc.on_ice_candidate do |candidate|
|
|
113
|
+
# @ws.send({
|
|
114
|
+
# type: 'ice-candidate',
|
|
115
|
+
# room: @room_id,
|
|
116
|
+
# candidate: candidate.candidate,
|
|
117
|
+
# sdpMid: candidate.sdp_mid,
|
|
118
|
+
# sdpMLineIndex: candidate.sdp_m_line_index
|
|
119
|
+
# }.to_json)
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
122
|
+
# # Handle signaling messages
|
|
123
|
+
# @ws.on_message do |msg|
|
|
124
|
+
# data = JSON.parse(msg)
|
|
125
|
+
# handle_signaling(data)
|
|
126
|
+
# end
|
|
127
|
+
# end
|
|
128
|
+
#
|
|
129
|
+
# def join
|
|
130
|
+
# @ws.send({ type: 'join', room: @room_id }.to_json)
|
|
131
|
+
# end
|
|
132
|
+
#
|
|
133
|
+
# def handle_signaling(data)
|
|
134
|
+
# case data['type']
|
|
135
|
+
# when 'ready'
|
|
136
|
+
# # We're the initiator, create offer
|
|
137
|
+
# create_and_send_offer
|
|
138
|
+
#
|
|
139
|
+
# when 'offer'
|
|
140
|
+
# # Received offer, create answer
|
|
141
|
+
# @pc.set_remote_description(type: :offer, sdp: data['sdp']).await
|
|
142
|
+
# answer = @pc.create_answer.await
|
|
143
|
+
# @pc.set_local_description(answer).await
|
|
144
|
+
#
|
|
145
|
+
# @ws.send({
|
|
146
|
+
# type: 'answer',
|
|
147
|
+
# room: @room_id,
|
|
148
|
+
# sdp: answer.sdp
|
|
149
|
+
# }.to_json)
|
|
150
|
+
#
|
|
151
|
+
# when 'answer'
|
|
152
|
+
# @pc.set_remote_description(type: :answer, sdp: data['sdp']).await
|
|
153
|
+
#
|
|
154
|
+
# when 'ice-candidate'
|
|
155
|
+
# candidate = WebRTC::RTCIceCandidate.new(
|
|
156
|
+
# candidate: data['candidate'],
|
|
157
|
+
# sdp_mid: data['sdpMid'],
|
|
158
|
+
# sdp_m_line_index: data['sdpMLineIndex']
|
|
159
|
+
# )
|
|
160
|
+
# @pc.add_ice_candidate(candidate).await
|
|
161
|
+
# end
|
|
162
|
+
# end
|
|
163
|
+
#
|
|
164
|
+
# def create_and_send_offer
|
|
165
|
+
# offer = @pc.create_offer.await
|
|
166
|
+
# @pc.set_local_description(offer).await
|
|
167
|
+
#
|
|
168
|
+
# @ws.send({
|
|
169
|
+
# type: 'offer',
|
|
170
|
+
# room: @room_id,
|
|
171
|
+
# sdp: offer.sdp
|
|
172
|
+
# }.to_json)
|
|
173
|
+
# end
|
|
174
|
+
# end
|
|
175
|
+
|
|
176
|
+
if __FILE__ == $0
|
|
177
|
+
puts 'WebRTC Signaling Server Example'
|
|
178
|
+
puts '================================'
|
|
179
|
+
puts
|
|
180
|
+
puts 'This is a conceptual example showing signaling server logic.'
|
|
181
|
+
puts 'In production, use with faye-websocket or similar WebSocket library.'
|
|
182
|
+
puts
|
|
183
|
+
puts 'Example signaling flow:'
|
|
184
|
+
puts "1. Client A joins room 'test'"
|
|
185
|
+
puts "2. Client B joins room 'test'"
|
|
186
|
+
puts '3. Server tells Client A to create offer'
|
|
187
|
+
puts '4. Client A sends offer to server'
|
|
188
|
+
puts '5. Server forwards offer to Client B'
|
|
189
|
+
puts '6. Client B creates and sends answer'
|
|
190
|
+
puts '7. Server forwards answer to Client A'
|
|
191
|
+
puts '8. Both clients exchange ICE candidates via server'
|
|
192
|
+
puts '9. P2P connection established!'
|
|
193
|
+
|
|
194
|
+
# Demo
|
|
195
|
+
server = SignalingServer.new
|
|
196
|
+
server.handle_message('client_a', '{"type":"join","room":"test"}')
|
|
197
|
+
server.handle_message('client_b', '{"type":"join","room":"test"}')
|
|
198
|
+
server.handle_message('client_a', '{"type":"offer","room":"test","sdp":"v=0..."}')
|
|
199
|
+
server.handle_message('client_b', '{"type":"answer","room":"test","sdp":"v=0..."}')
|
|
200
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Simple Data Channel Example
|
|
5
|
+
# This example demonstrates creating a data channel between two peers
|
|
6
|
+
# using the real libdatachannel WebRTC implementation.
|
|
7
|
+
|
|
8
|
+
require_relative '../lib/webrtc'
|
|
9
|
+
|
|
10
|
+
puts '=== WebRTC Ruby DataChannel Example ==='
|
|
11
|
+
puts
|
|
12
|
+
|
|
13
|
+
WebRTC.init
|
|
14
|
+
|
|
15
|
+
pc1 = WebRTC::RTCPeerConnection.new
|
|
16
|
+
pc2 = WebRTC::RTCPeerConnection.new
|
|
17
|
+
|
|
18
|
+
# Track ICE candidates for exchange after remote descriptions are set
|
|
19
|
+
pc1_candidates = []
|
|
20
|
+
pc2_candidates = []
|
|
21
|
+
|
|
22
|
+
pc1.on_ice_candidate do |candidate|
|
|
23
|
+
pc1_candidates << candidate if candidate
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
pc2.on_ice_candidate do |candidate|
|
|
27
|
+
pc2_candidates << candidate if candidate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
pc2.on_data_channel do |dc|
|
|
31
|
+
puts "PC2: Received data channel: #{dc.label}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Create data channel on PC1
|
|
35
|
+
dc1 = pc1.create_data_channel('chat')
|
|
36
|
+
puts "Created data channel: #{dc1.label}"
|
|
37
|
+
|
|
38
|
+
dc1.on_open do
|
|
39
|
+
puts 'DataChannel opened!'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
dc1.on_message do |event|
|
|
43
|
+
puts "Received: #{event.data}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Step 1: PC1 creates offer
|
|
47
|
+
offer = pc1.create_offer.await
|
|
48
|
+
puts 'Created offer'
|
|
49
|
+
puts " SDP length: #{offer.sdp.length} bytes"
|
|
50
|
+
|
|
51
|
+
# Step 2: PC1 sets local description
|
|
52
|
+
pc1.set_local_description(offer).await
|
|
53
|
+
puts 'PC1: Set local description'
|
|
54
|
+
|
|
55
|
+
# Step 3: PC2 sets remote description (the offer)
|
|
56
|
+
pc2.set_remote_description(offer).await
|
|
57
|
+
puts 'PC2: Set remote description'
|
|
58
|
+
|
|
59
|
+
# Step 4: Now exchange ICE candidates (after remote descriptions are set)
|
|
60
|
+
puts 'Exchanging ICE candidates...'
|
|
61
|
+
pc1_candidates.each do |candidate|
|
|
62
|
+
pc2.add_ice_candidate(candidate).await
|
|
63
|
+
rescue StandardError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
puts " Sent #{pc1_candidates.size} candidates from PC1 to PC2"
|
|
67
|
+
|
|
68
|
+
# NOTE: In a real application, you would wait for the connection to be established
|
|
69
|
+
# before sending data. For this demo, we just show the setup.
|
|
70
|
+
|
|
71
|
+
puts "\nSignaling complete!"
|
|
72
|
+
puts "DataChannel state: #{dc1.ready_state}"
|
|
73
|
+
puts ' (Channel will open when connection is established)'
|
|
74
|
+
|
|
75
|
+
# Cleanup
|
|
76
|
+
dc1.destroy
|
|
77
|
+
pc1.close
|
|
78
|
+
pc2.close
|
|
79
|
+
|
|
80
|
+
WebRTC.cleanup
|
|
81
|
+
puts "\nDone!"
|