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
|
@@ -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
|