rtp 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/History.rdoc CHANGED
@@ -1,3 +1,10 @@
1
+ === 0.1.4 / 2012-12-06
2
+
3
+ === Bug Fixes:
4
+
5
+ * gh-5: If Receiver#start wasn't given a block, it wouldn't write to the capture
6
+ file.
7
+
1
8
  === 0.1.3 / 2012-11-21
2
9
 
3
10
  ==== Improvements:
data/lib/rtp/receiver.rb CHANGED
@@ -184,7 +184,7 @@ module RTP
184
184
  # @yield [Time] The timestamp from the packet as it was received on the
185
185
  # socket.
186
186
  # @return [Thread] The packet writer thread.
187
- def start_packet_writer
187
+ def start_packet_writer(&block)
188
188
  return @packet_writer if @packet_writer
189
189
 
190
190
  # If a block is given for packet inspection, perhaps we should save
@@ -196,7 +196,7 @@ module RTP
196
196
 
197
197
  data_to_write = @strip_headers ? packet.rtp_payload : packet
198
198
 
199
- if block_given?
199
+ if block
200
200
  yield data_to_write, timestamp
201
201
  else
202
202
  @capture_file.write(data_to_write)
data/lib/rtp/sender.rb ADDED
@@ -0,0 +1,37 @@
1
+ require_relative 'senders/socat'
2
+ require 'singleton'
3
+
4
+
5
+ module RTP
6
+ class Sender
7
+ include Singleton
8
+
9
+ def initialize
10
+ @stream_module = RTP::Senders::Socat
11
+ @sessions = {}
12
+ @pids = {}
13
+ @rtcp_threads = {}
14
+ @rtp_timestamp = 2612015746
15
+ @rtp_sequence = 21934
16
+ @rtp_map = []
17
+ @fmtp = []
18
+ @source_ip = []
19
+ @source_port = []
20
+ end
21
+
22
+ # Sets the stream module to be used by the stream server.
23
+ #
24
+ # @param [Module] module_name Module name.
25
+ def stream_module=(module_name)
26
+ @stream_module = module_name
27
+ self.class.send(:include, module_name)
28
+ end
29
+
30
+ # Gets the current stream_module.
31
+ #
32
+ # @return [Module] Module name.
33
+ def stream_module
34
+ @stream_module
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,283 @@
1
+ require 'os'
2
+ require 'ipaddr'
3
+ require_relative '../packet'
4
+ require 'sys/proctable'
5
+
6
+
7
+ module RTP
8
+ module Senders
9
+ module Socat
10
+ RTCP_SOURCE = ["80c80006072dee6ad42c300f76c3b928377e99e5006c461ba92d8a3" +
11
+ "081ca0006072dee6a010e49583330444e2d41414a4248513600000000"]
12
+ MP4_RTP_MAP = "96 MP4V-ES/30000"
13
+ MP4_FMTP = "96 profile-level-id=5;config=000001b005000001b50900000100000" +
14
+ "0012000c888ba9860fa22c087828307"
15
+ H264_RTP_MAP = "96 H264/90000"
16
+ H264_FMTP = "96 packetization-mode=1;profile-level-id=428032;" +
17
+ "sprop-parameter-sets=Z0KAMtoAgAMEwAQAAjKAAAr8gYAAAYhMAABMS0IvfjAA" +
18
+ "ADEJgAAJiWhF78CA,aM48gA=="
19
+ SOCAT_OPTIONS = "rcvbuf=2500000,sndbuf=2500000,sndtimeo=0.00001,rcvtimeo=0.00001"
20
+ BLOCK_SIZE = 2000
21
+ BSD_OPTIONS = "setsockopt-int=0xffff:0x200:0x01"
22
+
23
+ # @return [Hash] Hash of session IDs and SOCAT commands.
24
+ attr_accessor :sessions
25
+
26
+ # @return [Hash] Hash of session IDs and pids.
27
+ attr_reader :pids
28
+
29
+ # @return [Hash] Hash of session IDs and RTCP threads.
30
+ attr_reader :rtcp_threads
31
+
32
+ # @return [Array<String>] IP address of the source camera.
33
+ attr_accessor :source_ip
34
+
35
+ # @return [Array<Fixnum>] Port where the source camera is streaming.
36
+ attr_accessor :source_port
37
+
38
+ # @return [String] IP address of the interface of the RTSP streamer.
39
+ attr_accessor :interface_ip
40
+
41
+ # @return [Fixnum] RTP timestamp of the source stream.
42
+ attr_accessor :rtp_timestamp
43
+
44
+ # @return [Fixnum] RTP sequence number of the source stream.
45
+ attr_accessor :rtp_sequence
46
+
47
+ # @return [String] RTCP source identifier.
48
+ attr_accessor :rtcp_source_identifier
49
+
50
+ # @return [Array<String>] Media type attributes.
51
+ attr_accessor :rtp_map
52
+
53
+ # @return [Array<String>] Media format attributes.
54
+ attr_accessor :fmtp
55
+
56
+ # Generates a RTCP source ID based on the friendly name.
57
+ # This ID is used in the RTCP communication with the client.
58
+ # The default +RTCP_SOURCE+ will be used if one is not provided.
59
+ #
60
+ # @param [String] friendly_name Name to be used in the RTCP source ID.
61
+ # @return [String] rtcp_source_id RTCP Source ID.
62
+ #def generate_rtcp_source_id friendly_name
63
+ # ["80c80006072dee6ad42c300f76c3b928377e99e5006c461ba92d8a3081ca0006072dee6a010e" +
64
+ # friendly_name.unpack("H*").first + "00000000"].pack("H*")
65
+ #end
66
+
67
+ # Creates a RTP streamer using socat.
68
+ #
69
+ # @param [String] sid Session ID.
70
+ # @param [String] transport_url Destination IP:port.
71
+ # @param [Fixnum] index Stream index.
72
+ # @return [Fixnum] The port the streamer will stream on.
73
+ def setup_streamer(sid, transport_url, index=1)
74
+ dest_ip, dest_port = transport_url.split ":"
75
+ @rtcp_source_identifier ||= RTCP_SOURCE.pack("H*")
76
+
77
+ @rtcp_threads[sid] = Thread.start do
78
+ s = UDPSocket.new
79
+ s.bind(@interface_ip, 0)
80
+
81
+ loop do
82
+ begin
83
+ _, sender = s.recvfrom(36)
84
+ s.send(@rtcp_source_identifier, 0, sender[3], sender[1])
85
+ end
86
+ end
87
+ end
88
+
89
+ @cleaner ||= Thread.start { cleanup_defunct }
90
+ @processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
91
+ @sessions[sid] = build_socat(dest_ip, dest_port, local_port, index)
92
+
93
+ local_port
94
+ end
95
+
96
+ # Start streaming for the requested session.
97
+ #
98
+ # @param [String] sid Session ID.
99
+ def start_streaming sid
100
+ spawn_socat(sid, @sessions[sid])
101
+ end
102
+
103
+ # Stop streaming for the requested session.
104
+ #
105
+ # @param [String] session ID.
106
+ def stop_streaming sid
107
+ if sid.nil?
108
+ disconnect_all_streams
109
+ else
110
+ disconnect sid
111
+ @rtcp_threads[sid].kill unless rtcp_threads[sid].nil?
112
+ @rtcp_threads.delete sid
113
+ end
114
+ end
115
+
116
+ # Returns the default stream description.
117
+ #
118
+ # @param[Boolean] multicast True if the description is for a multicast stream.
119
+ # @param [Fixnum] stream_index Index of the stream type.
120
+ =begin
121
+ def description(multicast=false, stream_index=1)
122
+ rtp_map = @rtp_map[stream_index - 1] || H264_RTP_MAP
123
+ fmtp = @fmtp[stream_index - 1] || H264_FMTP
124
+
125
+ <<EOF
126
+ v=0\r
127
+ o=- 1345481255966282 1 IN IP4 #{@interface_ip}\r
128
+ s=Session streamed by "Streaming Server"\r
129
+ i=stream1\r
130
+ t=0 0\r
131
+ a=tool:LIVE555 Streaming Media v2007.07.09\r
132
+ a=type:broadcast\r
133
+ a=control:*\r
134
+ a=range:npt=0-\r
135
+ a=x-qt-text-nam:Session streamed by "Streaming Server"\r
136
+ a=x-qt-text-inf:stream1\r
137
+ m=video 0 RTP/AVP 96\r
138
+ c=IN IP4 #{multicast ? "#{multicast_ip(stream_index)}/10" : "0.0.0.0"}\r
139
+ a=rtpmap:#{rtp_map}\r
140
+ a=fmtp:#{fmtp}\r
141
+ a=control:track1\r
142
+ EOF
143
+ end
144
+ =end
145
+
146
+ # Disconnects the stream matching the session ID.
147
+ #
148
+ # @param [String] sid Session ID.
149
+ def disconnect sid
150
+ pid = @pids[sid].to_i
151
+ @pids.delete(sid)
152
+ @sessions.delete(sid)
153
+ Process.kill(9, pid) if pid > 1000
154
+ rescue Errno::ESRCH
155
+ log "Tried to kill dead process: #{pid}"
156
+ end
157
+
158
+ # Parses the headers from an RTP stream.
159
+ #
160
+ # @param [String] src_ip Multicast IP address of RTP stream.
161
+ # @param [Fixnum] src_port Port of RTP stream.
162
+ # @return [Array<Fixnum>] Sequence number and timestamp
163
+ def parse_sequence_number(src_ip, src_port)
164
+ sock = UDPSocket.new
165
+ ip = IPAddr.new(src_ip).hton + IPAddr.new("0.0.0.0").hton
166
+ sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, ip)
167
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
168
+ sock.bind(Socket::INADDR_ANY, src_port)
169
+
170
+ begin
171
+ data = sock.recv_nonblock(1500)
172
+ rescue Errno::EAGAIN
173
+ retry
174
+ end
175
+
176
+ sock.close
177
+ packet = RTP::Packet.read(data)
178
+
179
+ [packet["sequence_number"], packet["timestamp"]]
180
+ end
181
+
182
+ private
183
+
184
+ # Returns the multicast IP on which the streamer will stream.
185
+ #
186
+ # @param [Fixnum] index Stream index.
187
+ # @return [String] Multicast IP.
188
+ def multicast_ip index=1
189
+ @interface_ip ||= find_best_interface_ipaddr @source_ip[index-1]
190
+ multicast_ip = @interface_ip.split "."
191
+ multicast_ip[0] = "239"
192
+
193
+ multicast_ip.join "."
194
+ end
195
+
196
+ # Cleans up defunct child processes
197
+ def cleanup_defunct
198
+ loop do
199
+ begin
200
+ Process.wait 0
201
+ rescue Errno::ECHILD
202
+ sleep 10
203
+ retry
204
+ end
205
+ end
206
+ end
207
+
208
+ # Determine the interface address that best matches an IP address. This
209
+ # is most useful when talking to a remote computer and needing to
210
+ # determine the interface that is being used for the connection.
211
+ #
212
+ # @param [String] device_ip IP address of the remote device you want to
213
+ # talk to.
214
+ # @return [String] IP of the interface that would be used to talk to.
215
+ def find_best_interface_ipaddr device_ip
216
+ UDPSocket.open { |s| s.connect(device_ip, 1); s.addr.last }
217
+ end
218
+
219
+ # Disconnects all streams that are currently streaming.
220
+ def disconnect_all_streams
221
+ @pids.values.each { |pid| Process.kill(9, pid.to_i) if pid.to_i > 1000 }
222
+ @sessions.clear
223
+ @pids.clear
224
+ end
225
+
226
+ # Spawns an instance of Socat.
227
+ #
228
+ # @param [String] sid The session ID of the stream.
229
+ # @param [String] command The SOCAT command to be spawned.
230
+ def spawn_socat(sid, command)
231
+ @processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
232
+
233
+ if command.nil?
234
+ log("SOCAT command for #{sid} was nil", :warn)
235
+ return
236
+ end
237
+
238
+ if @processes.include?(command)
239
+ pid = get_pid(command)
240
+ log "Streamer already running with pid #{pid}" if pid.is_a? Fixnum
241
+ else
242
+ @sessions[sid] = command
243
+
244
+ Thread.start do
245
+ log "Running stream spawner: #{command}"
246
+ @processes << command
247
+ pid = spawn command
248
+ @pids[sid] = pid
249
+ Thread.start { sleep 20; spawn_socat(sid, command) }
250
+ end
251
+ end
252
+ end
253
+
254
+ # Builds a socat stream command based on the source and target
255
+ # IP and ports of the RTP stream.
256
+ #
257
+ # @param [String] target_ip IP address of the remote device you want to
258
+ # talk to.
259
+ # @param [Fixnum] target_port Port on the remote device you want to
260
+ # talk to.
261
+ # @return [String] IP of the interface that would be used to talk to.
262
+ def build_socat(target_ip, target_port, server_port, index=1)
263
+ bsd_options = BSD_OPTIONS if OS.mac?
264
+ bsd_options ||= ""
265
+
266
+ "socat -b #{BLOCK_SIZE} UDP-RECV:#{@source_port[index-1]},reuseaddr," +
267
+ "#{bsd_options}"+ SOCAT_OPTIONS + ",ip-add-membership=#{@source_ip[index-1]}:" +
268
+ "#{@interface_ip} UDP:#{target_ip}:#{target_port},sourceport=#{server_port}," +
269
+ SOCAT_OPTIONS
270
+ end
271
+
272
+ # Gets the pid for a SOCAT command.
273
+ #
274
+ # @param [String] cmd SOCAT command
275
+ # @return [Fixnum] PID of the process.
276
+ def get_pid cmd
277
+ Sys::ProcTable.ps.each do |p|
278
+ return p.pid.to_i if p.cmdline.include? cmd
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
data/lib/rtp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module RTP
2
- VERSION = '0.1.3'
2
+ VERSION = '0.1.4'
3
3
  end
