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,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module WebRTC
6
+ module FFI
7
+ extend ::FFI::Library
8
+
9
+ LIB_NAME = case RbConfig::CONFIG['host_os']
10
+ when /darwin/ then 'libwebrtc_ruby.dylib'
11
+ when /mswin|mingw/ then 'webrtc_ruby.dll'
12
+ else 'libwebrtc_ruby.so'
13
+ end
14
+
15
+ LIB_PATH = File.join(__dir__, '..', '..', '..', 'ext', 'webrtc_ruby', 'build', LIB_NAME)
16
+
17
+ ffi_lib LIB_PATH if File.exist?(LIB_PATH)
18
+
19
+ class Error < ::FFI::Struct
20
+ layout :code, :int,
21
+ :message, :string
22
+ end
23
+
24
+ class Configuration < ::FFI::Struct
25
+ layout :ice_servers, :pointer,
26
+ :ice_servers_count, :size_t,
27
+ :ice_transport_policy, :pointer,
28
+ :bundle_policy, :pointer,
29
+ :rtcp_mux_policy, :pointer,
30
+ :enable_ice_tcp, :bool,
31
+ :enable_ice_udp_mux, :bool,
32
+ :disable_auto_negotiation, :bool,
33
+ :force_media_transport, :bool,
34
+ :mtu, :int,
35
+ :max_message_size, :int
36
+ end
37
+
38
+ callback :void_callback, [:pointer], :void
39
+ callback :ice_candidate_callback, %i[string string int pointer], :void
40
+ callback :data_channel_callback, %i[pointer pointer], :void
41
+ callback :message_callback, %i[pointer size_t bool pointer], :void
42
+ callback :state_change_callback, %i[int pointer], :void
43
+ callback :track_callback, %i[int pointer], :void
44
+
45
+ attach_function :webrtc_init, [], :int
46
+ attach_function :webrtc_cleanup, [], :void
47
+
48
+ attach_function :webrtc_peer_connection_create,
49
+ [Configuration.by_ref, Error.by_ref], :pointer
50
+ attach_function :webrtc_peer_connection_destroy, [:pointer], :void
51
+
52
+ attach_function :webrtc_peer_connection_create_offer,
53
+ [:pointer, :pointer, Error.by_ref], :int
54
+ attach_function :webrtc_peer_connection_create_answer,
55
+ [:pointer, :pointer, Error.by_ref], :int
56
+
57
+ attach_function :webrtc_peer_connection_set_local_description,
58
+ [:pointer, :pointer, Error.by_ref], :int
59
+ attach_function :webrtc_peer_connection_set_remote_description,
60
+ [:pointer, :pointer, Error.by_ref], :int
61
+ attach_function :webrtc_peer_connection_add_ice_candidate,
62
+ [:pointer, :pointer, Error.by_ref], :int
63
+
64
+ attach_function :webrtc_peer_connection_on_ice_candidate,
65
+ %i[pointer ice_candidate_callback pointer], :void
66
+ attach_function :webrtc_peer_connection_on_connection_state_change,
67
+ %i[pointer state_change_callback pointer], :void
68
+ attach_function :webrtc_peer_connection_on_ice_connection_state_change,
69
+ %i[pointer state_change_callback pointer], :void
70
+ attach_function :webrtc_peer_connection_on_ice_gathering_state_change,
71
+ %i[pointer state_change_callback pointer], :void
72
+ attach_function :webrtc_peer_connection_on_signaling_state_change,
73
+ %i[pointer state_change_callback pointer], :void
74
+ attach_function :webrtc_peer_connection_on_data_channel,
75
+ %i[pointer data_channel_callback pointer], :void
76
+ attach_function :webrtc_peer_connection_on_track,
77
+ %i[pointer track_callback pointer], :void
78
+
79
+ attach_function :webrtc_peer_connection_get_signaling_state, [:pointer], :int
80
+ attach_function :webrtc_peer_connection_get_ice_gathering_state, [:pointer], :int
81
+ attach_function :webrtc_peer_connection_get_ice_connection_state, [:pointer], :int
82
+ attach_function :webrtc_peer_connection_get_connection_state, [:pointer], :int
83
+ attach_function :webrtc_peer_connection_add_track,
84
+ [:pointer, :string, :int, :pointer, Error.by_ref], :int
85
+ attach_function :webrtc_peer_connection_remove_track,
86
+ [:pointer, :int, Error.by_ref], :int
87
+ attach_function :webrtc_track_get_direction, [:int], :int
88
+ attach_function :webrtc_track_get_kind, [:int], :string
89
+
90
+ attach_function :webrtc_peer_connection_create_data_channel,
91
+ [:pointer, :string, :bool, :int, :int, :string, :bool, :int, Error.by_ref],
92
+ :pointer
93
+
94
+ attach_function :webrtc_data_channel_destroy, [:pointer], :void
95
+ attach_function :webrtc_data_channel_send,
96
+ [:pointer, :pointer, :size_t, :bool, Error.by_ref], :int
97
+ attach_function :webrtc_data_channel_close, [:pointer], :void
98
+ attach_function :webrtc_data_channel_get_ready_state, [:pointer], :int
99
+ attach_function :webrtc_data_channel_get_label, [:pointer], :string
100
+ attach_function :webrtc_data_channel_get_buffered_amount, [:pointer], :size_t
101
+
102
+ attach_function :webrtc_data_channel_on_open,
103
+ %i[pointer void_callback pointer], :void
104
+ attach_function :webrtc_data_channel_on_message,
105
+ %i[pointer message_callback pointer], :void
106
+ attach_function :webrtc_data_channel_on_close,
107
+ %i[pointer void_callback pointer], :void
108
+
109
+ attach_function :webrtc_session_description_create,
110
+ [:string, :string, Error.by_ref], :pointer
111
+ attach_function :webrtc_session_description_destroy, [:pointer], :void
112
+ attach_function :webrtc_session_description_get_type, [:pointer], :string
113
+ attach_function :webrtc_session_description_get_sdp, [:pointer], :string
114
+
115
+ attach_function :webrtc_ice_candidate_create,
116
+ [:string, :string, :int, Error.by_ref], :pointer
117
+ attach_function :webrtc_ice_candidate_destroy, [:pointer], :void
118
+ attach_function :webrtc_ice_candidate_get_candidate, [:pointer], :string
119
+ attach_function :webrtc_ice_candidate_get_sdp_mid, [:pointer], :string
120
+ attach_function :webrtc_ice_candidate_get_sdp_mline_index, [:pointer], :int
121
+ end
122
+ end
@@ -0,0 +1,63 @@
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
+ init = init.to_h if init.respond_to?(:to_h)
9
+ @candidate = init[:candidate] || ''
10
+ @sdp_mid = init[:sdp_mid] || init[:sdpMid]
11
+ @sdp_m_line_index = init[:sdp_m_line_index] || init[:sdpMLineIndex]
12
+ @username_fragment = init[:username_fragment] || init[:usernameFragment]
13
+ @ptr = nil
14
+ end
15
+
16
+ def self.from_ptr(ptr)
17
+ return nil if ptr.nil? || ptr.null?
18
+
19
+ candidate_str = FFI.webrtc_ice_candidate_get_candidate(ptr)
20
+ sdp_mid_str = FFI.webrtc_ice_candidate_get_sdp_mid(ptr)
21
+ sdp_m_line_index = FFI.webrtc_ice_candidate_get_sdp_mline_index(ptr)
22
+
23
+ ice = new(
24
+ candidate: candidate_str,
25
+ sdp_mid: sdp_mid_str,
26
+ sdp_m_line_index: sdp_m_line_index
27
+ )
28
+ ice.instance_variable_set(:@ptr, ptr)
29
+ ice
30
+ end
31
+
32
+ def to_ptr
33
+ return @ptr if @ptr && !@ptr.null?
34
+
35
+ error = FFI::Error.new
36
+ @ptr = FFI.webrtc_ice_candidate_create(
37
+ candidate,
38
+ sdp_mid,
39
+ sdp_m_line_index || 0,
40
+ error
41
+ )
42
+ raise OperationError, error[:message] if error[:code] != 0
43
+
44
+ @ptr
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ candidate: candidate,
50
+ sdpMid: sdp_mid,
51
+ sdpMLineIndex: sdp_m_line_index,
52
+ usernameFragment: username_fragment
53
+ }
54
+ end
55
+
56
+ def release
57
+ return unless @ptr && !@ptr.null?
58
+
59
+ FFI.webrtc_ice_candidate_destroy(@ptr)
60
+ @ptr = nil
61
+ end
62
+ end
63
+ 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,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class MediaStreamInterface
5
+ attr_reader :stream
6
+
7
+ def initialize(stream = MediaStream.new)
8
+ @stream = stream
9
+ end
10
+
11
+ def add_track(track)
12
+ @stream.add_track(track)
13
+ end
14
+
15
+ def remove_track(track)
16
+ @stream.remove_track(track)
17
+ end
18
+
19
+ def get_tracks
20
+ @stream.get_tracks
21
+ end
22
+ end
23
+
24
+ class AudioTrackSinkInterface
25
+ def on_data(_frame); end
26
+ end
27
+
28
+ class AudioTrackSourceInterface
29
+ def initialize
30
+ @sinks = []
31
+ end
32
+
33
+ def add_sink(sink)
34
+ @sinks << sink unless @sinks.include?(sink)
35
+ end
36
+
37
+ def remove_sink(sink)
38
+ @sinks.delete(sink)
39
+ end
40
+
41
+ def push_data(frame)
42
+ @sinks.each { |sink| sink.on_data(frame) if sink.respond_to?(:on_data) }
43
+ end
44
+ end
45
+
46
+ class AudioTrackInterface < MediaStreamTrack
47
+ attr_reader :source
48
+
49
+ def initialize(source: AudioTrackSourceInterface.new, **options)
50
+ super({ kind: :audio }.merge(options))
51
+ @source = source
52
+ end
53
+
54
+ def add_sink(sink)
55
+ @source.add_sink(sink)
56
+ end
57
+
58
+ def remove_sink(sink)
59
+ @source.remove_sink(sink)
60
+ end
61
+ end
62
+
63
+ class VideoTrackSinkInterface
64
+ def on_frame(_frame); end
65
+ end
66
+
67
+ class VideoTrackSourceInterface
68
+ def initialize
69
+ @sinks = []
70
+ end
71
+
72
+ def add_sink(sink)
73
+ @sinks << sink unless @sinks.include?(sink)
74
+ end
75
+
76
+ def remove_sink(sink)
77
+ @sinks.delete(sink)
78
+ end
79
+
80
+ def push_frame(frame)
81
+ @sinks.each { |sink| sink.on_frame(frame) if sink.respond_to?(:on_frame) }
82
+ end
83
+ end
84
+
85
+ class VideoTrackInterface < MediaStreamTrack
86
+ attr_reader :source
87
+
88
+ def initialize(source: VideoTrackSourceInterface.new, **options)
89
+ super({ kind: :video }.merge(options))
90
+ @source = source
91
+ end
92
+
93
+ def add_sink(sink)
94
+ @source.add_sink(sink)
95
+ end
96
+
97
+ def remove_sink(sink)
98
+ @source.remove_sink(sink)
99
+ end
100
+ end
101
+ 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,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebRTC
4
+ class BaseObserver
5
+ def initialize(on_success: nil, on_failure: nil)
6
+ @on_success = on_success
7
+ @on_failure = on_failure
8
+ end
9
+
10
+ def on_success(value = nil)
11
+ @on_success&.call(value)
12
+ end
13
+
14
+ def on_failure(error)
15
+ @on_failure&.call(error)
16
+ end
17
+ end
18
+
19
+ class CreateSessionDescriptionObserver < BaseObserver; end
20
+ class SetSessionDescriptionObserver < BaseObserver; end
21
+ class SetLocalDescriptionObserver < SetSessionDescriptionObserver; end
22
+ class SetRemoteDescriptionObserver < SetSessionDescriptionObserver; end
23
+ class AddIceCandidateObserver < BaseObserver; end
24
+
25
+ class DataChannelObserver
26
+ def initialize(on_open: nil, on_close: nil, on_message: nil, on_error: nil)
27
+ @on_open = on_open
28
+ @on_close = on_close
29
+ @on_message = on_message
30
+ @on_error = on_error
31
+ end
32
+
33
+ def bind(channel)
34
+ channel.on_open { @on_open&.call }
35
+ channel.on_close { @on_close&.call }
36
+ channel.on_message { |message| @on_message&.call(message) }
37
+ channel.on_error { |error| @on_error&.call(error) }
38
+ channel
39
+ end
40
+ end
41
+
42
+ class RTCStatsCollectorCallback
43
+ def initialize(&block)
44
+ @block = block
45
+ end
46
+
47
+ def on_stats_delivered(response)
48
+ @block&.call(response)
49
+ end
50
+ end
51
+ end