webrtc-ruby 1.0.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +19 -0
  3. data/.rspec +3 -0
  4. data/CHANGELOG.md +12 -0
  5. data/Dockerfile +49 -0
  6. data/LICENSE +201 -0
  7. data/README.md +264 -0
  8. data/Rakefile +42 -0
  9. data/examples/signaling_server/server.rb +200 -0
  10. data/examples/simple_data_channel.rb +81 -0
  11. data/examples/video_call.rb +152 -0
  12. data/ext/webrtc_ruby/CMakeLists.txt +84 -0
  13. data/ext/webrtc_ruby/Makefile +31 -0
  14. data/ext/webrtc_ruby/webrtc_ruby.c +994 -0
  15. data/ext/webrtc_ruby/webrtc_ruby.h +212 -0
  16. data/lib/webrtc/configuration.rb +99 -0
  17. data/lib/webrtc/data_channel.rb +216 -0
  18. data/lib/webrtc/dtls_transport.rb +54 -0
  19. data/lib/webrtc/dtmf_sender.rb +81 -0
  20. data/lib/webrtc/errors.rb +10 -0
  21. data/lib/webrtc/factory.rb +28 -0
  22. data/lib/webrtc/ffi/library.rb +122 -0
  23. data/lib/webrtc/ice_candidate.rb +63 -0
  24. data/lib/webrtc/ice_transport.rb +95 -0
  25. data/lib/webrtc/media_interfaces.rb +101 -0
  26. data/lib/webrtc/media_stream.rb +67 -0
  27. data/lib/webrtc/media_stream_track.rb +83 -0
  28. data/lib/webrtc/observers.rb +51 -0
  29. data/lib/webrtc/parity_types.rb +358 -0
  30. data/lib/webrtc/peer_connection.rb +577 -0
  31. data/lib/webrtc/promise.rb +59 -0
  32. data/lib/webrtc/rtp_receiver.rb +79 -0
  33. data/lib/webrtc/rtp_sender.rb +117 -0
  34. data/lib/webrtc/rtp_transceiver.rb +39 -0
  35. data/lib/webrtc/sctp_transport.rb +31 -0
  36. data/lib/webrtc/session_description.rb +65 -0
  37. data/lib/webrtc/stats_report.rb +199 -0
  38. data/lib/webrtc/version.rb +5 -0
  39. data/lib/webrtc/video_frame.rb +29 -0
  40. data/lib/webrtc.rb +43 -0
  41. data/webrtc-ruby.gemspec +33 -0
  42. metadata +113 -0