@@ -14,7 +14,7 @@
14
14
  <img src="./assets/0.7.1/loading.gif" alt="loading"/>
15
15
  </div>
16
16
  <div id="wrapper" style="display:none;">
17
- <div class="timestamp">Generated <abbr class="timeago" title="2012-11-20T10:56:32-08:00">2012-11-20T10:56:32-08:00</abbr></div>
17
+ <div class="timestamp">Generated <abbr class="timeago" title="2012-12-06T10:53:51-08:00">2012-12-06T10:53:51-08:00</abbr></div>
18
18
  <ul class="group_tabs"></ul>
19
19
 
20
20
  <div id="content">
@@ -150,16 +150,21 @@ describe RTP::Receiver do
150
150
  describe "#start_packet_writer" do
151
151
  context "packet writer running" do
152
152
  let(:packet) { double "RTP::Packet" }
153
+ let(:msg) { "the data" }
154
+ let(:timestamp) { "12345" }
153
155
 
154
- before do
155
- Thread.stub(:start).and_yield
156
- subject.stub(:loop).and_yield
157
- subject.instance_variable_set(:@packets, [packet])
158
- RTP::Packet.should_receive(:read).and_return packet
156
+ let(:packets) do
157
+ p = double("Queue")
158
+ p.should_receive(:pop).and_return [msg, timestamp]
159
+
160
+ p
159
161
  end
