rtp 0.0.1
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.
- data/.gemtest +1 -0
- data/Gemfile +6 -0
- data/History.rdoc +4 -0
- data/README.rdoc +57 -0
- data/Rakefile +15 -0
- data/lib/rtp.rb +7 -0
- data/lib/rtp/error.rb +5 -0
- data/lib/rtp/packet.rb +43 -0
- data/lib/rtp/receiver.rb +220 -0
- data/lib/rtp/version.rb +3 -0
- data/rtp.gemspec +29 -0
- data/spec/rtp/receiver_spec.rb +368 -0
- data/spec/rtp_spec.rb +24 -0
- data/spec/spec_helper.rb +20 -0
- data/tasks/roodi.rake +10 -0
- data/tasks/roodi_config.yaml +14 -0
- data/tasks/rspec.rake +10 -0
- data/tasks/stats.rake +12 -0
- data/tasks/yard.rake +8 -0
- metadata +147 -0
data/.gemtest
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
data/Gemfile
ADDED
data/History.rdoc
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
= rtp
|
2
|
+
|
3
|
+
http://github.com/turboladen/rtp
|
4
|
+
|
5
|
+
== DESCRIPTION
|
6
|
+
|
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
|
9
|
+
the protocol allows for extensions that define new ways for transporting encoded
|
10
|
+
audio/video, this library doesn't yet deal with any specific media types. My
|
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.
|
16
|
+
|
17
|
+
Future goals will include:
|
18
|
+
|
19
|
+
* Implement "sink"--sort RTP packets
|
20
|
+
* Implementing RTCP
|
21
|
+
* Multiplexed sessions
|
22
|
+
* Decoding of basic audio/video codec headers
|
23
|
+
* Decoding of some popular A/V extension headers (h.264, et al.)
|
24
|
+
* 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
|
32
|
+
|
33
|
+
== SYNOPSIS
|
34
|
+
|
35
|
+
FIX (explain how to use, etc)
|
36
|
+
|
37
|
+
== REQUIREMENTS
|
38
|
+
|
39
|
+
* (Tested) Rubies
|
40
|
+
* 1.9.2
|
41
|
+
* 1.9.3
|
42
|
+
* RubyGems:
|
43
|
+
* bindata
|
44
|
+
* log_switch
|
45
|
+
|
46
|
+
== INSTALL
|
47
|
+
|
48
|
+
$ gem install rtp
|
49
|
+
|
50
|
+
== DEVELOPERS
|
51
|
+
|
52
|
+
After checking out the source, run:
|
53
|
+
|
54
|
+
$ bundle install
|
55
|
+
|
56
|
+
This task will install any missing dependencies for you.
|
57
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
# Load all extra rake task definitions
|
6
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].each { |ext| load ext }
|
7
|
+
|
8
|
+
task default: :install
|
9
|
+
|
10
|
+
YARD::Rake::YardocTask.new
|
11
|
+
RSpec::Core::RakeTask.new
|
12
|
+
|
13
|
+
# Alias for rubygems-test
|
14
|
+
task test: :spec
|
15
|
+
|
data/lib/rtp.rb
ADDED
data/lib/rtp/error.rb
ADDED
data/lib/rtp/packet.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bindata'
|
2
|
+
|
3
|
+
module RTP
|
4
|
+
class Packet < BinData::Record
|
5
|
+
endian :big
|
6
|
+
|
7
|
+
bit2 :version
|
8
|
+
bit1 :padding
|
9
|
+
bit1 :extension
|
10
|
+
bit4 :csrc_count
|
11
|
+
|
12
|
+
bit1 :marker
|
13
|
+
bit7 :payload_type
|
14
|
+
|
15
|
+
uint16 :sequence_number
|
16
|
+
uint32 :timestamp
|
17
|
+
uint32 :ssrc_id
|
18
|
+
array :csrc_ids, :type => :uint32, :initial_length => lambda { csrc_count }
|
19
|
+
|
20
|
+
# Extension header is variable length if :extension == 1
|
21
|
+
#uint16 :extension_id
|
22
|
+
#uint16 :extension_length
|
23
|
+
|
24
|
+
=begin
|
25
|
+
# h.264 payload
|
26
|
+
# NAL section of RTP Payload
|
27
|
+
# NAL unit header && payload header
|
28
|
+
bit1 :nal_unit_forbidden_zero
|
29
|
+
bit2 :nal_ref_idc
|
30
|
+
bit5 :nal_unit_type
|
31
|
+
|
32
|
+
# Payload byte string?
|
33
|
+
# FU header
|
34
|
+
bit1 :start_bit
|
35
|
+
bit1 :end_bit
|
36
|
+
bit1 :reserved # must be 0 and must be ignored
|
37
|
+
bit5 :nal_unit_payload_type
|
38
|
+
=end
|
39
|
+
count_bytes_remaining :bytes_remaining
|
40
|
+
string :rtp_payload, read_length: lambda { bytes_remaining }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
data/lib/rtp/receiver.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'socket'
|
3
|
+
|
4
|
+
require_relative '../rtp'
|
5
|
+
require_relative 'error'
|
6
|
+
require_relative 'packet'
|
7
|
+
|
8
|
+
module RTP
|
9
|
+
|
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).
|
20
|
+
class Receiver
|
21
|
+
|
22
|
+
# Name of the file the data will be captured to unless #rtp_file is set.
|
23
|
+
DEFAULT_CAPFILE_NAME = "rtp_capture.raw"
|
24
|
+
|
25
|
+
# Maximum number of bytes to receive on the socket.
|
26
|
+
MAX_BYTES_TO_RECEIVE = 1500
|
27
|
+
|
28
|
+
# Maximum times to retry using the next greatest port number.
|
29
|
+
MAX_PORT_NUMBER_RETRIES = 50
|
30
|
+
|
31
|
+
# @param [File] rtp_file The file to capture the RTP data to.
|
32
|
+
# @return [File]
|
33
|
+
attr_accessor :rtp_file
|
34
|
+
|
35
|
+
# @param [Boolean] strip_headers True if you want to strip the RTP headers.
|
36
|
+
attr_accessor :strip_headers
|
37
|
+
|
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
|
44
|
+
|
45
|
+
# @param [Symbol] transport_protocol +:UDP+ or +:TCP+.
|
46
|
+
# @return [Symbol]
|
47
|
+
attr_accessor :transport_protocol
|
48
|
+
|
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 = []
|
62
|
+
@listener = nil
|
63
|
+
@sequence_list = []
|
64
|
+
@payload_data = []
|
65
|
+
@strip_headers = false
|
66
|
+
end
|
67
|
+
|
68
|
+
# Initializes a server of the correct socket type.
|
69
|
+
#
|
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
|
81
|
+
|
82
|
+
server
|
83
|
+
end
|
84
|
+
|
85
|
+
# Simply calls #start_listener.
|
86
|
+
def run
|
87
|
+
RTP.log "Starting #{self.class} on port #{@rtp_port}..."
|
88
|
+
start_listener
|
89
|
+
end
|
90
|
+
|
91
|
+
|
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?
|
98
|
+
|
99
|
+
@listener = Thread.start do
|
100
|
+
server = init_server(@transport_protocol, @rtp_port)
|
101
|
+
|
102
|
+
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.
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
@listener.abort_on_exception = true
|
125
|
+
end
|
126
|
+
|
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]
|
131
|
+
end
|
132
|
+
|
133
|
+
@sequence_list.clear
|
134
|
+
@payload_data.clear
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Boolean] true if the +@listener+ thread is running; false if not.
|
138
|
+
def listening?
|
139
|
+
!@listener.nil? ? @listener.alive? : false
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns if the #run loop is in action.
|
143
|
+
#
|
144
|
+
# @return [Boolean] true if the run loop is running.
|
145
|
+
def running?
|
146
|
+
listening?
|
147
|
+
end
|
148
|
+
|
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?}"
|
156
|
+
end
|
157
|
+
|
158
|
+
# Kills the +@listener+ thread and sets the variable to nil.
|
159
|
+
def stop_listener
|
160
|
+
#@listener.kill if @listener
|
161
|
+
@listener.kill if listening?
|
162
|
+
@listener = nil
|
163
|
+
end
|
164
|
+
|
165
|
+
# Sets up to receive data on a UDP socket, using +@rtp_port+.
|
166
|
+
#
|
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
|
171
|
+
|
172
|
+
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
|
185
|
+
end
|
186
|
+
|
187
|
+
@rtp_port = port
|
188
|
+
RTP.log "UDP server setup to receive on port #{@rtp_port}"
|
189
|
+
|
190
|
+
server
|
191
|
+
end
|
192
|
+
|
193
|
+
# Sets up to receive data on a TCP socket, using +@rtp_port+.
|
194
|
+
#
|
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
|
199
|
+
|
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
|
213
|
+
|
214
|
+
@rtp_port = port
|
215
|
+
RTP.log "TCP server setup to receive on port #{@rtp_port}"
|
216
|
+
|
217
|
+
server
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
data/lib/rtp/version.rb
ADDED
data/rtp.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require 'rtp/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "rtp"
|
6
|
+
s.version = RTP::VERSION
|
7
|
+
s.authors = ["Steve Loveless", "Sujin Philip"]
|
8
|
+
s.homepage = %q(http://github.com/turboladen/rtp)
|
9
|
+
s.email = %w(steve.loveless@gmail.com)
|
10
|
+
s.description = %q(This is a pure Ruby implementation of RTP, initially geared
|
11
|
+
towards use with RTSP (but not limited to).)
|
12
|
+
s.summary = %q(Pure Ruby implementation of RTP)
|
13
|
+
|
14
|
+
s.required_rubygems_version = ">=1.8.0"
|
15
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.9.2")
|
16
|
+
s.files = Dir.glob("{lib,spec,tasks}/**/*") + Dir.glob("*.rdoc") +
|
17
|
+
%w(.gemtest Gemfile rtp.gemspec Rakefile)
|
18
|
+
s.test_files = Dir.glob("spec/**/*")
|
19
|
+
s.require_paths = %w(lib)
|
20
|
+
|
21
|
+
s.add_dependency("bindata", "~> 1.4")
|
22
|
+
s.add_dependency("log_switch", ">=0.2.0")
|
23
|
+
|
24
|
+
s.add_development_dependency("bundler", ">= 0")
|
25
|
+
s.add_development_dependency("rake", ">= 0")
|
26
|
+
s.add_development_dependency("rspec", "~> 2.7")
|
27
|
+
s.add_development_dependency("simplecov", ">= 0")
|
28
|
+
s.add_development_dependency("yard", ">= 0.7.2")
|
29
|
+
end
|
@@ -0,0 +1,368 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'rtp/receiver'
|
3
|
+
|
4
|
+
Thread.abort_on_exception = true
|
5
|
+
RTP.log = false
|
6
|
+
|
7
|
+
describe RTP::Receiver do
|
8
|
+
describe "#initialize" do
|
9
|
+
context "with default parameters" do
|
10
|
+
it "uses UDP" do
|
11
|
+
subject.instance_variable_get(:@transport_protocol).should == :UDP
|
12
|
+
end
|
13
|
+
|
14
|
+
it "uses port 9000" do
|
15
|
+
subject.instance_variable_get(:@rtp_port).should == 9000
|
16
|
+
end
|
17
|
+
|
18
|
+
it "creates a new Tempfile" do
|
19
|
+
subject.instance_variable_get(:@rtp_file).should be_a Tempfile
|
20
|
+
end
|
21
|
+
|
22
|
+
it "initializes @packet_timestamps" do
|
23
|
+
subject.instance_variable_get(:@packet_timestamps).should == []
|
24
|
+
end
|
25
|
+
|
26
|
+
it "initializes an Array for holding the data buffer" do
|
27
|
+
subject.instance_variable_get(:@payload_data).should be_a Array
|
28
|
+
end
|
29
|
+
|
30
|
+
it "initializes an Array for holding the sequence_list" do
|
31
|
+
subject.instance_variable_get(:@sequence_list).should be_a Array
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "non-default parameters" do
|
36
|
+
it "can use TCP" do
|
37
|
+
RTP::Receiver.new(:TCP).instance_variable_get(:@transport_protocol).should == :TCP
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can take another port" do
|
41
|
+
RTP::Receiver.new(:UDP, 12345).instance_variable_get(:@rtp_port).should == 12345
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can take an IO object" do
|
45
|
+
fd = IO.sysopen("/dev/null", "w")
|
46
|
+
io = IO.new(fd, 'w')
|
47
|
+
capturer = RTP::Receiver.new(:UDP, 12345, io)
|
48
|
+
capturer.instance_variable_get(:@rtp_file).should be_a IO
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "isn't running" do
|
53
|
+
subject.should_not be_running
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#init_server" do
|
58
|
+
context "UDP" do
|
59
|
+
it "calls #init_udp_server with port 9000" do
|
60
|
+
subject.should_receive(:init_udp_server).with(9000)
|
61
|
+
subject.init_server(:UDP)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns a UDPSocket" do
|
65
|
+
subject.init_server(:UDP).should be_a UDPSocket
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "TCP" do
|
70
|
+
it "calls #init_tcp_server with port 9000" do
|
71
|
+
subject.should_receive(:init_tcp_server).with(9000)
|
72
|
+
subject.init_server(:TCP)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns a TCPServer" do
|
76
|
+
subject.init_server(:TCP).should be_a(TCPServer)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it "raises an RTP::Error when some other protocol is given" do
|
81
|
+
expect { subject.init_server(:BOBO) }.to raise_error RTP::Error
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#init_udp_server" do
|
86
|
+
let(:udp_server) do
|
87
|
+
double "UDPSocket", setsockopt: nil
|
88
|
+
end
|
89
|
+
|
90
|
+
it "returns a UDPSocket" do
|
91
|
+
subject.init_udp_server(subject.rtp_port).should be_a UDPSocket
|
92
|
+
end
|
93
|
+
|
94
|
+
context "when port 9000 - 9048 are taken" do
|
95
|
+
it "retries MAX_PORT_NUMBER_RETRIES times then returns the UDPSocket" do
|
96
|
+
udp_server.should_receive(:bind).exactly(50).times.and_raise(Errno::EADDRINUSE)
|
97
|
+
udp_server.should_receive(:bind).with('0.0.0.0', 9050)
|
98
|
+
UDPSocket.stub(:open).and_return(udp_server)
|
99
|
+
|
100
|
+
subject.init_udp_server(9000).should == udp_server
|
101
|
+
|
102
|
+
UDPSocket.unstub(:open)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "when no available ports" do
|
107
|
+
before do
|
108
|
+
UDPSocket.should_receive(:open).exactly(51).times.and_raise(Errno::EADDRINUSE)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "retries 50 times to get a port then allows the Errno::EADDRINUSE to raise" do
|
112
|
+
expect { subject.init_udp_server(9000) }.to raise_error Errno::EADDRINUSE
|
113
|
+
end
|
114
|
+
|
115
|
+
it "sets @rtp_port back to 9000 after trying all" do
|
116
|
+
expect { subject.init_udp_server(9000) }.to raise_error Errno::EADDRINUSE
|
117
|
+
subject.rtp_port.should == 9000
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "#init_tcp_server" do
|
123
|
+
it "returns a TCPSocket" do
|
124
|
+
subject.init_tcp_server(3456).should be_a TCPSocket
|
125
|
+
end
|
126
|
+
|
127
|
+
it "uses port a port between 9000 and 9000 + MAX_PORT_NUMBER_RETRIES" do
|
128
|
+
subject.init_tcp_server(9000)
|
129
|
+
subject.rtp_port.should >= 9000
|
130
|
+
subject.rtp_port.should <= 9000 + RTP::Receiver::MAX_PORT_NUMBER_RETRIES
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#run" do
|
135
|
+
it "calls #start_listener" do
|
136
|
+
subject.should_receive(:start_listener)
|
137
|
+
subject.run
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "#running?" do
|
142
|
+
context "#listening? returns true" do
|
143
|
+
before { subject.stub(:listening?).and_return(true) }
|
144
|
+
it { should be_true }
|
145
|
+
end
|
146
|
+
|
147
|
+
context "#listening? returns true, #file_building? returns true" do
|
148
|
+
before do
|
149
|
+
subject.stub(:listening? => true, :file_building? => true)
|
150
|
+
it { should be_true }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "#listening? returns true, #file_building? returns false" do
|
155
|
+
before do
|
156
|
+
subject.stub(:listening? => true, :file_building? => false)
|
157
|
+
it { should be_false }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "#listening? returns false, #file_building? returns false" do
|
162
|
+
before do
|
163
|
+
subject.stub(:listening? => false, :file_building? => false)
|
164
|
+
it { should be_false }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "#stop" do
|
170
|
+
it "calls #stop_listener" do
|
171
|
+
subject.should_receive(:stop_listener)
|
172
|
+
subject.stop
|
173
|
+
end
|
174
|
+
|
175
|
+
it "calls #write_buffer_to_file" do
|
176
|
+
subject.should_receive(:write_buffer_to_file)
|
177
|
+
subject.stop
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe "#start_listener" do
|
182
|
+
let(:listener) do
|
183
|
+
l = double "@listener"
|
184
|
+
l.stub(:abort_on_exception=)
|
185
|
+
|
186
|
+
l
|
187
|
+
end
|
188
|
+
|
189
|
+
context "#listening? is true" do
|
190
|
+
before { subject.stub(:listening?).and_return true }
|
191
|
+
|
192
|
+
it "returns @listener" do
|
193
|
+
subject.instance_variable_set(:@listener, listener)
|
194
|
+
subject.start_listener.should equal listener
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "#listening? is false" do
|
199
|
+
before { subject.stub(:listening?).and_return false }
|
200
|
+
|
201
|
+
it "starts a new Thread and assigns that to @listener" do
|
202
|
+
Thread.should_receive(:start).and_return listener
|
203
|
+
subject.start_listener
|
204
|
+
subject.instance_variable_get(:@listener).should equal listener
|
205
|
+
end
|
206
|
+
|
207
|
+
it "initializes the server socket" do
|
208
|
+
subject.instance_variable_set(:@listener, listener)
|
209
|
+
Thread.stub(:start).and_yield.and_return listener
|
210
|
+
subject.should_receive(:init_server)
|
211
|
+
subject.stub(:loop)
|
212
|
+
|
213
|
+
subject.start_listener
|
214
|
+
|
215
|
+
Thread.unstub(:start)
|
216
|
+
end
|
217
|
+
|
218
|
+
let!(:data) do
|
219
|
+
d = double "data"
|
220
|
+
d.stub(:size)
|
221
|
+
|
222
|
+
d
|
223
|
+
end
|
224
|
+
|
225
|
+
let!(:timestamp) { double "timestamp" }
|
226
|
+
|
227
|
+
let(:message) do
|
228
|
+
m = double "msg"
|
229
|
+
m.stub(:first).and_return data
|
230
|
+
m.stub_chain(:last, :timestamp).and_return timestamp
|
231
|
+
|
232
|
+
m
|
233
|
+
end
|
234
|
+
|
235
|
+
let(:server) do
|
236
|
+
double "Server"
|
237
|
+
end
|
238
|
+
|
239
|
+
it "receives data from the client and hands it to RTP::Packet to read" do
|
240
|
+
subject.instance_variable_set(:@listener, listener)
|
241
|
+
Thread.stub(:start).and_yield.and_return listener
|
242
|
+
server.should_receive(:recvmsg_nonblock).with(1500).and_return message
|
243
|
+
subject.should_receive(:init_server).and_return server
|
244
|
+
packet = double "RTP::Packet"
|
245
|
+
packet.stub_chain(:[], :size)
|
246
|
+
packet.stub_chain(:[], :to_i).and_return(10)
|
247
|
+
RTP::Packet.should_receive(:read).with(data).and_return packet
|
248
|
+
subject.stub(:write_buffer_to_file)
|
249
|
+
subject.stub(:loop).and_yield
|
250
|
+
|
251
|
+
subject.start_listener
|
252
|
+
|
253
|
+
Thread.unstub(:start)
|
254
|
+
end
|
255
|
+
|
256
|
+
it "extracts the timestamp of the received data and adds it to @packet_timestamps" do
|
257
|
+
pending
|
258
|
+
end
|
259
|
+
|
260
|
+
context "@strip_headers is false" do
|
261
|
+
it "adds the incoming data to @payload_data buffer" do
|
262
|
+
subject.instance_variable_set(:@listener, listener)
|
263
|
+
Thread.stub(:start).and_yield.and_return listener
|
264
|
+
server.should_receive(:recvmsg_nonblock).with(1500).and_return message
|
265
|
+
subject.should_receive(:init_server).and_return server
|
266
|
+
packet = double "RTP::Packet"
|
267
|
+
packet.stub_chain(:[], :size)
|
268
|
+
packet.stub_chain(:[], :to_i).and_return(0)
|
269
|
+
RTP::Packet.stub(:read).and_return packet
|
270
|
+
subject.stub(:write_buffer_to_file)
|
271
|
+
subject.stub(:loop).and_yield
|
272
|
+
|
273
|
+
subject.start_listener
|
274
|
+
subject.instance_variable_get(:@payload_data).should == [data]
|
275
|
+
Thread.unstub(:start)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
context "@strip_headers is true" do
|
280
|
+
it "adds the stripped data to @payload_data buffer" do
|
281
|
+
subject.instance_variable_set(:@listener, listener)
|
282
|
+
subject.instance_variable_set(:@strip_headers, true)
|
283
|
+
Thread.stub(:start).and_yield.and_return listener
|
284
|
+
server.should_receive(:recvmsg_nonblock).with(1500).and_return message
|
285
|
+
subject.should_receive(:init_server).and_return server
|
286
|
+
packet = double "RTP::Packet"
|
287
|
+
packet.should_receive(:[]).with("rtp_payload").twice.and_return("payload_data")
|
288
|
+
packet.should_receive(:[]).with("sequence_number").exactly(3).times.and_return("0")
|
289
|
+
RTP::Packet.stub(:read).and_return packet
|
290
|
+
subject.stub(:write_buffer_to_file)
|
291
|
+
subject.stub(:loop).and_yield
|
292
|
+
|
293
|
+
subject.start_listener
|
294
|
+
subject.instance_variable_get(:@payload_data).should == ["payload_data"]
|
295
|
+
Thread.unstub(:start)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
describe "#write_buffer_to_file" do
|
302
|
+
before do
|
303
|
+
sequence_list = [54322, 54323, 54320, 54324, 54325, 54321]
|
304
|
+
data = []
|
305
|
+
|
306
|
+
sequence_list.each do |sequence|
|
307
|
+
data[sequence] = "data#{sequence}"
|
308
|
+
end
|
309
|
+
|
310
|
+
subject.instance_variable_set(:@sequence_list, sequence_list)
|
311
|
+
subject.instance_variable_set(:@payload_data, data)
|
312
|
+
end
|
313
|
+
|
314
|
+
it "sorts the buffer and writes to file" do
|
315
|
+
output = StringIO.new
|
316
|
+
expected_output = "data54320data54321data54322data54323data54324data54325"
|
317
|
+
subject.instance_variable_set(:@rtp_file, output)
|
318
|
+
subject.write_buffer_to_file
|
319
|
+
output.string.should == expected_output
|
320
|
+
end
|
321
|
+
|
322
|
+
it "clears the sequence list after writing to file" do
|
323
|
+
output = StringIO.new
|
324
|
+
subject.instance_variable_set(:@rtp_file, output)
|
325
|
+
subject.write_buffer_to_file
|
326
|
+
subject.instance_variable_get(:@sequence_list).should == []
|
327
|
+
end
|
328
|
+
|
329
|
+
it "clears the data buffer after writing to file" do
|
330
|
+
output = StringIO.new
|
331
|
+
subject.instance_variable_set(:@rtp_file, output)
|
332
|
+
subject.write_buffer_to_file
|
333
|
+
subject.instance_variable_get(:@payload_data).should == []
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
describe "#stop_listener" do
|
338
|
+
let(:listener) { double "@listener" }
|
339
|
+
|
340
|
+
it "sets @listener to nil" do
|
341
|
+
local_listener = "test"
|
342
|
+
subject.stub(:listening?).and_return false
|
343
|
+
subject.instance_variable_set(:@listener, local_listener)
|
344
|
+
subject.stop_listener
|
345
|
+
subject.instance_variable_get(:@listener)
|
346
|
+
end
|
347
|
+
|
348
|
+
context "#listeining? is false" do
|
349
|
+
before { subject.stub(:listening?).and_return false }
|
350
|
+
|
351
|
+
it "doesn't get #kill called on it" do
|
352
|
+
listener.should_not_receive(:kill)
|
353
|
+
subject.instance_variable_set(:@listener, listener)
|
354
|
+
subject.stop_listener
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
context "#listening? is true" do
|
359
|
+
before { subject.stub(:listening?).and_return true }
|
360
|
+
|
361
|
+
it "gets killed" do
|
362
|
+
listener.should_receive(:kill)
|
363
|
+
subject.instance_variable_set(:@listener, listener)
|
364
|
+
subject.stop_listener
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
data/spec/rtp_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require 'rtp'
|
3
|
+
|
4
|
+
describe Kernel do
|
5
|
+
def self.get_requires
|
6
|
+
Dir[File.dirname(__FILE__) + '/../lib/rtp/**/*.rb']
|
7
|
+
end
|
8
|
+
|
9
|
+
# Try to require each of the files in RTP
|
10
|
+
get_requires.each do |r|
|
11
|
+
it "should require #{r}" do
|
12
|
+
|
13
|
+
# A require returns true if it was required, false if it had already been
|
14
|
+
# required, and nil if it couldn't require.
|
15
|
+
Kernel.require(r.to_s).should_not be_nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe RTP do
|
21
|
+
it "should have a VERSION constant" do
|
22
|
+
RTP.const_defined?('VERSION').should be_true
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
#require 'simplecov-rcov-text'
|
3
|
+
|
4
|
+
class SimpleCov::Formatter::MergedFormatter
|
5
|
+
def format(result)
|
6
|
+
SimpleCov::Formatter::HTMLFormatter.new.format(result)
|
7
|
+
# SimpleCov::Formatter::RcovTextFormatter.new.format(result)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::MergedFormatter
|
12
|
+
|
13
|
+
SimpleCov.start do
|
14
|
+
add_filter "/spec"
|
15
|
+
add_filter "/lib/deps"
|
16
|
+
end
|
17
|
+
|
18
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
19
|
+
require 'rtp'
|
20
|
+
require 'stringio'
|
data/tasks/roodi.rake
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
---
|
2
|
+
AssignmentInConditionalCheck: { }
|
3
|
+
CaseMissingElseCheck: { }
|
4
|
+
ClassLineCountCheck: { line_count: 300 }
|
5
|
+
ClassNameCheck: { pattern: !ruby/regexp '/^[A-Z][a-zA-Z0-9]*$/' }
|
6
|
+
CyclomaticComplexityBlockCheck: { complexity: 4 }
|
7
|
+
CyclomaticComplexityMethodCheck: { complexity: 8 }
|
8
|
+
EmptyRescueBodyCheck: { }
|
9
|
+
ForLoopCheck: { }
|
10
|
+
MethodLineCountCheck: { line_count: 30 }
|
11
|
+
MethodNameCheck: { pattern: !ruby/regexp '/^[_a-z<>=\[\]|+-\/\*`]+[_a-z0-9_<>=~@\[\]]*[=!\?]?$/' }
|
12
|
+
ModuleLineCountCheck: { line_count: 300 }
|
13
|
+
ModuleNameCheck: { pattern: !ruby/regexp '/^[A-Z][a-zA-Z0-9]*$/' }
|
14
|
+
ParameterNumberCheck: { parameter_count: 5 }
|
data/tasks/rspec.rake
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
|
3
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
4
|
+
t.ruby_opts = %w(-w)
|
5
|
+
t.rspec_opts = %w(--format documentation --color)
|
6
|
+
end
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec_html) do |t|
|
9
|
+
t.rspec_opts = %w(--format html --out rspec_output.html)
|
10
|
+
end
|
data/tasks/stats.rake
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'code_statistics'
|
2
|
+
|
3
|
+
STATS_DIRECTORIES = [
|
4
|
+
%w(Library lib),
|
5
|
+
%w(Behavior\ tests features/step_definitions),
|
6
|
+
%w(Unit\ tests spec)
|
7
|
+
].collect { |name, dir| [ name, "#{dir}" ] }.select { |name, dir| File.directory?(dir) }
|
8
|
+
|
9
|
+
desc "Report code statistics (KLOCs, etc) from the application"
|
10
|
+
task :stats do
|
11
|
+
CodeStatistics::CodeStatistics.new(*STATS_DIRECTORIES).to_s
|
12
|
+
end
|
data/tasks/yard.rake
ADDED
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rtp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Steve Loveless
|
9
|
+
- Sujin Philip
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-03-02 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: bindata
|
17
|
+
requirement: &70231393476520 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '1.4'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70231393476520
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: log_switch
|
28
|
+
requirement: &70231393472440 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.2.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70231393472440
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: bundler
|
39
|
+
requirement: &70231393468300 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70231393468300
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: rake
|
50
|
+
requirement: &70231393465820 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *70231393465820
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: rspec
|
61
|
+
requirement: &70231393463540 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.7'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *70231393463540
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: simplecov
|
72
|
+
requirement: &70231393457440 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *70231393457440
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: yard
|
83
|
+
requirement: &70231393454320 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ! '>='
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.7.2
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: *70231393454320
|
92
|
+
description: ! "This is a pure Ruby implementation of RTP, initially geared\n towards
|
93
|
+
use with RTSP (but not limited to)."
|
94
|
+
email:
|
95
|
+
- steve.loveless@gmail.com
|
96
|
+
executables: []
|
97
|
+
extensions: []
|
98
|
+
extra_rdoc_files: []
|
99
|
+
files:
|
100
|
+
- lib/rtp/error.rb
|
101
|
+
- lib/rtp/packet.rb
|
102
|
+
- lib/rtp/receiver.rb
|
103
|
+
- lib/rtp/version.rb
|
104
|
+
- lib/rtp.rb
|
105
|
+
- spec/rtp/receiver_spec.rb
|
106
|
+
- spec/rtp_spec.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- tasks/roodi.rake
|
109
|
+
- tasks/roodi_config.yaml
|
110
|
+
- tasks/rspec.rake
|
111
|
+
- tasks/stats.rake
|
112
|
+
- tasks/yard.rake
|
113
|
+
- History.rdoc
|
114
|
+
- README.rdoc
|
115
|
+
- .gemtest
|
116
|
+
- Gemfile
|
117
|
+
- rtp.gemspec
|
118
|
+
- Rakefile
|
119
|
+
homepage: http://github.com/turboladen/rtp
|
120
|
+
licenses: []
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 1.9.2
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: 1.8.0
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.17
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: Pure Ruby implementation of RTP
|
143
|
+
test_files:
|
144
|
+
- spec/rtp/receiver_spec.rb
|
145
|
+
- spec/rtp_spec.rb
|
146
|
+
- spec/spec_helper.rb
|
147
|
+
has_rdoc:
|