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.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCIceCandidate
5
+ attr_reader :candidate, :sdp_mid, :sdp_m_line_index, :username_fragment
6
+
7
+ def initialize(init = {})
8
+ @candidate = init[:candidate] || ''
9
+ @sdp_mid = init[:sdp_mid] || init[:sdpMid]
10
+ @sdp_m_line_index = init[:sdp_m_line_index] || init[:sdpMLineIndex]
11
+ @username_fragment = init[:username_fragment] || init[:usernameFragment]
12
+ @ptr = nil
13
+ end
14
+
15
+ def self.from_ptr(ptr)
16
+ return nil if ptr.nil? || ptr.null?
17
+
18
+ candidate_str = FFI.webrtc_ice_candidate_get_candidate(ptr)
19
+ sdp_mid_str = FFI.webrtc_ice_candidate_get_sdp_mid(ptr)
20
+ sdp_m_line_index = FFI.webrtc_ice_candidate_get_sdp_mline_index(ptr)
21
+
22
+ ice = new(
23
+ candidate: candidate_str,
24
+ sdp_mid: sdp_mid_str,
25
+ sdp_m_line_index: sdp_m_line_index
26
+ )
27
+ ice.instance_variable_set(:@ptr, ptr)
28
+ ice
29
+ end
30
+
31
+ def to_ptr
32
+ return @ptr if @ptr && !@ptr.null?
33
+
34
+ error = FFI::Error.new
35
+ @ptr = FFI.webrtc_ice_candidate_create(
36
+ candidate,
37
+ sdp_mid,
38
+ sdp_m_line_index || 0,
39
+ error
40
+ )
41
+ raise OperationError, error[:message] if error[:code] != 0
42
+
43
+ @ptr
44
+ end
45
+
46
+ def to_h
47
+ {
48
+ candidate: candidate,
49
+ sdpMid: sdp_mid,
50
+ sdpMLineIndex: sdp_m_line_index,
51
+ usernameFragment: username_fragment
52
+ }
53
+ end
54
+
55
+ def release
56
+ return unless @ptr && !@ptr.null?
57
+
58
+ FFI.webrtc_ice_candidate_destroy(@ptr)
59
+ @ptr = nil
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCIceTransport
5
+ STATES = %i[new checking connected completed failed disconnected closed].freeze
6
+ GATHERING_STATES = %i[new gathering complete].freeze
7
+ ROLES = %i[controlling controlled unknown].freeze
8
+
9
+ attr_reader :role, :component, :state, :gathering_state
10
+
11
+ def initialize(options = {})
12
+ @state = :new
13
+ @gathering_state = :new
14
+ @role = options[:role] || :unknown
15
+ @component = options[:component] || :rtp
16
+ @local_candidates = []
17
+ @remote_candidates = []
18
+ @local_parameters = nil
19
+ @remote_parameters = nil
20
+ @callbacks = {}
21
+ @ptr = options[:ptr]
22
+ end
23
+
24
+ def get_local_candidates
25
+ @local_candidates.dup
26
+ end
27
+
28
+ def get_remote_candidates
29
+ @remote_candidates.dup
30
+ end
31
+
32
+ def get_local_parameters
33
+ @local_parameters
34
+ end
35
+
36
+ def get_remote_parameters
37
+ @remote_parameters
38
+ end
39
+
40
+ def get_selected_candidate_pair
41
+ return nil if @local_candidates.empty? || @remote_candidates.empty?
42
+
43
+ RTCIceCandidatePair.new(
44
+ local: @local_candidates.first,
45
+ remote: @remote_candidates.first
46
+ )
47
+ end
48
+
49
+ def on_state_change(&block)
50
+ @callbacks[:state_change] = block
51
+ end
52
+
53
+ def on_gathering_state_change(&block)
54
+ @callbacks[:gathering_state_change] = block
55
+ end
56
+
57
+ def on_selected_candidate_pair_change(&block)
58
+ @callbacks[:selected_candidate_pair_change] = block
59
+ end
60
+
61
+ private
62
+
63
+ def set_state(new_state)
64
+ return if @state == new_state
65
+
66
+ @state = new_state
67
+ @callbacks[:state_change]&.call
68
+ end
69
+ end
70
+
71
+ class RTCIceCandidatePair
72
+ attr_reader :local, :remote
73
+
74
+ def initialize(local:, remote:)
75
+ @local = local
76
+ @remote = remote
77
+ end
78
+ end
79
+
80
+ class RTCIceParameters
81
+ attr_reader :username_fragment, :password
82
+
83
+ def initialize(username_fragment:, password:)
84
+ @username_fragment = username_fragment
85
+ @password = password
86
+ end
87
+
88
+ def to_h
89
+ {
90
+ usernameFragment: @username_fragment,
91
+ password: @password
92
+ }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class MediaStream
5
+ attr_reader :id, :active
6
+
7
+ def initialize(tracks = [])
8
+ @id = generate_id
9
+ @tracks = {}
10
+ @callbacks = {}
11
+
12
+ tracks.each { |track| add_track(track) }
13
+ end
14
+
15
+ def active?
16
+ @tracks.values.any? { |track| track.ready_state == :live }
17
+ end
18
+
19
+ def get_audio_tracks
20
+ @tracks.values.select { |track| track.kind == :audio }
21
+ end
22
+
23
+ def get_video_tracks
24
+ @tracks.values.select { |track| track.kind == :video }
25
+ end
26
+
27
+ def get_tracks
28
+ @tracks.values
29
+ end
30
+
31
+ def get_track_by_id(id)
32
+ @tracks[id]
33
+ end
34
+
35
+ def add_track(track)
36
+ return if @tracks.key?(track.id)
37
+
38
+ @tracks[track.id] = track
39
+ @callbacks[:add_track]&.call(track)
40
+ end
41
+
42
+ def remove_track(track)
43
+ removed = @tracks.delete(track.id)
44
+ @callbacks[:remove_track]&.call(track) if removed
45
+ removed
46
+ end
47
+
48
+ def clone
49
+ cloned_tracks = @tracks.values.map(&:clone)
50
+ MediaStream.new(cloned_tracks)
51
+ end
52
+
53
+ def on_add_track(&block)
54
+ @callbacks[:add_track] = block
55
+ end
56
+
57
+ def on_remove_track(&block)
58
+ @callbacks[:remove_track] = block
59
+ end
60
+
61
+ private
62
+
63
+ def generate_id
64
+ "stream-#{SecureRandom.uuid}"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class MediaStreamTrack
5
+ KINDS = %i[audio video].freeze
6
+ READY_STATES = %i[live ended].freeze
7
+
8
+ attr_reader :id, :kind, :label, :ready_state
9
+ attr_accessor :enabled, :muted, :content_hint
10
+
11
+ def initialize(options = {})
12
+ @id = options[:id] || generate_id
13
+ @kind = options[:kind]&.to_sym
14
+ @label = options[:label] || ''
15
+ @enabled = options.fetch(:enabled, true)
16
+ @muted = options.fetch(:muted, false)
17
+ @ready_state = :live
18
+ @content_hint = options[:content_hint] || ''
19
+ @callbacks = {}
20
+ @ptr = options[:ptr]
21
+
22
+ validate!
23
+ end
24
+
25
+ def stop
26
+ @ready_state = :ended
27
+ @callbacks[:ended]&.call
28
+ end
29
+
30
+ def clone
31
+ MediaStreamTrack.new(
32
+ kind: kind,
33
+ label: label,
34
+ enabled: enabled,
35
+ muted: muted,
36
+ content_hint: content_hint
37
+ )
38
+ end
39
+
40
+ def get_constraints
41
+ {}
42
+ end
43
+
44
+ def get_capabilities
45
+ {}
46
+ end
47
+
48
+ def get_settings
49
+ {
50
+ device_id: '',
51
+ group_id: ''
52
+ }
53
+ end
54
+
55
+ def apply_constraints(_constraints = {})
56
+ Promise.resolve(nil)
57
+ end
58
+
59
+ def on_ended(&block)
60
+ @callbacks[:ended] = block
61
+ end
62
+
63
+ def on_mute(&block)
64
+ @callbacks[:mute] = block
65
+ end
66
+
67
+ def on_unmute(&block)
68
+ @callbacks[:unmute] = block
69
+ end
70
+
71
+ private
72
+
73
+ def generate_id
74
+ "track-#{SecureRandom.uuid}"
75
+ end
76
+
77
+ def validate!
78
+ return if kind.nil? || KINDS.include?(kind)
79
+
80
+ raise InvalidParameterError, "Invalid track kind: #{kind}"
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCPeerConnection
5
+ SIGNALING_STATES = %i[stable have_local_offer have_remote_offer
6
+ have_local_pranswer have_remote_pranswer closed].freeze
7
+ ICE_GATHERING_STATES = %i[new gathering complete].freeze
8
+ ICE_CONNECTION_STATES = %i[new checking connected completed failed
9
+ disconnected closed].freeze
10
+ CONNECTION_STATES = %i[new connecting connected disconnected failed closed].freeze
11
+
12
+ attr_reader :ptr, :local_description, :remote_description
13
+
14
+ def initialize(configuration = nil)
15
+ @configuration = configuration || {}
16
+ @callbacks = {}
17
+ @senders = []
18
+ @receivers = []
19
+ @transceivers = []
20
+ @ptr = create_native_peer_connection
21
+ end
22
+
23
+ def signaling_state
24
+ state_index = FFI.webrtc_peer_connection_get_signaling_state(@ptr)
25
+ SIGNALING_STATES[state_index] || :unknown
26
+ end
27
+
28
+ def ice_gathering_state
29
+ state_index = FFI.webrtc_peer_connection_get_ice_gathering_state(@ptr)
30
+ ICE_GATHERING_STATES[state_index] || :unknown
31
+ end
32
+
33
+ def ice_connection_state
34
+ state_index = FFI.webrtc_peer_connection_get_ice_connection_state(@ptr)
35
+ ICE_CONNECTION_STATES[state_index] || :unknown
36
+ end
37
+
38
+ def connection_state
39
+ state_index = FFI.webrtc_peer_connection_get_connection_state(@ptr)
40
+ CONNECTION_STATES[state_index] || :unknown
41
+ end
42
+
43
+ def close
44
+ return if @ptr.nil?
45
+
46
+ FFI.webrtc_peer_connection_destroy(@ptr)
47
+ @ptr = nil
48
+ end
49
+
50
+ def closed?
51
+ @ptr.nil?
52
+ end
53
+
54
+ def sctp_transport
55
+ @sctp_transport ||= RTCSctpTransport.new
56
+ end
57
+
58
+ def get_configuration
59
+ @configuration.dup
60
+ end
61
+
62
+ def set_configuration(configuration)
63
+ @configuration = configuration
64
+ end
65
+
66
+ def get_stats(selector = nil)
67
+ raise_if_closed!
68
+ Promise.new do
69
+ stats = {}
70
+
71
+ pc_stats = RTCPeerConnectionStats.new(
72
+ data_channels_opened: @data_channels_opened || 0,
73
+ data_channels_closed: @data_channels_closed || 0
74
+ )
75
+ stats[pc_stats.id] = pc_stats
76
+
77
+ transport_stats = RTCTransportStats.new(
78
+ bytes_sent: 0,
79
+ bytes_received: 0,
80
+ dtls_state: sctp_transport.transport.state
81
+ )
82
+ stats[transport_stats.id] = transport_stats
83
+
84
+ RTCStatsReport.new(stats)
85
+ end
86
+ end
87
+
88
+ def create_offer(options = {})
89
+ raise_if_closed!
90
+ Promise.new do
91
+ sdp_ptr = ::FFI::MemoryPointer.new(:pointer)
92
+ error = FFI::Error.new
93
+ result = FFI.webrtc_peer_connection_create_offer(@ptr, sdp_ptr, error)
94
+ raise OperationError, error[:message] if result != 0
95
+
96
+ RTCSessionDescription.from_ptr(sdp_ptr.read_pointer)
97
+ end
98
+ end
99
+
100
+ def create_answer(options = {})
101
+ raise_if_closed!
102
+ Promise.new do
103
+ sdp_ptr = ::FFI::MemoryPointer.new(:pointer)
104
+ error = FFI::Error.new
105
+ result = FFI.webrtc_peer_connection_create_answer(@ptr, sdp_ptr, error)
106
+ raise OperationError, error[:message] if result != 0
107
+
108
+ RTCSessionDescription.from_ptr(sdp_ptr.read_pointer)
109
+ end
110
+ end
111
+
112
+ def set_local_description(description)
113
+ raise_if_closed!
114
+ Promise.new do
115
+ desc = normalize_description(description)
116
+ error = FFI::Error.new
117
+ result = FFI.webrtc_peer_connection_set_local_description(@ptr, desc.to_ptr, error)
118
+ raise OperationError, error[:message] if result != 0
119
+
120
+ @local_description = desc
121
+ nil
122
+ end
123
+ end
124
+
125
+ def set_remote_description(description)
126
+ raise_if_closed!
127
+ Promise.new do
128
+ desc = normalize_description(description)
129
+ error = FFI::Error.new
130
+ result = FFI.webrtc_peer_connection_set_remote_description(@ptr, desc.to_ptr, error)
131
+ raise OperationError, error[:message] if result != 0
132
+
133
+ @remote_description = desc
134
+ nil
135
+ end
136
+ end
137
+
138
+ def add_ice_candidate(candidate)
139
+ raise_if_closed!
140
+ Promise.new do
141
+ ice = normalize_ice_candidate(candidate)
142
+ error = FFI::Error.new
143
+ result = FFI.webrtc_peer_connection_add_ice_candidate(@ptr, ice.to_ptr, error)
144
+ raise OperationError, error[:message] if result != 0
145
+
146
+ nil
147
+ end
148
+ end
149
+
150
+ def create_data_channel(label, options = {})
151
+ raise_if_closed!
152
+
153
+ ordered = options.fetch(:ordered, true)
154
+ max_retransmits = options[:max_retransmits] || -1
155
+ max_packet_life_time = options[:max_packet_life_time] || -1
156
+ protocol = options[:protocol] || ''
157
+ negotiated = options.fetch(:negotiated, false)
158
+ id = options[:id] || -1
159
+
160
+ error = FFI::Error.new
161
+ dc_ptr = FFI.webrtc_peer_connection_create_data_channel(
162
+ @ptr,
163
+ label,
164
+ ordered,
165
+ max_retransmits,
166
+ max_packet_life_time,
167
+ protocol,
168
+ negotiated,
169
+ id,
170
+ error
171
+ )
172
+
173
+ raise OperationError, error[:message] if dc_ptr.nil? || dc_ptr.null?
174
+
175
+ RTCDataChannel.new(dc_ptr, options.merge(label: label))
176
+ end
177
+
178
+ def on_ice_candidate(&block)
179
+ @callbacks[:ice_candidate] = block
180
+ setup_ice_candidate_callback
181
+ end
182
+
183
+ def on_connection_state_change(&block)
184
+ @callbacks[:connection_state_change] = block
185
+ setup_connection_state_callback
186
+ end
187
+
188
+ def on_signaling_state_change(&block)
189
+ @callbacks[:signaling_state_change] = block
190
+ end
191
+
192
+ def on_ice_gathering_state_change(&block)
193
+ @callbacks[:ice_gathering_state_change] = block
194
+ end
195
+
196
+ def on_ice_connection_state_change(&block)
197
+ @callbacks[:ice_connection_state_change] = block
198
+ end
199
+
200
+ def on_data_channel(&block)
201
+ @callbacks[:data_channel] = block
202
+ setup_data_channel_callback
203
+ end
204
+
205
+ def on_negotiation_needed(&block)
206
+ @callbacks[:negotiation_needed] = block
207
+ end
208
+
209
+ def on_track(&block)
210
+ @callbacks[:track] = block
211
+ end
212
+
213
+ def add_track(track, *streams)
214
+ raise_if_closed!
215
+ raise InvalidParameterError, 'track is required' unless track
216
+
217
+ sender = RTCRtpSender.new(track: track)
218
+ receiver = RTCRtpReceiver.new(track: MediaStreamTrack.new(kind: track.kind))
219
+ transceiver = RTCRtpTransceiver.new(
220
+ sender: sender,
221
+ receiver: receiver,
222
+ direction: :sendrecv
223
+ )
224
+
225
+ @senders << sender
226
+ @receivers << receiver
227
+ @transceivers << transceiver
228
+
229
+ @callbacks[:negotiation_needed]&.call
230
+
231
+ sender
232
+ end
233
+
234
+ def remove_track(sender)
235
+ raise_if_closed!
236
+ return unless @senders.include?(sender)
237
+
238
+ sender.replace_track(nil)
239
+ @callbacks[:negotiation_needed]&.call
240
+ end
241
+
242
+ def get_senders
243
+ @senders.dup
244
+ end
245
+
246
+ def get_receivers
247
+ @receivers.dup
248
+ end
249
+
250
+ def get_transceivers
251
+ @transceivers.dup
252
+ end
253
+
254
+ def add_transceiver(track_or_kind, init = {})
255
+ raise_if_closed!
256
+
257
+ track = if track_or_kind.is_a?(MediaStreamTrack)
258
+ track_or_kind
259
+ else
260
+ MediaStreamTrack.new(kind: track_or_kind.to_sym)
261
+ end
262
+
263
+ sender = RTCRtpSender.new(track: track)
264
+ receiver = RTCRtpReceiver.new(track: MediaStreamTrack.new(kind: track.kind))
265
+ transceiver = RTCRtpTransceiver.new(
266
+ sender: sender,
267
+ receiver: receiver,
268
+ direction: init[:direction] || :sendrecv
269
+ )
270
+
271
+ @senders << sender
272
+ @receivers << receiver
273
+ @transceivers << transceiver
274
+
275
+ @callbacks[:negotiation_needed]&.call
276
+
277
+ transceiver
278
+ end
279
+
280
+ private
281
+
282
+ def raise_if_closed!
283
+ raise InvalidStateError, 'PeerConnection is closed' if closed?
284
+ end
285
+
286
+ def normalize_description(desc)
287
+ return desc if desc.is_a?(RTCSessionDescription)
288
+
289
+ RTCSessionDescription.new(desc)
290
+ end
291
+
292
+ def normalize_ice_candidate(candidate)
293
+ return candidate if candidate.is_a?(RTCIceCandidate)
294
+
295
+ RTCIceCandidate.new(candidate)
296
+ end
297
+
298
+ def setup_ice_candidate_callback
299
+ callback = proc do |candidate_str, sdp_mid, sdp_mline_index, _user_data|
300
+ ice = RTCIceCandidate.new(
301
+ candidate: candidate_str,
302
+ sdp_mid: sdp_mid,
303
+ sdp_m_line_index: sdp_mline_index
304
+ )
305
+ @callbacks[:ice_candidate]&.call(ice)
306
+ end
307
+ @callbacks[:ice_candidate_proc] = callback
308
+ FFI.webrtc_peer_connection_on_ice_candidate(@ptr, callback, ::FFI::Pointer::NULL)
309
+ end
310
+
311
+ def setup_connection_state_callback
312
+ callback = proc do |state, _user_data|
313
+ @callbacks[:connection_state_change]&.call(CONNECTION_STATES[state])
314
+ end
315
+ @callbacks[:connection_state_proc] = callback
316
+ FFI.webrtc_peer_connection_on_connection_state_change(@ptr, callback, ::FFI::Pointer::NULL)
317
+ end
318
+
319
+ def setup_data_channel_callback
320
+ callback = proc do |dc_ptr, _user_data|
321
+ @callbacks[:data_channel]&.call(dc_ptr)
322
+ end
323
+ @callbacks[:data_channel_proc] = callback
324
+ FFI.webrtc_peer_connection_on_data_channel(@ptr, callback, ::FFI::Pointer::NULL)
325
+ end
326
+
327
+ def create_native_peer_connection
328
+ error = FFI::Error.new
329
+ config = build_ffi_configuration
330
+
331
+ ptr = FFI.webrtc_peer_connection_create(config, error)
332
+ raise OperationError, error[:message] if error[:code] != 0
333
+
334
+ ptr
335
+ end
336
+
337
+ def build_ffi_configuration
338
+ config = FFI::Configuration.new
339
+ config[:ice_servers] = ::FFI::Pointer::NULL
340
+ config[:ice_servers_count] = 0
341
+ config[:ice_transport_policy] = ::FFI::Pointer::NULL
342
+ config[:bundle_policy] = ::FFI::Pointer::NULL
343
+ config
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent-ruby'
4
+
5
+ module WebRTC
6
+ class Promise
7
+ def initialize(&block)
8
+ @future = Concurrent::Promises.future(&block)
9
+ end
10
+
11
+ def self.resolve(value)
12
+ new { value }
13
+ end
14
+
15
+ def self.reject(error)
16
+ promise = allocate
17
+ promise.instance_variable_set(:@future, Concurrent::Promises.rejected_future(error))
18
+ promise
19
+ end
20
+
21
+ def then(&block)
22
+ new_promise = Promise.allocate
23
+ new_promise.instance_variable_set(:@future, @future.then(&block))
24
+ new_promise
25
+ end
26
+
27
+ def catch(&block)
28
+ new_promise = Promise.allocate
29
+ new_promise.instance_variable_set(:@future, @future.rescue(&block))
30
+ new_promise
31
+ end
32
+
33
+ def finally(&block)
34
+ new_promise = Promise.allocate
35
+ new_promise.instance_variable_set(:@future, @future.on_resolution { block.call })
36
+ new_promise
37
+ end
38
+
39
+ def await(timeout = nil)
40
+ if timeout
41
+ @future.value!(timeout)
42
+ else
43
+ @future.value!
44
+ end
45
+ end
46
+
47
+ def pending?
48
+ @future.pending?
49
+ end
50
+
51
+ def fulfilled?
52
+ @future.fulfilled?
53
+ end
54
+
55
+ def rejected?
56
+ @future.rejected?
57
+ end
58
+ end
59
+ end