160
162
 
161
- after do
162
- Thread.unstub(:start)
163
+ before do
164
+ Thread.should_receive(:start).and_yield
165
+ subject.should_receive(:loop).and_yield
166
+ RTP::Packet.should_receive(:read).with(msg).and_return packet
167
+ subject.instance_variable_set(:@packets, packets)
163
168
  end
164
169
 
165
170
  context "@strip_headers is false" do
@@ -183,6 +188,40 @@ describe RTP::Receiver do
183
188
  subject.send(:start_packet_writer)
184
189
  end
185
190
  end
191
+
192
+ context "block is given" do
193
+ it "yields the data and its timestamp" do
194
+ expect { |block|
195
+ subject.send(:start_packet_writer, &block)
196
+ }.to yield_with_args packet, timestamp
197
+ end
198
+ end
199
+
200
+ context "no block given" do
201
+ let(:capture_file) do
202
+ c = double "@capture_file"
203
+ c.stub(:closed?)
204
+
205
+ c
206
+ end
207
+
208
+ before { RTP::Receiver.any_instance.instance_variable_set(:@capture_file, capture_file) }
209
+
210
+ it "writes to the capture file" do
211
+ subject.instance_variable_get(:@capture_file).should_receive(:write).
212
+ with(packet)
213
+
214
+ subject.send(:start_packet_writer)
215
+ end
216
+
217
+ it "adds timestamps to @timestamps" do
218
+ subject.instance_variable_get(:@capture_file).stub(:write)
219
+ subject.instance_variable_get(:@packet_timestamps).
220
+ should_receive(:<<).with(timestamp)
221
+
222
+ subject.send(:start_packet_writer)
223
+ end
224
+ end
186
225
  end
