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,577 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCPeerConnection
5
+ SIGNALING_STATES = %i[
6
+ stable have_local_offer have_remote_offer have_local_pranswer have_remote_pranswer closed
7
+ ].freeze
8
+ ICE_GATHERING_STATES = %i[new gathering complete].freeze
9
+ ICE_CONNECTION_STATES = %i[new checking connected completed failed disconnected closed].freeze
10
+ CONNECTION_STATES = %i[new connecting connected disconnected failed closed].freeze
11
+
12
+ DIRECTION_TO_NATIVE = {
13
+ sendonly: 1,
14
+ recvonly: 2,
15
+ sendrecv: 3,
16
+ inactive: 4
17
+ }.freeze
18
+
19
+ NATIVE_TO_DIRECTION = {
20
+ 1 => :sendonly,
21
+ 2 => :recvonly,
22
+ 3 => :sendrecv,
23
+ 4 => :inactive
24
+ }.freeze
25
+
26
+ attr_reader :ptr, :local_description, :remote_description
27
+
28
+ def initialize(configuration = nil, factory: nil)
29
+ @factory = factory
30
+ @configuration = configuration || {}
31
+ @callbacks = {}
32
+ @senders = []
33
+ @receivers = []
34
+ @transceivers = []
35
+ @data_channels = []
36
+ @data_channels_opened = 0
37
+ @data_channels_closed = 0
38
+ @ptr = create_native_peer_connection
39
+ end
40
+
41
+ def signaling_state
42
+ state_index = FFI.webrtc_peer_connection_get_signaling_state(@ptr)
43
+ SIGNALING_STATES[state_index] || :unknown
44
+ end
45
+
46
+ def ice_gathering_state
47
+ state_index = FFI.webrtc_peer_connection_get_ice_gathering_state(@ptr)
48
+ ICE_GATHERING_STATES[state_index] || :unknown
49
+ end
50
+
51
+ def ice_connection_state
52
+ state_index = FFI.webrtc_peer_connection_get_ice_connection_state(@ptr)
53
+ ICE_CONNECTION_STATES[state_index] || :unknown
54
+ end
55
+
56
+ def connection_state
57
+ state_index = FFI.webrtc_peer_connection_get_connection_state(@ptr)
58
+ CONNECTION_STATES[state_index] || :unknown
59
+ end
60
+
61
+ def close
62
+ return if @ptr.nil?
63
+
64
+ FFI.webrtc_peer_connection_destroy(@ptr)
65
+ @ptr = nil
66
+ end
67
+
68
+ def closed?
69
+ @ptr.nil?
70
+ end
71
+
72
+ def sctp_transport
73
+ @sctp_transport ||= RTCSctpTransport.new
74
+ end
75
+
76
+ def get_configuration
77
+ @configuration.dup
78
+ end
79
+
80
+ def set_configuration(configuration)
81
+ @configuration = configuration || {}
82
+ end
83
+
84
+ def get_stats(selector = nil, callback: nil)
85
+ raise_if_closed!
86
+ Promise.new do
87
+ stats = {}
88
+ total_bytes_sent = 0
89
+ total_bytes_received = 0
90
+
91
+ pc_stats = RTCPeerConnectionStats.new(
92
+ data_channels_opened: @data_channels_opened,
93
+ data_channels_closed: @data_channels_closed
94
+ )
95
+ stats[pc_stats.id] = pc_stats
96
+
97
+ @data_channels.each_with_index do |channel, index|
98
+ snapshot = channel.stats_snapshot
99
+ total_bytes_sent += snapshot[:bytes_sent]
100
+ total_bytes_received += snapshot[:bytes_received]
101
+
102
+ data_channel_stats = RTCDataChannelStats.new(
103
+ id: "data-channel-#{index}-#{channel.id || index}",
104
+ label: channel.label,
105
+ protocol: channel.protocol,
106
+ data_channel_identifier: channel.id,
107
+ state: snapshot[:state],
108
+ messages_sent: snapshot[:messages_sent],
109
+ bytes_sent: snapshot[:bytes_sent],
110
+ messages_received: snapshot[:messages_received],
111
+ bytes_received: snapshot[:bytes_received]
112
+ )
113
+ stats[data_channel_stats.id] = data_channel_stats
114
+ end
115
+
116
+ transport_stats = RTCTransportStats.new(
117
+ bytes_sent: total_bytes_sent,
118
+ bytes_received: total_bytes_received,
119
+ dtls_state: sctp_transport.transport.state
120
+ )
121
+ stats[transport_stats.id] = transport_stats
122
+
123
+ report = RTCStatsReport.new(stats)
124
+ stats_response = RTCStatsResponse.new(report)
125
+ callback&.on_stats_delivered(stats_response) if callback.respond_to?(:on_stats_delivered)
126
+ report
127
+ end
128
+ end
129
+
130
+ def create_offer(options = {}, observer: nil)
131
+ raise_if_closed!
132
+ observer ||= options.is_a?(Hash) ? options[:observer] : nil
133
+
134
+ Promise.new do
135
+ begin
136
+ sdp_ptr = ::FFI::MemoryPointer.new(:pointer)
137
+ error = FFI::Error.new
138
+ result = FFI.webrtc_peer_connection_create_offer(@ptr, sdp_ptr, error)
139
+ raise OperationError, error[:message] if result != 0
140
+
141
+ description = RTCSessionDescription.from_ptr(sdp_ptr.read_pointer)
142
+ observer&.on_success(description) if observer.respond_to?(:on_success)
143
+ description
144
+ rescue StandardError => e
145
+ observer&.on_failure(e) if observer.respond_to?(:on_failure)
146
+ raise e
147
+ end
148
+ end
149
+ end
150
+
151
+ def create_answer(options = {}, observer: nil)
152
+ raise_if_closed!
153
+ observer ||= options.is_a?(Hash) ? options[:observer] : nil
154
+
155
+ Promise.new do
156
+ begin
157
+ sdp_ptr = ::FFI::MemoryPointer.new(:pointer)
158
+ error = FFI::Error.new
159
+ result = FFI.webrtc_peer_connection_create_answer(@ptr, sdp_ptr, error)
160
+ raise OperationError, error[:message] if result != 0
161
+
162
+ description = RTCSessionDescription.from_ptr(sdp_ptr.read_pointer)
163
+ observer&.on_success(description) if observer.respond_to?(:on_success)
164
+ description
165
+ rescue StandardError => e
166
+ observer&.on_failure(e) if observer.respond_to?(:on_failure)
167
+ raise e
168
+ end
169
+ end
170
+ end
171
+
172
+ def set_local_description(description, observer: nil)
173
+ raise_if_closed!
174
+ Promise.new do
175
+ begin
176
+ desc = normalize_description(description)
177
+ error = FFI::Error.new
178
+ result = FFI.webrtc_peer_connection_set_local_description(@ptr, desc.to_ptr, error)
179
+ raise OperationError, error[:message] if result != 0
180
+
181
+ @local_description = desc
182
+ observer&.on_success(nil) if observer.respond_to?(:on_success)
183
+ nil
184
+ rescue StandardError => e
185
+ observer&.on_failure(e) if observer.respond_to?(:on_failure)
186
+ raise e
187
+ end
188
+ end
189
+ end
190
+
191
+ def set_remote_description(description, observer: nil)
192
+ raise_if_closed!
193
+ Promise.new do
194
+ begin
195
+ desc = normalize_description(description)
196
+ error = FFI::Error.new
197
+ result = FFI.webrtc_peer_connection_set_remote_description(@ptr, desc.to_ptr, error)
198
+ raise OperationError, error[:message] if result != 0
199
+
200
+ @remote_description = desc
201
+ observer&.on_success(nil) if observer.respond_to?(:on_success)
202
+ nil
203
+ rescue StandardError => e
204
+ observer&.on_failure(e) if observer.respond_to?(:on_failure)
205
+ raise e
206
+ end
207
+ end
208
+ end
209
+
210
+ def add_ice_candidate(candidate, observer: nil)
211
+ raise_if_closed!
212
+ Promise.new do
213
+ begin
214
+ ice = normalize_ice_candidate(candidate)
215
+ error = FFI::Error.new
216
+ result = FFI.webrtc_peer_connection_add_ice_candidate(@ptr, ice.to_ptr, error)
217
+ raise OperationError, error[:message] if result != 0
218
+
219
+ observer&.on_success(nil) if observer.respond_to?(:on_success)
220
+ nil
221
+ rescue StandardError => e
222
+ observer&.on_failure(e) if observer.respond_to?(:on_failure)
223
+ raise e
224
+ end
225
+ end
226
+ end
227
+
228
+ def create_data_channel(label, options = {})
229
+ raise_if_closed!
230
+
231
+ init = options.is_a?(DataChannelInit) ? options.to_h : options
232
+ ordered = init.fetch(:ordered, true)
233
+ max_retransmits = init[:max_retransmits] || -1
234
+ max_packet_life_time = init[:max_packet_life_time] || -1
235
+ protocol = init[:protocol] || ''
236
+ negotiated = init.fetch(:negotiated, false)
237
+ id = init[:id] || -1
238
+
239
+ error = FFI::Error.new
240
+ dc_ptr = FFI.webrtc_peer_connection_create_data_channel(
241
+ @ptr,
242
+ label,
243
+ ordered,
244
+ max_retransmits,
245
+ max_packet_life_time,
246
+ protocol,
247
+ negotiated,
248
+ id,
249
+ error
250
+ )
251
+
252
+ raise OperationError, error[:message] if dc_ptr.nil? || dc_ptr.null?
253
+
254
+ channel = RTCDataChannel.new(dc_ptr, init.merge(label: label))
255
+ track_data_channel(channel)
256
+ channel
257
+ end
258
+
259
+ def on_ice_candidate(&block)
260
+ @callbacks[:ice_candidate] = block
261
+ setup_ice_candidate_callback
262
+ end
263
+
264
+ def on_connection_state_change(&block)
265
+ @callbacks[:connection_state_change] = block
266
+ setup_connection_state_callback
267
+ end
268
+
269
+ def on_signaling_state_change(&block)
270
+ @callbacks[:signaling_state_change] = block
271
+ setup_signaling_state_callback
272
+ end
273
+
274
+ def on_ice_gathering_state_change(&block)
275
+ @callbacks[:ice_gathering_state_change] = block
276
+ setup_ice_gathering_state_callback
277
+ end
278
+
279
+ def on_ice_connection_state_change(&block)
280
+ @callbacks[:ice_connection_state_change] = block
281
+ setup_ice_connection_state_callback
282
+ end
283
+
284
+ def on_data_channel(&block)
285
+ @callbacks[:data_channel] = block
286
+ setup_data_channel_callback
287
+ end
288
+
289
+ def on_negotiation_needed(&block)
290
+ @callbacks[:negotiation_needed] = block
291
+ end
292
+
293
+ def on_track(&block)
294
+ @callbacks[:track] = block
295
+ setup_track_callback
296
+ end
297
+
298
+ def add_track(track, *streams)
299
+ raise_if_closed!
300
+ raise InvalidParameterError, 'track is required' unless track
301
+
302
+ native_track_id = try_add_native_track(track.kind, :sendrecv)
303
+
304
+ sender = RTCRtpSender.new(track: track, native_track_id: native_track_id)
305
+ receiver = RTCRtpReceiver.new(track: MediaStreamTrack.new(kind: track.kind), native_track_id: native_track_id)
306
+ transceiver = RTCRtpTransceiver.new(
307
+ sender: sender,
308
+ receiver: receiver,
309
+ direction: :sendrecv,
310
+ native_track_id: native_track_id
311
+ )
312
+
313
+ @senders << sender
314
+ @receivers << receiver
315
+ @transceivers << transceiver
316
+ @callbacks[:negotiation_needed]&.call
317
+
318
+ sender
319
+ end
320
+
321
+ def remove_track(sender)
322
+ raise_if_closed!
323
+ return unless @senders.include?(sender)
324
+
325
+ if sender.native_track_id
326
+ error = FFI::Error.new
327
+ FFI.webrtc_peer_connection_remove_track(@ptr, sender.native_track_id, error)
328
+ end
329
+
330
+ sender.replace_track(nil)
331
+ @callbacks[:negotiation_needed]&.call
332
+ end
333
+
334
+ def get_senders
335
+ @senders.dup
336
+ end
337
+
338
+ def get_receivers
339
+ @receivers.dup
340
+ end
341
+
342
+ def get_transceivers
343
+ @transceivers.dup
344
+ end
345
+
346
+ def add_transceiver(track_or_kind, init = {})
347
+ raise_if_closed!
348
+
349
+ track = if track_or_kind.is_a?(MediaStreamTrack)
350
+ track_or_kind
351
+ else
352
+ MediaStreamTrack.new(kind: track_or_kind.to_sym)
353
+ end
354
+
355
+ direction = (init[:direction] || :sendrecv).to_sym
356
+ native_track_id = try_add_native_track(track.kind, direction)
357
+
358
+ sender = RTCRtpSender.new(track: track, native_track_id: native_track_id)
359
+ receiver = RTCRtpReceiver.new(track: MediaStreamTrack.new(kind: track.kind), native_track_id: native_track_id)
360
+ transceiver = RTCRtpTransceiver.new(
361
+ sender: sender,
362
+ receiver: receiver,
363
+ direction: direction,
364
+ native_track_id: native_track_id
365
+ )
366
+
367
+ @senders << sender
368
+ @receivers << receiver
369
+ @transceivers << transceiver
370
+ @callbacks[:negotiation_needed]&.call
371
+
372
+ transceiver
373
+ end
374
+
375
+ private
376
+
377
+ def raise_if_closed!
378
+ raise InvalidStateError, 'PeerConnection is closed' if closed?
379
+ end
380
+
381
+ def normalize_description(desc)
382
+ return desc if desc.is_a?(RTCSessionDescription)
383
+ return RTCSessionDescription.new(desc.to_h) if desc.respond_to?(:to_h)
384
+
385
+ RTCSessionDescription.new(desc)
386
+ end
387
+
388
+ def normalize_ice_candidate(candidate)
389
+ return candidate if candidate.is_a?(RTCIceCandidate)
390
+ return RTCIceCandidate.new(candidate.to_h) if candidate.respond_to?(:to_h)
391
+
392
+ RTCIceCandidate.new(candidate)
393
+ end
394
+
395
+ def setup_ice_candidate_callback
396
+ callback = proc do |candidate_str, sdp_mid, sdp_mline_index, _user_data|
397
+ ice = RTCIceCandidate.new(
398
+ candidate: candidate_str,
399
+ sdp_mid: sdp_mid,
400
+ sdp_m_line_index: sdp_mline_index
401
+ )
402
+ @callbacks[:ice_candidate]&.call(ice)
403
+ end
404
+ @callbacks[:ice_candidate_proc] = callback
405
+ FFI.webrtc_peer_connection_on_ice_candidate(@ptr, callback, ::FFI::Pointer::NULL)
406
+ end
407
+
408
+ def setup_connection_state_callback
409
+ callback = proc do |state, _user_data|
410
+ @callbacks[:connection_state_change]&.call(CONNECTION_STATES[state] || :unknown)
411
+ end
412
+ @callbacks[:connection_state_proc] = callback
413
+ FFI.webrtc_peer_connection_on_connection_state_change(@ptr, callback, ::FFI::Pointer::NULL)
414
+ end
415
+
416
+ def setup_signaling_state_callback
417
+ callback = proc do |state, _user_data|
418
+ @callbacks[:signaling_state_change]&.call(SIGNALING_STATES[state] || :unknown)
419
+ end
420
+ @callbacks[:signaling_state_proc] = callback
421
+ FFI.webrtc_peer_connection_on_signaling_state_change(@ptr, callback, ::FFI::Pointer::NULL)
422
+ end
423
+
424
+ def setup_ice_gathering_state_callback
425
+ callback = proc do |state, _user_data|
426
+ @callbacks[:ice_gathering_state_change]&.call(ICE_GATHERING_STATES[state] || :unknown)
427
+ end
428
+ @callbacks[:ice_gathering_state_proc] = callback
429
+ FFI.webrtc_peer_connection_on_ice_gathering_state_change(@ptr, callback, ::FFI::Pointer::NULL)
430
+ end
431
+
432
+ def setup_ice_connection_state_callback
433
+ callback = proc do |state, _user_data|
434
+ @callbacks[:ice_connection_state_change]&.call(ICE_CONNECTION_STATES[state] || :unknown)
435
+ end
436
+ @callbacks[:ice_connection_state_proc] = callback
437
+ FFI.webrtc_peer_connection_on_ice_connection_state_change(@ptr, callback, ::FFI::Pointer::NULL)
438
+ end
439
+
440
+ def setup_data_channel_callback
441
+ callback = proc do |dc_ptr, _user_data|
442
+ channel = RTCDataChannel.new(dc_ptr)
443
+ track_data_channel(channel)
444
+ @callbacks[:data_channel]&.call(channel)
445
+ end
446
+ @callbacks[:data_channel_proc] = callback
447
+ FFI.webrtc_peer_connection_on_data_channel(@ptr, callback, ::FFI::Pointer::NULL)
448
+ end
449
+
450
+ def setup_track_callback
451
+ callback = proc do |track_id, _user_data|
452
+ direction_index = FFI.webrtc_track_get_direction(track_id)
453
+ direction = NATIVE_TO_DIRECTION[direction_index] || :sendrecv
454
+ kind = (FFI.webrtc_track_get_kind(track_id) || 'audio').to_sym
455
+ track = MediaStreamTrack.new(kind: kind)
456
+ sender = RTCRtpSender.new(track: nil, native_track_id: track_id)
457
+ receiver = RTCRtpReceiver.new(track: track, native_track_id: track_id)
458
+ transceiver = RTCRtpTransceiver.new(
459
+ sender: sender,
460
+ receiver: receiver,
461
+ direction: direction,
462
+ native_track_id: track_id
463
+ )
464
+ @transceivers << transceiver
465
+ @receivers << receiver
466
+ @callbacks[:track]&.call(RTCTrackEvent.new(track: track, receiver: receiver, transceiver: transceiver, streams: []))
467
+ end
468
+ @callbacks[:track_proc] = callback
469
+ FFI.webrtc_peer_connection_on_track(@ptr, callback, ::FFI::Pointer::NULL)
470
+ end
471
+
472
+ def track_data_channel(channel)
473
+ return if @data_channels.include?(channel)
474
+
475
+ @data_channels << channel
476
+ channel.add_internal_listener(:open) do
477
+ @data_channels_opened += 1
478
+ end
479
+ channel.add_internal_listener(:close) do
480
+ @data_channels_closed += 1
481
+ end
482
+ end
483
+
484
+ def try_add_native_track(kind, direction)
485
+ track_id_ptr = ::FFI::MemoryPointer.new(:int)
486
+ error = FFI::Error.new
487
+ native_direction = DIRECTION_TO_NATIVE[direction] || DIRECTION_TO_NATIVE[:sendrecv]
488
+
489
+ result = FFI.webrtc_peer_connection_add_track(
490
+ @ptr,
491
+ kind.to_s,
492
+ native_direction,
493
+ track_id_ptr,
494
+ error
495
+ )
496
+ return nil if result != 0
497
+
498
+ track_id_ptr.read_int
499
+ end
500
+
501
+ def create_native_peer_connection
502
+ error = FFI::Error.new
503
+ config = build_ffi_configuration
504
+
505
+ ptr = FFI.webrtc_peer_connection_create(config, error)
506
+ raise OperationError, error[:message] if ptr.nil? || ptr.null?
507
+ raise OperationError, error[:message] if error[:code] != 0
508
+
509
+ ptr
510
+ end
511
+
512
+ def build_ffi_configuration
513
+ config_hash = normalize_configuration_hash
514
+ config = FFI::Configuration.new
515
+
516
+ ice_server_urls = build_ice_server_urls(config_hash)
517
+ if ice_server_urls.empty?
518
+ config[:ice_servers] = ::FFI::Pointer::NULL
519
+ config[:ice_servers_count] = 0
520
+ else
521
+ url_ptrs = ice_server_urls.map { |url| ::FFI::MemoryPointer.from_string(url) }
522
+ urls_array_ptr = ::FFI::MemoryPointer.new(:pointer, url_ptrs.length)
523
+ urls_array_ptr.write_array_of_pointer(url_ptrs)
524
+ @ffi_config_buffers = [urls_array_ptr, *url_ptrs]
525
+ config[:ice_servers] = urls_array_ptr
526
+ config[:ice_servers_count] = url_ptrs.length
527
+ end
528
+
529
+ config[:ice_transport_policy] = string_ptr(config_hash[:ice_transport_policy] || config_hash[:iceTransportPolicy])
530
+ config[:bundle_policy] = string_ptr(config_hash[:bundle_policy] || config_hash[:bundlePolicy])
531
+ config[:rtcp_mux_policy] = string_ptr(config_hash[:rtcp_mux_policy] || config_hash[:rtcpMuxPolicy])
532
+ config[:enable_ice_tcp] = !!(config_hash[:enable_ice_tcp] || config_hash[:enableIceTcp])
533
+ config[:enable_ice_udp_mux] = !!(config_hash[:enable_ice_udp_mux] || config_hash[:enableIceUdpMux])
534
+ config[:disable_auto_negotiation] = !!(config_hash[:disable_auto_negotiation] || config_hash[:disableAutoNegotiation])
535
+ config[:force_media_transport] = !!(config_hash[:force_media_transport] || config_hash[:forceMediaTransport])
536
+ config[:mtu] = (config_hash[:mtu] || 0).to_i
537
+ config[:max_message_size] = (config_hash[:max_message_size] || config_hash[:maxMessageSize] || 0).to_i
538
+
539
+ config
540
+ end
541
+
542
+ def normalize_configuration_hash
543
+ if @configuration.is_a?(RTCConfiguration)
544
+ @configuration.to_h
545
+ else
546
+ @configuration
547
+ end
548
+ end
549
+
550
+ def build_ice_server_urls(config_hash)
551
+ servers = config_hash[:ice_servers] || config_hash[:iceServers] || []
552
+
553
+ servers.flat_map do |entry|
554
+ server = entry.is_a?(RTCIceServer) ? entry.to_h : entry
555
+ urls = server[:urls] || server['urls'] || []
556
+ urls = [urls] if urls.is_a?(String)
557
+ username = server[:username] || server['username']
558
+ credential = server[:credential] || server['credential']
559
+
560
+ urls.map do |url|
561
+ next url unless username && credential && url.start_with?('turn:')
562
+
563
+ "turn:#{username}:#{credential}@#{url.sub('turn:', '')}"
564
+ end
565
+ end.compact
566
+ end
567
+
568
+ def string_ptr(value)
569
+ return ::FFI::Pointer::NULL if value.nil?
570
+
571
+ ptr = ::FFI::MemoryPointer.from_string(value.to_s)
572
+ @ffi_config_buffers ||= []
573
+ @ffi_config_buffers << ptr
574
+ ptr
575
+ end
576
+ end
577
+ 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
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCRtpReceiver
5
+ attr_reader :track, :transport, :native_track_id
6
+
7
+ def initialize(options = {})
8
+ @track = options[:track]
9
+ @transport = options[:transport]
10
+ @ptr = options[:ptr]
11
+ @native_track_id = options[:native_track_id]
12
+ @parameters = default_parameters
13
+ end
14
+
15
+ def self.get_capabilities(kind)
16
+ typed = get_typed_capabilities(kind)
17
+ {
18
+ codecs: typed.codecs.map do |codec|
19
+ { mime_type: codec.mime_type, clock_rate: codec.clock_rate, channels: codec.channels }
20
+ end,
21
+ header_extensions: typed.header_extensions.map(&:uri)
22
+ }
23
+ end
24
+
25
+ def self.get_typed_capabilities(kind)
26
+ RTCRtpCapabilities.new(
27
+ codecs: default_codecs(kind).map do |codec|
28
+ RTCRtpCodecCapability.new(
29
+ mime_type: codec[:mime_type],
30
+ clock_rate: codec[:clock_rate],
31
+ channels: codec[:channels]
32
+ )
33
+ end,
34
+ header_extensions: []
35
+ )
36
+ end
37
+
38
+ def self.default_codecs(kind)
39
+ RTCRtpSender.default_codecs(kind)
40
+ end
41
+
42
+ def get_parameters
43
+ @parameters.dup
44
+ end
45
+
46
+ def get_typed_parameters
47
+ RTCRtpReceiveParameters.new(
48
+ header_extensions: @parameters[:header_extensions],
49
+ rtcp: RTCRtcpParameters.new(
50
+ cname: @parameters.dig(:rtcp, :cname).to_s,
51
+ reduced_size: !!@parameters.dig(:rtcp, :reduced_size)
52
+ ),
53
+ codecs: @parameters[:codecs]
54
+ )
55
+ end
56
+
57
+ def get_contributing_sources
58
+ []
59
+ end
60
+
61
+ def get_synchronization_sources
62
+ []
63
+ end
64
+
65
+ def get_stats
66
+ Promise.resolve({})
67
+ end
68
+
69
+ private
70
+
71
+ def default_parameters
72
+ {
73
+ header_extensions: [],
74
+ rtcp: { cname: '', reduced_size: false },
75
+ codecs: []
76
+ }
77
+ end
78
+ end
79
+ end