rtp 0.1.3 → 0.1.4

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