187
226
 
188
227
  context "packet writer not running" do
@@ -194,6 +233,7 @@ describe RTP::Receiver do
194
233
 
195
234
  specify { subject.send(:start_packet_writer).should == packet_writer }
196
235
  end
236
+
197
237
  end
198
238
 
199
239
  describe "#init_socket" do
@@ -275,26 +315,10 @@ describe RTP::Receiver do
275
315
  l
276
316
  end
277
317
 
278
- let(:data) do
279
- d = double "data"
280
- d.stub(:size)
281
-
282
- d
283
- end
284
-
285
- let(:timestamp) { double "timestamp" }
286
-
287
- let(:message) do
288
- m = double "msg"
289
- m.stub(:first).and_return data
290
- m.stub_chain(:last, :timestamp).and_return timestamp
291
-
292
- m
293
- end
294
-
295
- let(:socket) do
296
- double "Socket", recvmsg: message
297
- end
318
+ let(:data) { double "socket data", size: 10 }
319
+ let(:socket_info) { double "socket info", timestamp: '12345' }
320
+ let(:message) { [data, socket_info] }
321
+ let(:socket) { double "Socket", recvmsg: message }
298
322
 
299
323
  it "starts a new Thread and returns that" do
300
324
  Thread.should_receive(:start).with(socket).and_return listener
