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,212 @@
1
+ #ifndef WEBRTC_RUBY_H
2
+ #define WEBRTC_RUBY_H
3
+
4
+ #include <stdint.h>
5
+ #include <stdbool.h>
6
+ #include <stddef.h>
7
+
8
+ #ifdef __cplusplus
9
+ extern "C" {
10
+ #endif
11
+
12
+ #ifdef _WIN32
13
+ #ifdef WEBRTC_RUBY_BUILDING
14
+ #define WEBRTC_RUBY_API __declspec(dllexport)
15
+ #else
16
+ #define WEBRTC_RUBY_API __declspec(dllimport)
17
+ #endif
18
+ #else
19
+ #define WEBRTC_RUBY_API __attribute__((visibility("default")))
20
+ #endif
21
+
22
+ typedef struct webrtc_peer_connection* webrtc_peer_connection_t;
23
+ typedef struct webrtc_data_channel* webrtc_data_channel_t;
24
+ typedef struct webrtc_session_description* webrtc_session_description_t;
25
+ typedef struct webrtc_ice_candidate* webrtc_ice_candidate_t;
26
+
27
+ typedef struct {
28
+ int code;
29
+ const char* message;
30
+ } webrtc_error_t;
31
+
32
+ typedef struct {
33
+ const char** ice_servers;
34
+ size_t ice_servers_count;
35
+ const char* ice_transport_policy;
36
+ const char* bundle_policy;
37
+ const char* rtcp_mux_policy;
38
+ bool enable_ice_tcp;
39
+ bool enable_ice_udp_mux;
40
+ bool disable_auto_negotiation;
41
+ bool force_media_transport;
42
+ int mtu;
43
+ int max_message_size;
44
+ } webrtc_configuration_t;
45
+
46
+ typedef void (*webrtc_void_callback)(void* user_data);
47
+ typedef void (*webrtc_ice_candidate_callback)(const char* candidate,
48
+ const char* sdp_mid,
49
+ int sdp_mline_index,
50
+ void* user_data);
51
+ typedef void (*webrtc_data_channel_callback)(webrtc_data_channel_t dc,
52
+ void* user_data);
53
+ typedef void (*webrtc_message_callback)(const uint8_t* data,
54
+ size_t length,
55
+ bool is_binary,
56
+ void* user_data);
57
+ typedef void (*webrtc_state_change_callback)(int state, void* user_data);
58
+ typedef void (*webrtc_track_callback)(int track_id, void* user_data);
59
+
60
+ WEBRTC_RUBY_API int webrtc_init(void);
61
+ WEBRTC_RUBY_API void webrtc_cleanup(void);
62
+
63
+ WEBRTC_RUBY_API webrtc_peer_connection_t webrtc_peer_connection_create(
64
+ const webrtc_configuration_t* config,
65
+ webrtc_error_t* error);
66
+ WEBRTC_RUBY_API void webrtc_peer_connection_destroy(webrtc_peer_connection_t pc);
67
+
68
+ WEBRTC_RUBY_API int webrtc_peer_connection_create_offer(
69
+ webrtc_peer_connection_t pc,
70
+ webrtc_session_description_t* out_sdp,
71
+ webrtc_error_t* error);
72
+
73
+ WEBRTC_RUBY_API int webrtc_peer_connection_create_answer(
74
+ webrtc_peer_connection_t pc,
75
+ webrtc_session_description_t* out_sdp,
76
+ webrtc_error_t* error);
77
+
78
+ WEBRTC_RUBY_API int webrtc_peer_connection_set_local_description(
79
+ webrtc_peer_connection_t pc,
80
+ webrtc_session_description_t sdp,
81
+ webrtc_error_t* error);
82
+
83
+ WEBRTC_RUBY_API int webrtc_peer_connection_set_remote_description(
84
+ webrtc_peer_connection_t pc,
85
+ webrtc_session_description_t sdp,
86
+ webrtc_error_t* error);
87
+
88
+ WEBRTC_RUBY_API int webrtc_peer_connection_add_ice_candidate(
89
+ webrtc_peer_connection_t pc,
90
+ webrtc_ice_candidate_t candidate,
91
+ webrtc_error_t* error);
92
+
93
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_ice_candidate(
94
+ webrtc_peer_connection_t pc,
95
+ webrtc_ice_candidate_callback callback,
96
+ void* user_data);
97
+
98
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_connection_state_change(
99
+ webrtc_peer_connection_t pc,
100
+ webrtc_state_change_callback callback,
101
+ void* user_data);
102
+
103
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_ice_connection_state_change(
104
+ webrtc_peer_connection_t pc,
105
+ webrtc_state_change_callback callback,
106
+ void* user_data);
107
+
108
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_ice_gathering_state_change(
109
+ webrtc_peer_connection_t pc,
110
+ webrtc_state_change_callback callback,
111
+ void* user_data);
112
+
113
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_signaling_state_change(
114
+ webrtc_peer_connection_t pc,
115
+ webrtc_state_change_callback callback,
116
+ void* user_data);
117
+
118
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_data_channel(
119
+ webrtc_peer_connection_t pc,
120
+ webrtc_data_channel_callback callback,
121
+ void* user_data);
122
+
123
+ WEBRTC_RUBY_API void webrtc_peer_connection_on_track(
124
+ webrtc_peer_connection_t pc,
125
+ webrtc_track_callback callback,
126
+ void* user_data);
127
+
128
+ WEBRTC_RUBY_API int webrtc_peer_connection_get_signaling_state(
129
+ webrtc_peer_connection_t pc);
130
+ WEBRTC_RUBY_API int webrtc_peer_connection_get_ice_gathering_state(
131
+ webrtc_peer_connection_t pc);
132
+ WEBRTC_RUBY_API int webrtc_peer_connection_get_ice_connection_state(
133
+ webrtc_peer_connection_t pc);
134
+ WEBRTC_RUBY_API int webrtc_peer_connection_get_connection_state(
135
+ webrtc_peer_connection_t pc);
136
+
137
+ WEBRTC_RUBY_API int webrtc_peer_connection_add_track(
138
+ webrtc_peer_connection_t pc,
139
+ const char* kind,
140
+ int direction,
141
+ int* out_track_id,
142
+ webrtc_error_t* error);
143
+
144
+ WEBRTC_RUBY_API int webrtc_peer_connection_remove_track(
145
+ webrtc_peer_connection_t pc,
146
+ int track_id,
147
+ webrtc_error_t* error);
148
+
149
+ WEBRTC_RUBY_API int webrtc_track_get_direction(int track_id);
150
+ WEBRTC_RUBY_API const char* webrtc_track_get_kind(int track_id);
151
+
152
+ WEBRTC_RUBY_API webrtc_data_channel_t webrtc_peer_connection_create_data_channel(
153
+ webrtc_peer_connection_t pc,
154
+ const char* label,
155
+ bool ordered,
156
+ int max_retransmits,
157
+ int max_packet_life_time,
158
+ const char* protocol,
159
+ bool negotiated,
160
+ int id,
161
+ webrtc_error_t* error);
162
+
163
+ WEBRTC_RUBY_API void webrtc_data_channel_destroy(webrtc_data_channel_t dc);
164
+ WEBRTC_RUBY_API int webrtc_data_channel_send(webrtc_data_channel_t dc,
165
+ const uint8_t* data,
166
+ size_t length,
167
+ bool is_binary,
168
+ webrtc_error_t* error);
169
+ WEBRTC_RUBY_API void webrtc_data_channel_close(webrtc_data_channel_t dc);
170
+ WEBRTC_RUBY_API int webrtc_data_channel_get_ready_state(webrtc_data_channel_t dc);
171
+ WEBRTC_RUBY_API const char* webrtc_data_channel_get_label(webrtc_data_channel_t dc);
172
+ WEBRTC_RUBY_API size_t webrtc_data_channel_get_buffered_amount(webrtc_data_channel_t dc);
173
+
174
+ WEBRTC_RUBY_API void webrtc_data_channel_on_open(webrtc_data_channel_t dc,
175
+ webrtc_void_callback callback,
176
+ void* user_data);
177
+ WEBRTC_RUBY_API void webrtc_data_channel_on_message(webrtc_data_channel_t dc,
178
+ webrtc_message_callback callback,
179
+ void* user_data);
180
+ WEBRTC_RUBY_API void webrtc_data_channel_on_close(webrtc_data_channel_t dc,
181
+ webrtc_void_callback callback,
182
+ void* user_data);
183
+
184
+ WEBRTC_RUBY_API webrtc_session_description_t webrtc_session_description_create(
185
+ const char* type,
186
+ const char* sdp,
187
+ webrtc_error_t* error);
188
+ WEBRTC_RUBY_API void webrtc_session_description_destroy(
189
+ webrtc_session_description_t sd);
190
+ WEBRTC_RUBY_API const char* webrtc_session_description_get_type(
191
+ webrtc_session_description_t sd);
192
+ WEBRTC_RUBY_API const char* webrtc_session_description_get_sdp(
193
+ webrtc_session_description_t sd);
194
+
195
+ WEBRTC_RUBY_API webrtc_ice_candidate_t webrtc_ice_candidate_create(
196
+ const char* candidate,
197
+ const char* sdp_mid,
198
+ int sdp_mline_index,
199
+ webrtc_error_t* error);
200
+ WEBRTC_RUBY_API void webrtc_ice_candidate_destroy(webrtc_ice_candidate_t ic);
201
+ WEBRTC_RUBY_API const char* webrtc_ice_candidate_get_candidate(
202
+ webrtc_ice_candidate_t ic);
203
+ WEBRTC_RUBY_API const char* webrtc_ice_candidate_get_sdp_mid(
204
+ webrtc_ice_candidate_t ic);
205
+ WEBRTC_RUBY_API int webrtc_ice_candidate_get_sdp_mline_index(
206
+ webrtc_ice_candidate_t ic);
207
+
208
+ #ifdef __cplusplus
209
+ }
210
+ #endif
211
+
212
+ #endif
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCConfiguration
5
+ ICE_TRANSPORT_POLICIES = %i[all relay].freeze
6
+ BUNDLE_POLICIES = %i[balanced max_compat max_bundle].freeze
7
+ RTCP_MUX_POLICIES = %i[require].freeze
8
+
9
+ attr_accessor :ice_servers, :ice_transport_policy, :bundle_policy, :rtcp_mux_policy, :certificates,
10
+ :ice_candidate_pool_size
11
+
12
+ def initialize(options = {})
13
+ @ice_servers = normalize_ice_servers(options[:ice_servers] || options[:iceServers] || [])
14
+ @ice_transport_policy = options[:ice_transport_policy] || options[:iceTransportPolicy] || :all
15
+ @bundle_policy = options[:bundle_policy] || options[:bundlePolicy] || :balanced
16
+ @rtcp_mux_policy = options[:rtcp_mux_policy] || options[:rtcpMuxPolicy] || :require
17
+ @certificates = options[:certificates] || []
18
+ @ice_candidate_pool_size = options[:ice_candidate_pool_size] || options[:iceCandidatePoolSize] || 0
19
+
20
+ validate!
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ iceServers: @ice_servers.map(&:to_h),
26
+ iceTransportPolicy: @ice_transport_policy,
27
+ bundlePolicy: @bundle_policy,
28
+ rtcpMuxPolicy: @rtcp_mux_policy,
29
+ iceCandidatePoolSize: @ice_candidate_pool_size
30
+ }
31
+ end
32
+
33
+ private
34
+
35
+ def normalize_ice_servers(servers)
36
+ servers.map do |server|
37
+ server.is_a?(RTCIceServer) ? server : RTCIceServer.new(server)
38
+ end
39
+ end
40
+
41
+ def validate!
42
+ unless ICE_TRANSPORT_POLICIES.include?(@ice_transport_policy)
43
+ raise InvalidParameterError, "Invalid ice_transport_policy: #{@ice_transport_policy}"
44
+ end
45
+
46
+ unless BUNDLE_POLICIES.include?(@bundle_policy)
47
+ raise InvalidParameterError, "Invalid bundle_policy: #{@bundle_policy}"
48
+ end
49
+
50
+ return if @ice_candidate_pool_size.between?(0, 255)
51
+
52
+ raise InvalidParameterError, 'ice_candidate_pool_size must be 0-255'
53
+ end
54
+ end
55
+
56
+ class RTCIceServer
57
+ attr_accessor :urls, :username, :credential, :credential_type
58
+
59
+ def initialize(options = {})
60
+ @urls = normalize_urls(options[:urls] || options[:url])
61
+ @username = options[:username]
62
+ @credential = options[:credential]
63
+ @credential_type = options[:credential_type] || options[:credentialType] || :password
64
+
65
+ validate!
66
+ end
67
+
68
+ def to_h
69
+ hash = { urls: @urls }
70
+ hash[:username] = @username if @username
71
+ hash[:credential] = @credential if @credential
72
+ hash[:credentialType] = @credential_type if @credential
73
+ hash
74
+ end
75
+
76
+ private
77
+
78
+ def normalize_urls(urls)
79
+ case urls
80
+ when String
81
+ [urls]
82
+ when Array
83
+ urls
84
+ else
85
+ []
86
+ end
87
+ end
88
+
89
+ def validate!
90
+ raise InvalidParameterError, 'ICE server URLs required' if @urls.empty?
91
+
92
+ @urls.each do |url|
93
+ unless url.start_with?('stun:', 'stuns:', 'turn:', 'turns:')
94
+ raise InvalidParameterError, "Invalid ICE server URL: #{url}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCDataChannel
5
+ READY_STATES = %i[connecting open closing closed].freeze
6
+ BINARY_TYPES = %i[blob arraybuffer].freeze
7
+
8
+ attr_reader :label, :ordered, :max_packet_life_time, :max_retransmits, :protocol, :negotiated, :id
9
+ attr_accessor :buffered_amount_low_threshold, :binary_type
10
+
11
+ def initialize(ptr, options = {})
12
+ @ptr = ptr
13
+ @callbacks = {}
14
+ @internal_callbacks = Hash.new { |h, k| h[k] = [] }
15
+ @native_callbacks_setup = {}
16
+ @binary_type = :arraybuffer
17
+ @buffered_amount_low_threshold = 0
18
+ @messages_sent = 0
19
+ @bytes_sent = 0
20
+ @messages_received = 0
21
+ @bytes_received = 0
22
+
23
+ @label = options[:label] || FFI.webrtc_data_channel_get_label(ptr)
24
+ @ordered = options.fetch(:ordered, true)
25
+ @max_packet_life_time = options[:max_packet_life_time]
26
+ @max_retransmits = options[:max_retransmits]
27
+ @protocol = options[:protocol] || ''
28
+ @negotiated = options.fetch(:negotiated, false)
29
+ @id = options[:id]
30
+ end
31
+
32
+ def ready_state
33
+ return :closed if @ptr.nil?
34
+
35
+ state_index = FFI.webrtc_data_channel_get_ready_state(@ptr)
36
+ READY_STATES[state_index] || :unknown
37
+ end
38
+
39
+ def buffered_amount
40
+ return 0 if @ptr.nil?
41
+
42
+ FFI.webrtc_data_channel_get_buffered_amount(@ptr)
43
+ end
44
+
45
+ def send(data)
46
+ raise InvalidStateError, 'DataChannel is not open' unless ready_state == :open
47
+
48
+ is_binary = data.is_a?(String) && data.encoding == Encoding::BINARY
49
+ send_data(data, is_binary)
50
+ end
51
+
52
+ def send_text(data)
53
+ raise InvalidStateError, 'DataChannel is not open' unless ready_state == :open
54
+
55
+ send_data(data.to_s, false)
56
+ end
57
+
58
+ def send_binary(data)
59
+ raise InvalidStateError, 'DataChannel is not open' unless ready_state == :open
60
+
61
+ send_data(data, true)
62
+ end
63
+
64
+ def close
65
+ return if @ptr.nil?
66
+
67
+ FFI.webrtc_data_channel_close(@ptr)
68
+ end
69
+
70
+ def destroy
71
+ return if @ptr.nil?
72
+
73
+ FFI.webrtc_data_channel_destroy(@ptr)
74
+ @ptr = nil
75
+ end
76
+
77
+ def on_open(&block)
78
+ @callbacks[:open] = block
79
+ setup_open_callback
80
+ end
81
+
82
+ def on_close(&block)
83
+ @callbacks[:close] = block
84
+ setup_close_callback
85
+ end
86
+
87
+ def on_error(&block)
88
+ @callbacks[:error] = block
89
+ end
90
+
91
+ def on_message(&block)
92
+ @callbacks[:message] = block
93
+ setup_message_callback
94
+ end
95
+
96
+ def on_buffered_amount_low(&block)
97
+ @callbacks[:buffered_amount_low] = block
98
+ end
99
+
100
+ def observe(observer)
101
+ return self unless observer.respond_to?(:bind)
102
+
103
+ observer.bind(self)
104
+ end
105
+
106
+ def add_internal_listener(event, &block)
107
+ return self unless block
108
+
109
+ @internal_callbacks[event] << block
110
+ case event
111
+ when :open
112
+ setup_open_callback
113
+ when :close
114
+ setup_close_callback
115
+ when :message
116
+ setup_message_callback
117
+ end
118
+ self
119
+ end
120
+
121
+ def stats_snapshot
122
+ {
123
+ messages_sent: @messages_sent,
124
+ bytes_sent: @bytes_sent,
125
+ messages_received: @messages_received,
126
+ bytes_received: @bytes_received,
127
+ state: ready_state
128
+ }
129
+ end
130
+
131
+ private
132
+
133
+ def send_data(data, is_binary)
134
+ data_str = data.to_s
135
+ ptr = ::FFI::MemoryPointer.from_string(data_str)
136
+ error = FFI::Error.new
137
+
138
+ result = FFI.webrtc_data_channel_send(@ptr, ptr, data_str.bytesize, is_binary, error)
139
+ raise OperationError, error[:message] if result != 0
140
+
141
+ @messages_sent += 1
142
+ @bytes_sent += data_str.bytesize
143
+ emit_internal(:sent, data_str.bytesize, is_binary)
144
+
145
+ data_str.bytesize
146
+ end
147
+
148
+ def setup_open_callback
149
+ return if @native_callbacks_setup[:open] || @ptr.nil?
150
+
151
+ callback = proc do |_user_data|
152
+ emit_internal(:open)
153
+ @callbacks[:open]&.call
154
+ end
155
+ @callbacks[:open_proc] = callback
156
+ @native_callbacks_setup[:open] = true
157
+ FFI.webrtc_data_channel_on_open(@ptr, callback, ::FFI::Pointer::NULL)
158
+ end
159
+
160
+ def setup_close_callback
161
+ return if @native_callbacks_setup[:close] || @ptr.nil?
162
+
163
+ callback = proc do |_user_data|
164
+ emit_internal(:close)
165
+ @callbacks[:close]&.call
166
+ end
167
+ @callbacks[:close_proc] = callback
168
+ @native_callbacks_setup[:close] = true
169
+ FFI.webrtc_data_channel_on_close(@ptr, callback, ::FFI::Pointer::NULL)
170
+ end
171
+
172
+ def setup_message_callback
173
+ return if @native_callbacks_setup[:message] || @ptr.nil?
174
+
175
+ callback = proc do |data_ptr, length, is_binary, _user_data|
176
+ data = data_ptr.read_bytes(length)
177
+ data = data.force_encoding(Encoding::UTF_8) unless is_binary
178
+
179
+ message = DataChannelMessage.new(data, is_binary)
180
+ @messages_received += 1
181
+ @bytes_received += data.bytesize
182
+ emit_internal(:message, message)
183
+ @callbacks[:message]&.call(message)
184
+ end
185
+ @callbacks[:message_proc] = callback
186
+ @native_callbacks_setup[:message] = true
187
+ FFI.webrtc_data_channel_on_message(@ptr, callback, ::FFI::Pointer::NULL)
188
+ end
189
+
190
+ def emit_internal(event, *args)
191
+ callbacks = @internal_callbacks[event]
192
+ return if callbacks.nil? || callbacks.empty?
193
+
194
+ callbacks.each { |callback| callback.call(*args) }
195
+ end
196
+ end
197
+
198
+ class MessageEvent
199
+ attr_reader :data, :origin, :last_event_id
200
+
201
+ def initialize(data, is_binary)
202
+ @data = data
203
+ @is_binary = is_binary
204
+ @origin = ''
205
+ @last_event_id = ''
206
+ end
207
+
208
+ def binary?
209
+ @is_binary
210
+ end
211
+
212
+ def text?
213
+ !@is_binary
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCDtlsTransport
5
+ STATES = %i[new connecting connected closed failed].freeze
6
+
7
+ attr_reader :ice_transport, :state
8
+
9
+ def initialize(options = {})
10
+ @ice_transport = options[:ice_transport] || RTCIceTransport.new
11
+ @state = :new
12
+ @remote_certificates = []
13
+ @callbacks = {}
14
+ @ptr = options[:ptr]
15
+ end
16
+
17
+ def get_remote_certificates
18
+ @remote_certificates.dup
19
+ end
20
+
21
+ def on_state_change(&block)
22
+ @callbacks[:state_change] = block
23
+ end
24
+
25
+ def on_error(&block)
26
+ @callbacks[:error] = block
27
+ end
28
+
29
+ private
30
+
31
+ def set_state(new_state)
32
+ return if @state == new_state
33
+
34
+ @state = new_state
35
+ @callbacks[:state_change]&.call
36
+ end
37
+ end
38
+
39
+ class RTCDtlsFingerprint
40
+ attr_reader :algorithm, :value
41
+
42
+ def initialize(algorithm:, value:)
43
+ @algorithm = algorithm
44
+ @value = value
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ algorithm: @algorithm,
50
+ value: @value
51
+ }
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class RTCDTMFSender
5
+ VALID_TONES = '0123456789ABCD*#,'
6
+
7
+ attr_reader :tone_buffer, :duration, :inter_tone_gap
8
+
9
+ def initialize(options = {})
10
+ @sender = options[:sender]
11
+ @tone_buffer = ''
12
+ @duration = 100
13
+ @inter_tone_gap = 70
14
+ @callbacks = {}
15
+ end
16
+
17
+ def can_insert_dtmf?
18
+ @sender&.track&.kind == :audio
19
+ end
20
+
21
+ def insert_dtmf(tones, duration: 100, inter_tone_gap: 70)
22
+ raise InvalidStateError, 'Cannot insert DTMF' unless can_insert_dtmf?
23
+
24
+ validate_tones!(tones)
25
+ validate_duration!(duration)
26
+ validate_inter_tone_gap!(inter_tone_gap)
27
+
28
+ @duration = duration
29
+ @inter_tone_gap = inter_tone_gap
30
+ @tone_buffer = tones.upcase
31
+
32
+ schedule_tone_playback
33
+ end
34
+
35
+ def on_tone_change(&block)
36
+ @callbacks[:tone_change] = block
37
+ end
38
+
39
+ private
40
+
41
+ def validate_tones!(tones)
42
+ tones.each_char do |char|
43
+ raise InvalidParameterError, "Invalid DTMF tone: #{char}" unless VALID_TONES.include?(char.upcase)
44
+ end
45
+ end
46
+
47
+ def validate_duration!(duration)
48
+ return if duration >= 40 && duration <= 6000
49
+
50
+ raise InvalidParameterError, 'Duration must be between 40 and 6000 ms'
51
+ end
52
+
53
+ def validate_inter_tone_gap!(gap)
54
+ return if gap >= 30
55
+
56
+ raise InvalidParameterError, 'Inter-tone gap must be at least 30 ms'
57
+ end
58
+
59
+ def schedule_tone_playback
60
+ return if @tone_buffer.empty?
61
+
62
+ tone = @tone_buffer[0]
63
+ @tone_buffer = @tone_buffer[1..]
64
+
65
+ emit_tone_change(tone)
66
+ end
67
+
68
+ def emit_tone_change(tone)
69
+ event = RTCDTMFToneChangeEvent.new(tone: tone)
70
+ @callbacks[:tone_change]&.call(event)
71
+ end
72
+ end
73
+
74
+ class RTCDTMFToneChangeEvent
75
+ attr_reader :tone
76
+
77
+ def initialize(tone:)
78
+ @tone = tone
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class Error < StandardError; end
5
+
6
+ class InitializationError < Error; end
7
+ class OperationError < Error; end
8
+ class InvalidStateError < Error; end
9
+ class InvalidParameterError < Error; end
10
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class CurrentTaskQueueFactory
5
+ def create
6
+ self
7
+ end
8
+ end
9
+
10
+ class PeerConnectionFactory
11
+ attr_reader :task_queue_factory
12
+
13
+ def initialize(task_queue_factory: CurrentTaskQueueFactory.new, default_configuration: nil)
14
+ @task_queue_factory = task_queue_factory
15
+ @default_configuration = default_configuration
16
+ end
17
+
18
+ def create_peer_connection(configuration = nil)
19
+ RTCPeerConnection.new(configuration || @default_configuration, factory: self)
20
+ end
21
+ end
22
+
23
+ class << self
24
+ def create_modular_peer_connection_factory(task_queue_factory: CurrentTaskQueueFactory.new)
25
+ PeerConnectionFactory.new(task_queue_factory: task_queue_factory)
26
+ end
27
+ end
28
+ end