rtp 0.0.1 → 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.
Files changed (60) hide show
  1. data/Gemfile +0 -3
  2. data/History.rdoc +42 -0
  3. data/README.rdoc +60 -15
  4. data/Rakefile +9 -4
  5. data/lib/rtp.rb +0 -3
  6. data/lib/rtp/logger.rb +11 -0
  7. data/lib/rtp/packet.rb +39 -3
  8. data/lib/rtp/receiver.rb +255 -152
  9. data/lib/rtp/version.rb +1 -1
  10. data/rtp.gemspec +2 -1
  11. data/spec/rtp/coverage/assets/0.7.1/application.css +1110 -0
  12. data/spec/rtp/coverage/assets/0.7.1/application.js +626 -0
  13. data/spec/rtp/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  14. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  15. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  16. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  17. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  18. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  19. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  20. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  21. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  22. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  23. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  24. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  25. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  26. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  27. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  28. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  29. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  30. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  31. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  32. data/spec/rtp/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  33. data/spec/rtp/coverage/assets/0.7.1/favicon_green.png +0 -0
  34. data/spec/rtp/coverage/assets/0.7.1/favicon_red.png +0 -0
  35. data/spec/rtp/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  36. data/spec/rtp/coverage/assets/0.7.1/loading.gif +0 -0
  37. data/spec/rtp/coverage/assets/0.7.1/magnify.png +0 -0
  38. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  39. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  40. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  41. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  42. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  43. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  44. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  45. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  46. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  47. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  48. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  49. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  50. data/spec/rtp/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  51. data/spec/rtp/coverage/index.html +146 -0
  52. data/spec/rtp/receiver_spec.rb +241 -258
  53. data/spec/rtp_spec.rb +1 -1
  54. data/spec/spec_helper.rb +4 -13
  55. metadata +152 -23
  56. data/tasks/roodi.rake +0 -10
  57. data/tasks/roodi_config.yaml +0 -14
  58. data/tasks/rspec.rake +0 -10
  59. data/tasks/stats.rake +0 -12
  60. data/tasks/yard.rake +0 -8
data/Gemfile CHANGED
@@ -1,6 +1,3 @@
1
1
  source :rubygems
2
2
  gemspec
3
3
 
4
- group :test do
5
- gem 'rcov', '~> 0.9'
6
- end
data/History.rdoc CHANGED
@@ -1,3 +1,45 @@
1
+ === 0.1.0 / 2012-11-14
2
+
3
+ Lots of big changes this release! Only a minor bump though, since I'm assuming
4
+ that I'm about the only one using this at this point and I'm not going to mess
5
+ anyone else up by making these changes. This is how the initial release should
6
+ have been.
7
+
8
+ New features:
9
+ * RTP::Receiver#start (used to be #run--see below) now takes an optional block
10
+ that will yield a parsed RTP::Packet. This allows for inspecting packets
11
+ as they come in. If a block is given, no data will be written to the capture
12
+ file, so as to conserve I/O.
13
+ * Added ability to receive on multicast sockets. To do so, just set the
14
+ :ip_address option on init to the multicast address you want to use.
15
+ * Turned logging off by default. To turn on, just do <tt>RTP::Logger = true</tt>.
16
+ * Updated RTP::Logger to use log_switch 0.4.0's log_class_name feature.
17
+ * Setting RTP::Receiver#rtp_port will now set #rtcp_port. There's no
18
+ functionality behind the #rtcp_port yet; just a numerical placeholder, really.
19
+ * Use Kernel#at_exit to make sure capture files get closed and deleted.
20
+
21
+ Refactorings:
22
+ * RTP::Receiver.new now takes an options Hash instead of a bunch of params.
23
+ * Merged RTP::Receiver#run and RTP::Receiver#start_listener into
24
+ RTP::Receiver#start. There wasn't any benefit of having them separate.
25
+ * Removed all packet sorting provisions--pretty sure this shouldn't happen at
26
+ this level. This means RTP::Receiver#write_buffer_to_file is gone.
27
+ * Added a @packets Queue, dedicated to writing packets out to the capture file.
28
+ Besides getting out of the way of the receiving I/O, this also abstracts away
29
+ the writing to file logic from the receiving and parsing logic. In a related
30
+ manner...
31
+ * #strip_headers is an RTP::Receiver accessor now, so you can set
32
+ this to true if you only want to capture/yield the RTP payload.
33
+ * RTP::Receiver#init_server is now RTP::Receiver#init_socket.
34
+ * Removed port retrying for RTP::Receiver; if the port requested for use by the
35
+ Receiver is in use, I don't think it should be the role of the Receiver to try
36
+ to keep trying--you asked for port X--if it's not available, you should be
37
+ told so. Thus, users of Receivers should now implement this if they so desire.
38
+ * RTP::Receiver users are no longer required to tell the object what type of
39
+ IP addressing to use--this is now inferred from the IP address given to use.
40
+ Default is set to 0.0.0.0; if an address in the range 224.0.0.0 -
41
+ 239.255.255.255 is given, multicasat settings are assumed.
42
+
1
43
  === 0.0.1 / 2012-03-02