@@ -312,8 +336,13 @@ describe RTP::Receiver do
312
336
  Thread.unstub(:start)
313
337
  end
314
338
 
315
- it "extracts the timestamp of the received data and adds it to @packet_timestamps" do
316
- pending
339
+ it "adds the socket data and timestamp to @packets" do
340
+ Thread.stub(:start).and_yield
341
+ subject.stub(:loop).and_yield
342
+
343
+ subject.instance_variable_get(:@packets).should_receive(:<<).
344
+ with [data, '12345']
345
+ subject.send(:start_listener, socket)
317
346
  end
318
347
  end
319
348
 
@@ -346,6 +375,37 @@ describe RTP::Receiver do
346
375
  end
347
376
 
348
377
  describe "#stop_packet_writer" do
349
- pending
378
+ let(:packet_writer) { double "@packet_writer" }
379
+
380
+ it "closes the @capture_file" do
381
+ subject.instance_variable_get(:@capture_file).should_receive(:close)
382
+ subject.send(:stop_packet_writer)
383
+ end
384
+
385
+ context "writing packets" do
386
+ before do
387
+ subject.should_receive(:writing_packets?).and_return true
388
+ subject.should_receive(:writing_packets?).and_return false
389
+ end
390
+
391
+ it "kills the @packet_writer and sets it to nil" do
392
+ subject.instance_variable_get(:@packet_writer).should_receive(:kill)
393
+ subject.send(:stop_packet_writer)
394
+ subject.instance_variable_get(:@packet_writer).should be_nil
395
+ end
396
+ end
397
+
398
+ context "not writing packets" do
399
+ before do
400
+ subject.should_receive(:writing_packets?).and_return false
401
+ subject.should_receive(:writing_packets?).and_return false
402
+ end
403
+
404
+ it "sets @packet_writer it to nil" do
405
+ subject.instance_variable_get(:@packet_writer).should_not_receive(:kill)
406
+ subject.send(:stop_packet_writer)
407
+ subject.instance_variable_get(:@packet_writer).should be_nil
408
+ end
409
+ end
350
410
  end
351
411
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rtp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-11-22 00:00:00.000000000 Z
13
+ date: 2012-12-06 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bindata
@@ -152,6 +152,8 @@ files:
152
152
  - lib/rtp/logger.rb
153
153
  - lib/rtp/packet.rb
154
154
  - lib/rtp/receiver.rb
155
+ - lib/rtp/sender.rb
156
+ - lib/rtp/senders/socat.rb
155
157
  - lib/rtp/version.rb
156
158
  - lib/rtp.rb
157
159
  - spec/rtp/coverage/assets/0.7.1/application.css