@@ -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!"
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # WebRTC Connection Example
5
+ # This example demonstrates a WebRTC connection between two peers
6
+ # with data channel and DTMF support simulation.
7
+
8
+ require_relative '../lib/webrtc'
9
+
10
+ puts '=== WebRTC Ruby Connection Example ==='
11
+ puts
12
+
13
+ # Initialize WebRTC
14
+ WebRTC.init
15
+
16
+ # Create two peer connections (simulating caller and callee)
17
+ caller_config = {
18
+ ice_servers: [
19
+ { urls: 'stun:stun.l.google.com:19302' }
20
+ ]
21
+ }
22
+
23
+ caller_pc = WebRTC::RTCPeerConnection.new(caller_config)
24
+ callee_pc = WebRTC::RTCPeerConnection.new(caller_config)
25
+
26
+ puts 'Created peer connections'
27
+
28
+ # Set up ICE candidate exchange
29
+ caller_ice_candidates = []
30
+ callee_ice_candidates = []
31
+
32
+ caller_pc.on_ice_candidate do |candidate|
33
+ caller_ice_candidates << candidate if candidate
34
+ end
35
+
36
+ callee_pc.on_ice_candidate do |candidate|
37
+ callee_ice_candidates << candidate if candidate
38
+ end
39
+
40
+ # Monitor connection states
41
+ caller_pc.on_connection_state_change do |state|
42
+ puts "Caller connection state: #{state}"
43
+ end
44
+
45
+ callee_pc.on_connection_state_change do |state|
46
+ puts "Callee connection state: #{state}"
47
+ end
48
+
49
+ # Create data channel for signaling/messaging
50
+ puts "\nCreating data channel..."
51
+ data_channel = caller_pc.create_data_channel('messaging', ordered: true)
52
+ puts " Data channel created: #{data_channel.label}"
53
+
54
+ data_channel.on_open do
55
+ puts ' Data channel opened!'
56
+ end
57
+
58
+ data_channel.on_message do |event|
59
+ puts " Received message: #{event.data}"
60
+ end
61
+
62
+ # Set up callee to receive data channels
63
+ callee_pc.on_data_channel do |dc|
64
+ puts "Callee received data channel: #{dc.label}"
65
+ dc.on_message do |event|
66
+ puts "Callee received: #{event.data}"
67
+ end
68
+ end
69
+
70
+ # Perform signaling
71
+ puts "\n=== Signaling Exchange ==="
72
+
73
+ # 1. Caller creates offer
74
+ puts "\n1. Caller creates offer..."
75
+ offer = caller_pc.create_offer.await
76
+ puts " Offer type: #{offer.type}"
77
+ puts " SDP length: #{offer.sdp.length} bytes"
78
+
79
+ # 2. Caller sets local description
80
+ puts "\n2. Caller sets local description..."
81
+ caller_pc.set_local_description(offer).await
82
+ puts ' Local description set'
83
+
84
+ # 3. Callee receives and sets remote description
85
+ puts "\n3. Callee sets remote description..."
86
+ callee_pc.set_remote_description(offer).await
87
+ puts ' Remote description set'
88
+
89
+ # 4. Exchange ICE candidates
90
+ puts "\n4. Exchanging ICE candidates..."
91
+ caller_ice_candidates.each do |candidate|
92
+ callee_pc.add_ice_candidate(candidate).await
93
+ rescue StandardError
94
+ nil
95
+ end
96
+ puts " Sent #{caller_ice_candidates.size} candidates from caller to callee"
97
+
98
+ callee_ice_candidates.each do |candidate|
99
+ caller_pc.add_ice_candidate(candidate).await
100
+ rescue StandardError
101
+ nil
102
+ end
103
+ puts " Sent #{callee_ice_candidates.size} candidates from callee to caller"
104
+
105
+ # Check status
106
+ puts "\n=== Connection Status ==="
107
+ puts "Caller signaling state: #{caller_pc.signaling_state}"
108
+ puts "Callee signaling state: #{callee_pc.signaling_state}"
109
+ puts "Data channel state: #{data_channel.ready_state}"
110
+
111
+ # Simulate DTMF (audio track simulation)
112
+ puts "\n=== DTMF Simulation ==="
113
+ audio_track = WebRTC::MediaStreamTrack.new(kind: :audio, label: 'virtual-audio')
114
+ sender = WebRTC::RTCRtpSender.new(track: audio_track)
115
+
116
+ if sender.dtmf
117
+ dtmf = sender.dtmf
118
+ puts "DTMF available: #{dtmf.can_insert_dtmf?}"
119
+
120
+ if dtmf.can_insert_dtmf?
121
+ dtmf.on_tone_change do |event|
122
+ puts " Playing tone: #{event.tone}"
123
+ end
124
+
125
+ puts 'Sending DTMF tones: 123#'
126
+ dtmf.insert_dtmf('123#', duration: 100, inter_tone_gap: 70)
127
+ end
128
+ end
129
+
130
+ # Get statistics
131
+ puts "\n=== Connection Statistics ==="
132
+ stats = caller_pc.get_stats.await
133
+ puts "Stats entries: #{stats.size}"
134
+ stats.each do |id, stat|
135
+ case stat
136
+ when WebRTC::RTCPeerConnectionStats
137
+ puts ' PeerConnection Stats:'
138
+ puts " Data channels opened: #{stat.data_channels_opened}"
139
+ when WebRTC::RTCTransportStats
140
+ puts ' Transport Stats:'
141
+ puts " Bytes sent: #{stat.bytes_sent}"
142
+ end
143
+ end
144
+
145
+ # Cleanup
146
+ puts "\n=== Cleanup ==="
147
+ data_channel.destroy
148
+ caller_pc.close
149
+ callee_pc.close
150
+ WebRTC.cleanup
151
+
152
+ puts "\nExample completed!"
@@ -0,0 +1,84 @@
1
+ cmake_minimum_required(VERSION 3.14)
2
+ project(webrtc_ruby C)
3
+
4
+ set(CMAKE_C_STANDARD 11)
5
+ set(CMAKE_C_STANDARD_REQUIRED ON)
6
+
7
+ if(NOT CMAKE_BUILD_TYPE)
8
+ set(CMAKE_BUILD_TYPE Release)
9
+ endif()
10
+
11
+ set(CMAKE_C_FLAGS_DEBUG "-g -O0")
12
+ set(CMAKE_C_FLAGS_RELEASE "-O3")
13
+
14
+ add_compile_definitions(WEBRTC_RUBY_BUILDING)
15
+
16
+ if(APPLE)
17
+ set(CMAKE_C_VISIBILITY_PRESET hidden)
18
+ endif()
19
+
20
+ # Find libdatachannel
21
+ set(LIBDATACHANNEL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../vendor/libdatachannel")
22
+
23
+ if(EXISTS "${LIBDATACHANNEL_DIR}/build/libdatachannel.dylib" OR
24
+ EXISTS "${LIBDATACHANNEL_DIR}/build/libdatachannel.so")
25
+ message(STATUS "Found libdatachannel in vendor directory")
26
+ set(DATACHANNEL_INCLUDE_DIR "${LIBDATACHANNEL_DIR}/include")
27
+ set(DATACHANNEL_LIBRARY_DIR "${LIBDATACHANNEL_DIR}/build")
28
+
29
+ if(APPLE)
30
+ set(DATACHANNEL_LIBRARY "${DATACHANNEL_LIBRARY_DIR}/libdatachannel.dylib")
31
+ else()
32
+ set(DATACHANNEL_LIBRARY "${DATACHANNEL_LIBRARY_DIR}/libdatachannel.so")
33
+ endif()
34
+ else()
35
+ # Try pkg-config
36
+ find_package(PkgConfig QUIET)
37
+ if(PkgConfig_FOUND)
38
+ pkg_check_modules(DATACHANNEL libdatachannel)
39
+ endif()
40
+
41
+ if(NOT DATACHANNEL_FOUND)
42
+ message(FATAL_ERROR "libdatachannel not found. Build it first.")
43
+ endif()
44
+ endif()
45
+
46
+ add_library(webrtc_ruby SHARED
47
+ webrtc_ruby.c
48
+ )
49
+
50
+ target_include_directories(webrtc_ruby PUBLIC
51
+ ${CMAKE_CURRENT_SOURCE_DIR}
52
+ ${DATACHANNEL_INCLUDE_DIR}
53
+ )
54
+
55
+ target_link_directories(webrtc_ruby PRIVATE
56
+ ${DATACHANNEL_LIBRARY_DIR}
57
+ )
58
+
59
+ target_link_libraries(webrtc_ruby PRIVATE
60
+ ${DATACHANNEL_LIBRARY}
61
+ )
62
+
63
+ if(APPLE)
64
+ set_target_properties(webrtc_ruby PROPERTIES
65
+ SUFFIX ".dylib"
66
+ INSTALL_RPATH "@loader_path"
67
+ BUILD_WITH_INSTALL_RPATH TRUE
68
+ )
69
+ elseif(WIN32)
70
+ set_target_properties(webrtc_ruby PROPERTIES
71
+ SUFFIX ".dll"
72
+ )
73
+ else()
74
+ set_target_properties(webrtc_ruby PROPERTIES
75
+ SUFFIX ".so"
76
+ INSTALL_RPATH "$ORIGIN"
77
+ BUILD_WITH_INSTALL_RPATH TRUE
78
+ )
79
+ endif()
80
+
81
+ install(TARGETS webrtc_ruby
82
+ LIBRARY DESTINATION lib
83
+ RUNTIME DESTINATION bin
84
+ )
@@ -0,0 +1,31 @@
1
+ CC = cc
2
+ CFLAGS = -Wall -Wextra -fPIC -O2 -DWEBRTC_RUBY_BUILDING
3
+ LDFLAGS = -shared
4
+
5
+ UNAME_S := $(shell uname -s)
6
+ ifeq ($(UNAME_S),Darwin)
7
+ TARGET = libwebrtc_ruby.dylib
8
+ LDFLAGS += -dynamiclib -install_name @rpath/$(TARGET)
9
+ else ifeq ($(OS),Windows_NT)
10
+ TARGET = webrtc_ruby.dll
11
+ else
12
+ TARGET = libwebrtc_ruby.so
13
+ endif
14
+
15
+ SOURCES = webrtc_ruby.c
16
+ OBJECTS = $(SOURCES:.c=.o)
17
+
18
+ BUILD_DIR = build
19
+
20
+ all: $(BUILD_DIR)/$(TARGET)
21
+
22
+ $(BUILD_DIR):
23
+ mkdir -p $(BUILD_DIR)
24
+
25
+ $(BUILD_DIR)/$(TARGET): $(SOURCES) | $(BUILD_DIR)
26
+ $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(SOURCES)
27
+
28
+ clean:
29
+ rm -rf $(BUILD_DIR)
30
+
31
+ .PHONY: all clean