2
44
 
3
45
  * Initial release.
data/README.rdoc CHANGED
@@ -5,40 +5,85 @@ http://github.com/turboladen/rtp
5
5
  == DESCRIPTION
6
6
 
7
7
  This is a RTP library that provides the most basic features of the Real-Time
8
- Transport Protocol (RTP, {RFC 3550}[http://tools.ietf.org/html/rfc3550]. While
8
+ Transport Protocol (RTP, {RFC 3550}[http://tools.ietf.org/html/rfc3550]). While
9
9
  the protocol allows for extensions that define new ways for transporting encoded
10
10
  audio/video, this library doesn't yet deal with any specific media types. My
11
11
  current goal is simply for transporting a single RTP stream of data and allowing
12
- you to decode its fixed headers. Quite frankly, this first release is just a
13
- break out of the Receiver stuff that was built in to the
14
- {rtsp library}[https://github.com/turboladen/rtsp], with the ability to parse
15
- RTP header data.
12
+ you to inspect its packets.
13
+
14
+ == FEATURES/PROBLEMS
15
+
16
+ Features:
17
+
18
+ * Receive a single RTP stream over UDP or TCP, unicast or multicast
19
+ * Parse RTP packet headers
20
+ * Inspect packets on the fly
16
21
 
17
22
  Future goals will include:
18
23
 
19
- * Implement "sink"--sort RTP packets
20
- * Implementing RTCP
24
+ * RTCP
21
25
  * Multiplexed sessions
22
26
  * Decoding of basic audio/video codec headers
23
27
  * Decoding of some popular A/V extension headers (h.264, et al.)
24
28
  * An easy way to hand over your data to an app that will decode your A/V stream(s)
25
-
26
- == FEATURES/PROBLEMS
27
-
28
- Features:
29
-
30
- * Receive a single RTP stream over UDP
31
- * Parse RTP packet headers
29
+ * Sending an RTP stream
32
30
 
33
31
  == SYNOPSIS
34
32
 
35
- FIX (explain how to use, etc)
33
+ === Streaming to file
34
+
35
+ With its default settings, an {RTP::Receiver} will open a UDP socket on port 6790,
36
+ within a Thread, on 0.0.0.0 and capture the data it receives to a file.
37
+
38
+ receiver = RTP::Receiver.new # Uses a Tempfile by default
39
+ receiver.rtp_port # => 6790
40
+ receiver.transport_protocol # => :UDP
41
+ receiver.multicast? # => false
42
+ receiver.unicast? # => true
43
+ receiver.start
44
+ sleep 5
45
+ receiver.stop
46
+ receiver.capture_file.size # => 20040
47
+
48
+ To init a Receiver with some other options...
49
+
50
+ options = {
51
+ rtp_port: 9000,
52
+ transport_protocol: :TCP,
53
+ ip_address: '239.255.255.251', # Multicast!
54
+ strip_headers: true, # Strips RTP headers before writing to file
55
+ capture_file: File.new('data.rtp', 'wb')
56
+ }
57
+ receiver = RTP::Receiver.new(options)
58
+ receiver.start
59
+ sleep 5
60
+ receiver.stop
61
+ receiver.capture_file.size # => 18848
62
+
63
+ === Packet inspecting
64
+
65
+ To inspect (and do what you like with) packets as they come in...
66
+
67
+ receiver = RTP::Receiver.new
68
+ receiver.start do |packet|
69
+ puts "Sequence number: #{packet.sequence_number}" # => Sequence number: 2931830
70
+ end
71
+ sleep 5
72
+ receiver.stop
73
+ receiver.capture_file.size # => 0
74
+
75
+ Notice how no data was written to file when the block was passed to +#start+. The
76
+ idea there is to save on I/O; if you're inspecting packets, it's not fair to
77
+ assume you want to save the data to a file. If you do, however, in that case,
78
+ you can simply do so within the block.
36
79
 
37
80
  == REQUIREMENTS
38
81
 
39
82
  * (Tested) Rubies
40
83
  * 1.9.2
41
84
  * 1.9.3
85
+ * JRuby 1.9 mode
86
+ * Rubinius 1.9 mode
42
87
  * RubyGems:
43
88
  * bindata
44
89
  * log_switch
data/Rakefile CHANGED
@@ -5,11 +5,16 @@ require 'yard'
5
5
  # Load all extra rake task definitions
6
6
  Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].each { |ext| load ext }
7
7
 
8
- task default: :install
9
8
 
10
- YARD::Rake::YardocTask.new
11
- RSpec::Core::RakeTask.new
9
+ YARD::Rake::YardocTask.new do |t|
10
+ t.files = %w(lib/**/*.rb - *.rdoc)
11
+ t.options = %w[--private --protected --verbose]
12
+ end
13
+
14
+ RSpec::Core::RakeTask.new do |t|
15
+ t.ruby_opts = %w[-w]
16
+ end
12
17
 
13
18
  # Alias for rubygems-test
14
19
  task test: :spec
15
-
20
+ task default: :test
data/lib/rtp.rb CHANGED
@@ -1,7 +1,4 @@
1
1
  require_relative 'rtp/version'
2
- require 'pathname'
3
- require 'log_switch'
4
2
 
5
3
  module RTP
6
- extend LogSwitch
7
4
  end
data/lib/rtp/logger.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'log_switch'
2
+
3
+ module RTP
4
+ class Logger
5
+ extend LogSwitch
6
+ end
7
+ end
8
+
9
+ RTP::Logger.log_class_name = true
10
+ RTP::Logger.log = false
11
+
data/lib/rtp/packet.rb CHANGED
@@ -1,25 +1,56 @@
1
1
  require 'bindata'
2
2
 
3
3
  module RTP
4
+
5
+ # Decodes a single RTP packet into a Hash, so the packet can inspected and
6
+ # used accordingly. Form more info on types, see
7
+ # {bindata}[http://bindata.rubyforge.org].
4
8
  class Packet < BinData::Record
5
9
  endian :big
6
10
 
11
+ #---------------------------------------------------------------------------
12
+ # RTP Header
13
+ #---------------------------------------------------------------------------
14
+
15
+ # @return [BinData::Bit2]
7
16
  bit2 :version
17
+
18
+ # @return [BinData::Bit1]
8
19
  bit1 :padding
20
+
21
+ # @return [BinData::Bit1]
9
22
  bit1 :extension
23
+
24
+ # @return [BinData::Bit4]
10
25
  bit4 :csrc_count
11
26
 
27
+ # @return [BinData::Bit1]
12
28
  bit1 :marker
29
+
30
+ # @return [BinData::Bit7]
13
31
  bit7 :payload_type
14
32
 
33
+ # @return [BinData::Uint16be]
15
34
  uint16 :sequence_number
35
+
36
+ # @return [BinData::Uint32be]
16
37
  uint32 :timestamp
38
+
39
+ # @return [BinData::Uint32be]
17
40
  uint32 :ssrc_id
41
+
42
+ # @return [BinData::Array]
18
43
  array :csrc_ids, :type => :uint32, :initial_length => lambda { csrc_count }
19
44
 
45
+ #---------------------------------------------------------------------------
20
46
  # Extension header is variable length if :extension == 1
21
- #uint16 :extension_id
22
- #uint16 :extension_length
47
+ #---------------------------------------------------------------------------
48
+
49
+ # @return [BinData::Uint16be]
50
+ uint16 :extension_id, onlyif: :has_extension?
51
+
52
+ # @return [BinData::Uint16be]
53
+ uint16 :extension_length, onlyif: :has_extension?
23
54
 
24
55
  =begin
25
56
  # h.264 payload
@@ -37,7 +68,12 @@ module RTP
37
68
  bit5 :nal_unit_payload_type
38
69
  =end
39
70
  count_bytes_remaining :bytes_remaining
71
+
72
+ # @return [BinData::String]
40
73
  string :rtp_payload, read_length: lambda { bytes_remaining }
74
+
75
+ def has_extension?
76
+ extension == 1
77
+ end
41
78
  end
42
79
  end
43
-
data/lib/rtp/receiver.rb CHANGED
@@ -1,23 +1,19 @@
1
1
  require 'tempfile'
2
2
  require 'socket'
3
+ require 'timeout'
3
4
 
4
- require_relative '../rtp'
5
+ require_relative 'logger'
5
6
  require_relative 'error'
6
7
  require_relative 'packet'
7
8
 
9
+
8
10
  module RTP
9
11
 
10
- # Objects of this type can be used with a +RTSP::Client+ object in order to
11
- # capture the RTP data transmitted to the client as a result of an RTSP
12
- # PLAY call.
13
- #
14
- # In this version, objects of this type don't do much other than just capture
15
- # the data to a file; in later versions, objects of this type will be able
16
- # to provide a "sink" and allow for ensuring that the received RTP packets
17
- # will be reassembled in the correct order, as they're written to file
18
- # (objects of this type don't don't currently allow for checking RTP sequence
19
- # numbers on the data that's been received).
12
+ # Objects of this type receive RTP data over a socket and either save them to
13
+ # a file, or yield the packets to a given block. This is useful with other
14
+ # protocols, like RTSP.
20
15
  class Receiver
16
+ include LogSwitch::Mixin
21
17
 
22
18
  # Name of the file the data will be captured to unless #rtp_file is set.
23
19
  DEFAULT_CAPFILE_NAME = "rtp_capture.raw"
@@ -25,196 +21,303 @@ module RTP
25
21
  # Maximum number of bytes to receive on the socket.
26
22
  MAX_BYTES_TO_RECEIVE = 1500
27
23
 
28
- # Maximum times to retry using the next greatest port number.
29
- MAX_PORT_NUMBER_RETRIES = 50
24
+ # TTL value that will be used if receiving on a multicast socket.
25
+ MULTICAST_TTL = 4
26
+
27
+ # @return [File] The file to capture the RTP data to.
28
+ attr_reader :capture_file
30
29
 
31
- # @param [File] rtp_file The file to capture the RTP data to.
32
- # @return [File]
33
- attr_accessor :rtp_file
30
+ # @return [Array<Time>] The packet receipt timestamps.
31
+ attr_reader :packet_timestamps
34
32
 
35
- # @param [Boolean] strip_headers True if you want to strip the RTP headers.
36
- attr_accessor :strip_headers
33
+ # @return [Fixnum] The port on which to capture RTP data.
34
+ attr_reader :rtp_port
37
35
 
38
- # @return [Array<Time>] packet_timestamps The packet receipt timestamps.
39
- attr_accessor :packet_timestamps
40
-
41
- # @param [Fixnum] rtp_port The port on which to capture the RTP data.
42
- # @return [Fixnum]
43
- attr_accessor :rtp_port
36
+ # @return [Fixnum] Added for clarifying the roles of ports; not
37
+ # currently used though.
38
+ attr_reader :rtcp_port
44
39
 
45
- # @param [Symbol] transport_protocol +:UDP+ or +:TCP+.
46
- # @return [Symbol]
40
+ # @return [Symbol] The type of socket to use for capturing the RTP data.
41
+ # +:UDP+ or +:TCP+.
47
42
  attr_accessor :transport_protocol
48
43
 
49
- # @param [Symbol] broadcast_type +:multicast+ or +:unicast+.
50
- # @return [Symbol]
51
- attr_accessor :broadcast_type
52
-
53
- # @param [Symbol] transport_protocol The type of socket to use for capturing
54
- # the data. +:UDP+ or +:TCP+.
55
- # @param [Fixnum] rtp_port The port on which to capture RTP data.
56
- # @param [File] rtp_capture_file The file object to capture the RTP data to.
57
- def initialize(transport_protocol=:UDP, rtp_port=9000, rtp_capture_file=nil)
58
- @transport_protocol = transport_protocol
59
- @rtp_port = rtp_port
60
- @rtp_file = rtp_capture_file || Tempfile.new(DEFAULT_CAPFILE_NAME)
61
- @packet_timestamps = []
44
+ # @return [String] The IP address to receive RTP data on.
45
+ attr_accessor :ip_address
46
+
47
+ # @param [Hash] options
48
+ # @option options [Fixnum] :rtp_port The port on which to capture RTP data.
49
+ # +rtcp_port+ will be set to the next port above this.
50
+ # @option options [Symbol] :transport_protocol The type of socket to use for
51
+ # capturing the data. +:UDP+ or +:TCP+.
52
+ # @option options [String] :ip_address The IP address to open the socket on.
53
+ # If this is a multicast address, multicast options will be set.
54
+ # @option options [Boolean] :strip_headers If set to true, RTP headers will
55
+ # be stripped from packets before they're written to the capture file.
56
+ # @option options [File] :capture_file The file object to capture the RTP
57
+ # data to.
58
+ def initialize(options={})
59
+ @rtp_port = options[:rtp_port] || 6970
60
+ @rtcp_port = @rtp_port + 1
61
+ @transport_protocol = options[:transport_protocol] || :UDP
62
+ @ip_address = options[:ip_address] || '0.0.0.0'
63
+ @strip_headers = options[:strip_headers] || false
64
+ @capture_file = options[:capture_file] ||
65
+ Tempfile.new(DEFAULT_CAPFILE_NAME)
66
+
67
+ at_exit do
68
+ unless @capture_file.closed?
69
+ log "Closing and deleting capture capture file..."
70
+ @capture_file.close
71
+ @capture_file.unlink
72
+ end
73
+ end
74
+
75
+ @socket = nil
62
76
  @listener = nil
63
- @sequence_list = []
64
- @payload_data = []
65
- @strip_headers = false
77
+ @packet_writer = nil
78
+ @packets = Queue.new
79
+ @packet_timestamps = []
66
80
  end
67
81
 
68
- # Initializes a server of the correct socket type.
82
+ # Starts the packet writer (buffer) and listener.
69
83
  #
70
- # @return [UDPSocket, TCPSocket]
71
- # @raise [RTP::Error] If +@transport_protocol was not set to +:UDP+ or
72
- # +:TCP+.
73
- def init_server(protocol, port=9000)
74
- if protocol == :UDP
75
- server = init_udp_server(port)
76
- elsif protocol == :TCP
77
- server = init_tcp_server(port)
78
- else
79
- raise RTP::Error, "Unknown streaming_protocol requested: #{@transport_protocol}"
80
- end
84
+ # If a block is given, this will yield each parsed packet as an RTP::Packet.
85
+ # This lets you inspect packets as they come in:
86
+ # @example
87
+ # receiver = RTP::Receiver.new
88
+ # receiver.start do |packet|
89
+ # puts packet["sequence_number"]
90
+ # end
91
+ #
92
+ # @yield [RTP::Packet] Each parsed packet that comes in over the wire.
93
+ #
94
+ # @return [Boolean] true if started successfully.
95
+ def start(&block)
96
+ return false if running?
97
+ log "Starting receiving on port #{@rtp_port}..."
98
+
99
+ @packet_writer = start_packet_writer(&block)
100
+ @packet_writer.abort_on_exception = true
101
+
102
+ @socket = init_socket(@transport_protocol, @rtp_port, @ip_address)
103
+
104
+ @listener = start_listener(@socket)
105
+ @listener.abort_on_exception = true
106
+
107
+ running?
108
+ end
81
109
 
82
- server
110
+ # Stops the listener and packet writer threads.
111
+ #
112
+ # @return [Boolean] true if stopped successfully.
113
+ def stop
114
+ return false if !running?
115
+
116
+ log "Stopping #{self.class} on port #{@rtp_port}..."
117
+ stop_listener
118
+ log "listening? #{listening?}"
119
+
120
+ stop_packet_writer
121
+ log "writing packets? #{writing_packets?}"
122
+ log "running? #{running?}"
123
+
124
+ !running?
83
125
  end
84
126
 
85
- # Simply calls #start_listener.
86
- def run
87
- RTP.log "Starting #{self.class} on port #{@rtp_port}..."
88
- start_listener
127
+ # @return [Boolean] true if the listener thread is running; false if not.
128
+ def listening?
129
+ !@listener.nil? ? @listener.alive? : false
89
130
  end
90
131
 
132
+ # @return [Boolean] true if ready to write packets to file.
133
+ def writing_packets?
134
+ !@packet_writer.nil? ? @packet_writer.alive? : false
135
+ end
91
136
 
92
- # Starts the +@listener+ thread that starts up the server, then takes the
93
- # data received from the server and pushes it on to the +@@payload_data+.
94
- #
95
- # @return [Thread] The listener thread (+@listener+).
96
- def start_listener
97
- return @listener if listening?
137
+ # @return [Boolean] true if the Receiver is listening and writing packets.
138
+ def running?
139
+ listening? && writing_packets?
140
+ end
98
141
 
99
- @listener = Thread.start do
100
- server = init_server(@transport_protocol, @rtp_port)
142
+ # Updates the rtp_port and sets the rtcp_port to be this +1.
143
+ def rtp_port=(port)
144
+ @rtp_port = port
145
+ @rtcp_port = @rtp_port + 1
146
+ end
101
147
 
148
+ # @return [Boolean] true if +ip_address+ is a multicast address or not.
149
+ def multicast?
150
+ first_octet = @ip_address.match(/^(\d\d\d?)/).to_s.to_i
151
+ first_octet >= 224 && first_octet <= 239
152
+ end
153
+
154
+ # @return [Boolean] true if +ip_address+ is a unicast address or not.
155
+ def unicast?
156
+ !multicast?
157
+ end
158
+
159
+ # @return [Symbol] The IP addressing type to use for capturing the data.
160
+ # +:multicast+ or +:unicast:.
161
+ def ip_addressing_type
162
+ multicast? ? :multicast : :unicast
163
+ end
164
+
165
+ private
166
+
167
+ # This starts a new Thread for reading packets off of the list of packets
168
+ # that has been read in by the listener. If no block is given, this writes
169
+ # all received packets (in the @packets Queue) to the +capture_file+. If a
170
+ # block is given, it yields each packet, parsed as an RTP::Packet. If
171
+ # +strip_headers+ is set, it only writes/yields the RTP payload to the file.
172
+ #
173
+ # @yield [RTP::Packet] Each parsed packet that comes in over the wire.
174
+ # @return [Thread] The packet writer thread.
175
+ def start_packet_writer
176
+ return @packet_writer if @packet_writer
177
+
178
+ # If a block is given for packet inspection, perhaps we should save
179
+ # some I/O ano not write the packet to file?
180
+ Thread.start do
102
181
  loop do
103
- begin
104
- msg = server.recvmsg_nonblock(MAX_BYTES_TO_RECEIVE)
105
- data = msg.first
106
- @packet_timestamps << msg.last.timestamp
107
- RTP.log "received data with size: #{data.size}"
108
- packet = RTP::Packet.read(data)
109
- RTP.log "rtp payload size: #{packet["rtp_payload"].size}"
110
- write_buffer_to_file if @sequence_list.include? packet["sequence_number"].to_i
111
- @sequence_list << packet["sequence_number"].to_i
112
-
113
- if @strip_headers
114
- @payload_data[packet["sequence_number"].to_i] = packet["rtp_payload"]
115
- else
116
- @payload_data[packet["sequence_number"].to_i] = data
117
- end
118
- rescue Errno::EAGAIN;
119
- write_buffer_to_file
120
- end # rescue error when no data is available to read.
182
+ packet = RTP::Packet.read(@packets.pop)
183
+ data_to_write = @strip_headers ? packet.rtp_payload : packet
184
+
185
+ if block_given?
186
+ yield data_to_write
187
+ else
188
+ @capture_file.write(data_to_write)
189
+ end
121
190
  end
122
191
  end
192
+ end
123
193
 
124
- @listener.abort_on_exception = true
125
- end
194
+ # Initializes a socket of the requested type.
195
+ #
196
+ # @param [Symbol] protocol The protocol on which to receive RTP data.
197
+ # @param [Fixnum] port The port on which to receive RTP data.
198
+ # @param [String] ip_address The IP on which to receive RTP data.
199
+ # @return [UDPSocket, TCPServer]
200
+ # @raise [RTP::Error] If +protocol+ was not set to +:UDP+ or +:TCP+.
201
+ def init_socket(protocol, port, ip_address)
202
+ log "Setting up #{protocol} socket on #{ip_address}:#{port}"
126
203
 
127
- # Sorts the sequence numbers and writes the data in the buffer to file.
128
- def write_buffer_to_file
129
- @sequence_list.sort.each do |sequence|
130
- @rtp_file.write @payload_data[sequence]
204
+ if protocol == :UDP
205
+ socket = UDPSocket.open
206
+ socket.bind(ip_address, port)
207
+ elsif protocol == :TCP
208
+ socket = TCPServer.new(ip_address, port)
209
+ else
210
+ raise RTP::Error,
211
+ "Unknown protocol requested: #{protocol}. Options are :TCP or :UDP"
131
212
  end
132
213
 
133
- @sequence_list.clear
134
- @payload_data.clear
135
- end
214
+ set_socket_time_options(socket)
215
+ setup_multicast_socket(socket, multicast_address) if multicast?
136
216
 
137
- # @return [Boolean] true if the +@listener+ thread is running; false if not.
138
- def listening?
139
- !@listener.nil? ? @listener.alive? : false
217
+ @rtp_port = port
218
+ log "#{protocol} server setup to receive on port #{@rtp_port}"
219
+
220
+ socket
140
221
  end
141
222
 
142
- # Returns if the #run loop is in action.
223
+ # Starts the thread that receives the RTP data, then takes that data and
224
+ # pushes it on to +@packets+ for processing.
143
225
  #
144
- # @return [Boolean] true if the run loop is running.
145
- def running?
146
- listening?
147
- end
226
+ # @param [IPSocket] socket The socket to listen on.
227
+ # @return [Thread] The listener thread.
228
+ def start_listener(socket)
229
+ return @listener if @listener
148
230
 
149
- # Breaks out of the run loop.
150
- def stop
151
- RTP.log "Stopping #{self.class} on port #{@rtp_port}..."
152
- stop_listener
153
- RTP.log "listening? #{listening?}"
154
- write_buffer_to_file
155
- RTP.log "running? #{running?}"
231
+ Thread.start(socket) do
232
+ loop do
233
+ msg = socket.recvmsg(MAX_BYTES_TO_RECEIVE)
234
+ data = msg.first
235
+ log "Received data at size: #{data.size}"
236
+
237
+ log "RTP timestamp from socket info: #{msg.last.timestamp}"
238
+ @packet_timestamps << msg.last.timestamp
239
+ @packets << data
240
+ end
241
+ end
156
242
  end
157
243
 
158
244
  # Kills the +@listener+ thread and sets the variable to nil.
245
+ #
246
+ # @return [Boolean] true if it stopped listening.
159
247
  def stop_listener
160
- #@listener.kill if @listener
248
+ log "Stopping listener..."
249
+ @socket.close if @socket
161
250
  @listener.kill if listening?
162
251
  @listener = nil
252
+ log "Listener stopped."
253
+
254
+ !listening?
163
255
  end
164
256
 
165
- # Sets up to receive data on a UDP socket, using +@rtp_port+.
257
+ # Waits for all packets to be written out before killing. AFter 10 seconds
258
+ # it'll force close the writer.
166
259
  #
167
- # @param [Fixnum] port Port number to listen for RTP data on.
168
- # @return [UDPSocket]
169
- def init_udp_server(port)
170
- port_retries = 0
260
+ # @return [Boolean] true if it stopped the packet writer.
261
+ def stop_packet_writer
262
+ log "Stopping packet writer..."
263
+ wait_for = 10
171
264
 
172
265
  begin
173
- server = UDPSocket.open
174
- server.bind('0.0.0.0', port)
175
- server.setsockopt(:SOCKET, :TIMESTAMP, true)
176
- optval = [0, 1].pack("l_2")
177
- server.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval)
178
- rescue Errno::EADDRINUSE
179
- RTP.log "RTP port #{port} in use, trying #{port + 1}..."
180
- port += 1
181
- port_retries += 1
182
- retry until port_retries == MAX_PORT_NUMBER_RETRIES + 1
183
- port = 9000
184
- raise
266
+ timeout(wait_for) do
267
+ sleep 0.2 until @packets.empty?
268
+ end
269
+ rescue Timeout::Error
270
+ log "Packet buffer not empty after #{wait_for} seconds. Trying to stop listener..."
271
+ stop_listener
185
272
  end
186
273
 
187
- @rtp_port = port
188
- RTP.log "UDP server setup to receive on port #{@rtp_port}"
274
+ @packet_writer.kill if writing_packets?
275
+ @packet_writer = nil
276
+ log "Packet writer stopped."
277
+
278
+ @capture_file.close
189
279
 
190
- server
280
+ !writing_packets?
191
281
  end
192
282
 
193
- # Sets up to receive data on a TCP socket, using +@rtp_port+.
283
+ # Sets SO_TIMESTAMP socket option to true. Sets SO_RCVTIMEO to 2.
194
284
  #
195
- # @param [Fixnum] port Port number to listen for RTP data on.
196
- # @return [TCPServer]
197
- def init_tcp_server(port)
198
- port_retries = 0
285
+ # @param [Socket] socket The Socket to set options on.
286
+ def set_socket_time_options(socket)
287
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_TIMESTAMP, true)
288
+ optval = [0, 1].pack("l_2")
289
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval)
290
+ end
199
291
 
200
- begin
201
- server = TCPServer.new(port)
202
- server.setsockopt(:SOCKET, :TIMESTAMP, true)
203
- optval = [0, 1].pack("l_2")
204
- server.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval)
205
- rescue Errno::EADDRINUSE
206
- RTP.log "RTP port #{port} in use, trying #{port + 1}..."
207
- port += 1
208
- port_retries += 1
209
- retry until port_retries == MAX_PORT_NUMBER_RETRIES + 1
210
- port = 9000
211
- raise
212
- end
292
+ # Sets Socket options to allow for multicasting. If ENV["RUBY_UPNP_ENV"] is
293
+ # equal to "testing", then it doesn't turn off multicast looping.
294
+ #
295
+ # @param [Socket] socket The socket to set the options on.
296
+ # @param [String] multicast_address The IP address to set the options on.
297
+ def setup_multicast_socket(socket, multicast_address)
298
+ set_membership(socket,
299
+ IPAddr.new(multicast_address).hton + IPAddr.new('0.0.0.0').hton)
300
+ set_multicast_ttl(socket, MULTICAST_TTL)
301
+ set_ttl(socket, MULTICAST_TTL)
302
+ end
213
303
 
214
- @rtp_port = port
215
- RTP.log "TCP server setup to receive on port #{@rtp_port}"
304
+ # @param [Socket] socket The socket to set the options on.
305
+ # @param [String] membership The network byte ordered String that represents
306
+ # the IP(s) that should join the membership group.
307
+ def set_membership(socket, membership)
308
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
309
+ end
310
+
311
+ # @param [Socket] socket The socket to set the options on.
312
+ # @param [Fixnum] ttl TTL to set IP_MULTICAST_TTL to.
313
+ def set_multicast_ttl(socket, ttl)
314
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, [ttl].pack('i'))
315
+ end
216
316
 
217
- server
317
+ # @param [Socket] socket The socket to set the options on.
318
+ # @param [Fixnum] ttl TTL to set IP_TTL to.
319
+ def set_ttl(socket, ttl)
320
+ socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_TTL, [ttl].pack('i'))
218
321
  end
219
322
  end
220
- end
323
+ end