rtsp_server 0.0.2-universal-java

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/ChangeLog.rdoc +74 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.rdoc +20 -0
  6. data/README.rdoc +152 -0
  7. data/Rakefile +23 -0
  8. data/bin/rtsp_client +133 -0
  9. data/features/client_changes_state.feature +58 -0
  10. data/features/client_requests.feature +27 -0
  11. data/features/control_streams_as_client.feature +26 -0
  12. data/features/step_definitions/client_changes_state_steps.rb +52 -0
  13. data/features/step_definitions/client_requests_steps.rb +68 -0
  14. data/features/step_definitions/control_streams_as_client_steps.rb +34 -0
  15. data/features/support/env.rb +50 -0
  16. data/features/support/hooks.rb +3 -0
  17. data/lib/ext/logger.rb +8 -0
  18. data/lib/rtsp/client.rb +520 -0
  19. data/lib/rtsp/common.rb +148 -0
  20. data/lib/rtsp/error.rb +6 -0
  21. data/lib/rtsp/global.rb +63 -0
  22. data/lib/rtsp/helpers.rb +28 -0
  23. data/lib/rtsp/message.rb +272 -0
  24. data/lib/rtsp/request.rb +39 -0
  25. data/lib/rtsp/response.rb +47 -0
  26. data/lib/rtsp/server.rb +311 -0
  27. data/lib/rtsp/socat_streaming.rb +320 -0
  28. data/lib/rtsp/stream_server.rb +37 -0
  29. data/lib/rtsp/transport_parser.rb +96 -0
  30. data/lib/rtsp/version.rb +4 -0
  31. data/lib/rtsp.rb +6 -0
  32. data/rtsp.gemspec +44 -0
  33. data/spec/rtsp/client_spec.rb +326 -0
  34. data/spec/rtsp/helpers_spec.rb +53 -0
  35. data/spec/rtsp/message_spec.rb +420 -0
  36. data/spec/rtsp/response_spec.rb +306 -0
  37. data/spec/rtsp/transport_parser_spec.rb +137 -0
  38. data/spec/rtsp_spec.rb +27 -0
  39. data/spec/spec_helper.rb +88 -0
  40. data/spec/support/fake_rtsp_server.rb +123 -0
  41. data/tasks/roodi.rake +9 -0
  42. data/tasks/roodi_config.yaml +14 -0
  43. data/tasks/stats.rake +12 -0
  44. metadata +280 -0
@@ -0,0 +1,311 @@
1
+ require_relative 'request'
2
+ require_relative 'stream_server'
3
+ require_relative 'global'
4
+ require 'socket'
5
+
6
+ module RTSP
7
+ SUPPORTED_VERSION = "1.0"
8
+
9
+ # Instantiates an RTSP Server
10
+ # Streaming is performed using socat.
11
+ # All you need is the multicast source RTP host and port.
12
+ #
13
+ # require 'rtsp/server'
14
+ # server = RTSP::Server.new "10.221.222.90", 8554
15
+ #
16
+ # This is for the stream at index 1 (rtsp://10.221.222.90:8554/stream1)
17
+ # RTSP::StreamServer.instance.source_ip << "239.221.222.241"
18
+ # RTSP::StreamServer.instance.source_port << 6780
19
+ # RTSP::StreamServer.instance.fmtp << "96 packetization-mode=1..."
20
+ # RTSP::StreamServer.instance.rtp_map << "96 H264/90000"
21
+ #
22
+ # This is for the stream at index 2 (rtsp://10.221.222.90:8554/stream2)
23
+ # RTSP::StreamServer.instance.source_ip << "239.221.222.141"
24
+ # RTSP::StreamServer.instance.source_port << 6740
25
+ # RTSP::StreamServer.instance.fmtp << "96 packetization-mode=1..."
26
+ # RTSP::StreamServer.instance.rtp_map << "96 MP4/90000"
27
+ #
28
+ # Now start the server
29
+ # server.start
30
+ class Server
31
+ extend RTSP::Global
32
+
33
+ OPTIONS_LIST = %w(OPTIONS DESCRIBE SETUP TEARDOWN PLAY
34
+ PAUSE GET_PARAMETER SET_PARAMETER)
35
+
36
+ attr_accessor :options_list
37
+ attr_accessor :version
38
+ attr_accessor :session
39
+ attr_accessor :agent
40
+
41
+ # Initializes the the Stream Server.
42
+ #
43
+ # @param [Fixnum] host IP interface to bind.
44
+ # @param [Fixnum] port RTSP port.
45
+ def initialize(host, port=554)
46
+ @session = rand(99999999)
47
+ @stream_server = RTSP::StreamServer.instance
48
+ @interface_ip = host
49
+ @stream_server.interface_ip = host
50
+ @tcp_server = TCPServer.new(host, port)
51
+ @udp_server = UDPSocket.new
52
+ @udp_server.bind(host, port)
53
+ @agent = {}
54
+ end
55
+
56
+ # Starts accepting TCP connections
57
+ def start
58
+ Thread.start { udp_listen }
59
+
60
+ loop do
61
+ client = @tcp_server.accept
62
+
63
+ Thread.start do
64
+ begin
65
+ loop { break if serve(client) == -1 }
66
+ rescue EOFError
67
+ # do nothing
68
+ ensure
69
+ client.close unless @agent[client].include? "QuickTime"
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # Listens on the UDP socket for RTSP requests.
76
+ def udp_listen
77
+ loop do
78
+ data, sender = @udp_server.recvfrom(500)
79
+ response = process_request(data, sender[3])
80
+ @udp_server.send(response, 0, sender[3], sender[1])
81
+ end
82
+ end
83
+
84
+ # Serves a client request.
85
+ #
86
+ # @param [IO] io Request/response socket object.
87
+ def serve io
88
+ request_str = ""
89
+ count = 0
90
+
91
+ begin
92
+ request_str << io.read_nonblock(500)
93
+ rescue Errno::EAGAIN
94
+ return -1 if count > 50
95
+ count += 1
96
+ sleep 0.01
97
+ retry
98
+ end
99
+
100
+ response = process_request(request_str, io)
101
+ io.send(response, 0)
102
+ end
103
+
104
+ # Process an RTSP request
105
+ #
106
+ # @param [String] request_str RTSP request.
107
+ # @param [String] remote_address IP address of sender.
108
+ # @return [String] Response.
109
+ def process_request(request_str, io)
110
+ remote_address = io.remote_address.ip_address
111
+ /(?<action>.*) rtsp:\/\// =~ request_str
112
+ request = RTSP::Request.new(request_str, remote_address)
113
+ @agent[io] = request.user_agent
114
+ response, body = send(action.downcase.to_sym, request)
115
+
116
+ add_headers(request, response, body)
117
+ end
118
+
119
+ # Handles the options request.
120
+ #
121
+ # @param [RTSP::Request] request
122
+ # @return [Array<Array<String>>] Response headers and body.
123
+ def options(request)
124
+ RTSP::Server.log "Received OPTIONS request from #{request.remote_host}"
125
+ response = []
126
+ response << "Public: #{OPTIONS_LIST.join ','}"
127
+ response << "\r\n"
128
+
129
+ [response]
130
+ end
131
+
132
+ # Handles the describe request.
133
+ #
134
+ # @param [RTSP::Request] request
135
+ # @return [Array<Array<String>>] Response headers and body.
136
+ def describe(request)
137
+ RTSP::Server.log "Received DESCRIBE request from #{request.remote_host}"
138
+ description = @stream_server.description(request.multicast?, request.stream_index)
139
+
140
+ [[], description]
141
+ end
142
+
143
+ # Handles the announce request.
144
+ #
145
+ # @param [RTSP::Request] request
146
+ # @return [Array<Array<String>>] Response headers and body.
147
+ def announce(request)
148
+ []
149
+ end
150
+
151
+ # Handles the setup request.
152
+ #
153
+ # @param [RTSP::Request] request
154
+ # @return [Array<Array<String>>] Response headers and body.
155
+ def setup(request)
156
+ RTSP::Server.log "Received SETUP request from #{request.remote_host}"
157
+ @session = @session.next
158
+ multicast_check = request.transport.include?('multicast')
159
+ server_port = @stream_server.setup_streamer(@session,
160
+ request.transport_url, request.stream_index, multicast_check)
161
+ response = []
162
+ transport = generate_transport(request, server_port, request.stream_index)
163
+ response << "Transport: #{transport.join}"
164
+ response << "Session: #{@session}"
165
+ response << "\r\n"
166
+
167
+ [response]
168
+ end
169
+
170
+ # Handles the play request.
171
+ #
172
+ # @param [RTSP::Request] request
173
+ # @return [Array<Array<String>>] Response headers and body.
174
+ def play(request)
175
+ RTSP::Server.log "Received PLAY request from #{request.remote_host}"
176
+ sid = request.session[:session_id]
177
+ response = []
178
+ response << "Session: #{sid}"
179
+ response << "Range: #{request.range}"
180
+ index = request.stream_index - 1
181
+ rtp_sequence, rtp_timestamp = @stream_server.parse_sequence_number(
182
+ @stream_server.source_ip[index], @stream_server.source_port[index])
183
+ @stream_server.start_streaming sid
184
+ response << "RTP-Info: url=#{request.url}/track1;" +
185
+ "seq=#{rtp_sequence + 6} ;rtptime=#{rtp_timestamp}"
186
+ response << "\r\n"
187
+
188
+ [response]
189
+ end
190
+
191
+ # Handles the get_parameter request.
192
+ #
193
+ # @param [RTSP::Request] request
194
+ # @return [Array<Array<String>>] Response headers and body.
195
+ def get_parameter(request)
196
+ RTSP::Server.log "Received GET_PARAMETER request from #{request.remote_host}"
197
+ " Pending Implementation"
198
+
199
+ [[]]
200
+ end
201
+
202
+ # Handles the set_parameter request.
203
+ #
204
+ # @param [RTSP::Request] request
205
+ # @return [Array<Array<String>>] Response headers and body.
206
+ def set_parameter(request)
207
+ RTSP::Server.log "Received SET_PARAMETER request from #{request.remote_host}"
208
+ " Pending Implementation"
209
+
210
+ [[]]
211
+ end
212
+
213
+ # Handles the redirect request.
214
+ #
215
+ # @param [RTSP::Request] request
216
+ # @return [Array<Array<String>>] Response headers and body.
217
+ def redirect(request)
218
+ RTSP::Server.log "Received REDIRECT request from #{request.remote_host}"
219
+ " Pending Implementation"
220
+
221
+ [[]]
222
+ end
223
+
224
+ # Handles the teardown request.
225
+ #
226
+ # @param [RTSP::Request] request
227
+ # @return [Array<Array<String>>] Response headers and body.
228
+ def teardown(request)
229
+ RTSP::Server.log "Received TEARDOWN request from #{request.remote_host}"
230
+ sid = request.session[:session_id]
231
+ @stream_server.stop_streaming sid
232
+
233
+ [[]]
234
+ end
235
+
236
+ # Handles a pause request.
237
+ #
238
+ # @param [RTSP::Request] request
239
+ # @return [Array<Array<String>>] Response headers and body.
240
+ def pause(request)
241
+ RTSP::Server.log "Received PAUSE request from #{request.remote_host}"
242
+ response = []
243
+ sid = request.session[:session_id]
244
+ response << "Session: #{sid}"
245
+ @stream_server.disconnect sid
246
+
247
+ [response]
248
+ end
249
+
250
+ # Adds the headers to the response.
251
+ #
252
+ # @param [RTSP::Request] request
253
+ # @param [Array<String>] response Response headers
254
+ # @param [String] body Response body
255
+ # @param [String] status Response status
256
+ # @return [Array<Array<String>>] Response headers and body.
257
+ def add_headers(request, response, body, status="200 OK")
258
+ result = []
259
+ version ||= SUPPORTED_VERSION
260
+ result << "RTSP/#{version} #{status}"
261
+ result << "CSeq: #{request.cseq}"
262
+
263
+ unless body.nil?
264
+ result << "Content-Type: #{request.accept}"
265
+ result << "Content-Base: #{request.url}/"
266
+ result << "Content-Length: #{body.size}"
267
+ end
268
+
269
+ result << "Date: #{Time.now.gmtime.strftime('%a, %b %d %Y %H:%M:%S GMT')}"
270
+ result << response.join("\r\n") unless response.nil?
271
+ result << body unless body.nil?
272
+
273
+ result.flatten.join "\r\n"
274
+ end
275
+
276
+ # Handles unsupported RTSP requests.
277
+ #
278
+ # @param [Symbol] method_name Method name to be called.
279
+ # @param [Array] args Arguments to be passed in to the method.
280
+ # @param [Proc] block A block of code to be passed to a method.
281
+ def method_missing(method_name, *args, &block)
282
+ RTSP::Server.log("Received request for #{method_name} (not implemented)", :warn)
283
+
284
+ [[], "Not Implemented"]
285
+ end
286
+
287
+ private
288
+
289
+ # Generates the transport headers for the response.
290
+ #
291
+ # @param [RTSP::Request] Request object.
292
+ # @param [Fixnum] server_port Port on which the stream_server is streaming from.
293
+ def generate_transport request, server_port, index=1
294
+ port_specifier = 'client_port'
295
+ transport = request.transport.split(port_specifier)
296
+ transport[1] = port_specifier + transport[1]
297
+
298
+ if request.transport.include?("unicast")
299
+ transport[0] << "destination=#{request.remote_host};"
300
+ transport[1] << ";server_port=#{server_port}-#{server_port+1}"
301
+ else
302
+ transport[0] << "destination=#{@stream_server.source_ip[index - 1]};"
303
+ transport[1] << ";ttl=10"
304
+ end
305
+
306
+ transport[0] << "source=#{@stream_server.interface_ip};"
307
+
308
+ transport
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,320 @@
1
+ require 'sys/proctable'
2
+ require_relative 'global'
3
+ require 'os'
4
+ require 'ipaddr'
5
+ require 'rtp/packet'
6
+
7
+ module RTSP
8
+ module SocatStreaming
9
+ include RTSP::Global
10
+
11
+ RTCP_SOURCE = ["80c80006072dee6ad42c300f76c3b928377e99e5006c461ba92d8a3" +
12
+ "081ca0006072dee6a010e49583330444e2d41414a4248513600000000"]
13
+ MP4_RTP_MAP = "96 MP4V-ES/30000"
14
+ MP4_FMTP = "96 profile-level-id=5;config=000001b005000001b50900000100000" +
15
+ "0012000c888ba9860fa22c087828307"
16
+ H264_RTP_MAP = "96 H264/90000"
17
+ H264_FMTP = "96 packetization-mode=1;profile-level-id=428032;" +
18
+ "sprop-parameter-sets=Z0KAMtoAgAMEwAQAAjKAAAr8gYAAAYhMAABMS0IvfjAA" +
19
+ "ADEJgAAJiWhF78CA,aM48gA=="
20
+
21
+ # @return [Hash] Hash of session IDs and SOCAT commands.
22
+ attr_accessor :sessions
23
+
24
+ # @return [Hash] Hash of session IDs and pids.
25
+ attr_reader :pids
26
+
27
+ # @return [Hash] Hash of session IDs and RTCP threads.
28
+ attr_reader :rtcp_threads
29
+
30
+ # @return [Array<String>] IP address of the source camera.
31
+ attr_accessor :source_ip
32
+
33
+ # @return [Array<Fixnum>] Port where the source camera is streaming.
34
+ attr_accessor :source_port
35
+
36
+ # @return [String] IP address of the interface of the RTSP streamer.
37
+ attr_accessor :interface_ip
38
+
39
+ # @return [Fixnum] RTP timestamp of the source stream.
40
+ attr_accessor :rtp_timestamp
41
+
42
+ # @return [Fixnum] RTP sequence number of the source stream.
43
+ attr_accessor :rtp_sequence
44
+
45
+ # @return [String] RTCP source identifier.
46
+ attr_accessor :rtcp_source_identifier
47
+
48
+ # @return [Array<String>] Media type attributes.
49
+ attr_accessor :rtp_map
50
+
51
+ # @return [Array<String>] Media format attributes.
52
+ attr_accessor :fmtp
53
+
54
+ # Generates a RTCP source ID based on the friendly name.
55
+ # This ID is used in the RTCP communication with the client.
56
+ # The default +RTCP_SOURCE+ will be used if one is not provided.
57
+ #
58
+ # @param [String] friendly_name Name to be used in the RTCP source ID.
59
+ # @return [String] rtcp_source_id RTCP Source ID.
60
+ def generate_rtcp_source_id friendly_name
61
+ ["80c80006072dee6ad42c300f76c3b928377e99e5006c461ba92d8a3081ca0006072dee6a010e" +
62
+ friendly_name.unpack("H*").first + "00000000"].pack("H*")
63
+ end
64
+
65
+ # Creates a RTP streamer using socat.
66
+ #
67
+ # @param [String] sid Session ID.
68
+ # @param [String] transport_url Destination IP:port.
69
+ # @param [Fixnum] index Stream index.
70
+ # @return [Fixnum] The port the streamer will stream on.
71
+ def setup_streamer(sid, transport_url, index=1, multicast=false)
72
+ dest_ip, dest_port = transport_url.split ":"
73
+
74
+ @rtcp_source_identifier ||= RTCP_SOURCE.pack("H*")
75
+ local_port = multicast ? @source_port[index - 1] : free_port(true)
76
+
77
+ unless multicast
78
+ @rtcp_threads[sid] = Thread.start do
79
+ s = UDPSocket.new
80
+ s.bind(@interface_ip, local_port+1)
81
+
82
+ loop do
83
+ begin
84
+ _, sender = s.recvfrom(36)
85
+ s.send(@rtcp_source_identifier, 0, sender[3], sender[1])
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ @cleaner ||= Thread.start { cleanup_defunct }
92
+ @processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
93
+
94
+ if multicast
95
+ @sessions[sid] = :multicast
96
+ else
97
+ @sessions[sid] = build_socat(dest_ip, dest_port, local_port, index)
98
+ end
99
+
100
+ local_port
101
+ end
102
+
103
+ SOCAT_OPTIONS = "rcvbuf=2500000,sndbuf=2500000,sndtimeo=0.00001,rcvtimeo=0.00001"
104
+ BLOCK_SIZE = 2000
105
+ BSD_OPTIONS = "setsockopt-int=0xffff:0x200:0x01"
106
+
107
+ # Start streaming for the requested session.
108
+ #
109
+ # @param [String] session ID.
110
+ def start_streaming sid
111
+ spawn_socat(sid, @sessions[sid]) unless @sessions[sid] == :multicast
112
+ end
113
+
114
+ # Stop streaming for the requested session.
115
+ #
116
+ # @param [String] session ID.
117
+ def stop_streaming sid
118
+ if sid.nil?
119
+ disconnect_all_streams
120
+ else
121
+ disconnect sid
122
+ @rtcp_threads[sid].kill unless rtcp_threads[sid].nil?
123
+ @rtcp_threads.delete sid
124
+ end
125
+ end
126
+
127
+ # Returns the default stream description.
128
+ #
129
+ # @param[Boolean] multicast True if the description is for a multicast stream.
130
+ # @param [Fixnum] stream_index Index of the stream type.
131
+ def description multicast=false, stream_index=1
132
+ rtp_map = @rtp_map[stream_index - 1] || H264_RTP_MAP
133
+ fmtp = @fmtp[stream_index - 1] || H264_FMTP
134
+
135
+ <<EOF
136
+ v=0\r
137
+ o=- 1345481255966282 1 IN IP4 #{@interface_ip}\r
138
+ s=Session streamed by "Streaming Server"\r
139
+ i=stream1#{multicast ? 'm' : ''}\r
140
+ t=0 0\r
141
+ a=tool:LIVE555 Streaming Media v2007.07.09\r
142
+ a=type:broadcast\r
143
+ a=control:*\r
144
+ a=range:npt=0-\r
145
+ a=x-qt-text-nam:Session streamed by "Streaming Server"\r
146
+ a=x-qt-text-inf:stream1#{multicast ? 'm' : ''}\r
147
+ m=video #{multicast ? @source_port[stream_index - 1] : 0} RTP/AVP 96\r
148
+ c=IN IP4 #{multicast ? "#{multicast_ip(stream_index)}/10" : "0.0.0.0"}\r
149
+ a=rtpmap:#{rtp_map}\r
150
+ a=fmtp:#{fmtp}\r
151
+ a=control:track1\r
152
+ EOF
153
+ end
154
+
155
+ # Disconnects the stream matching the session ID.
156
+ #
157
+ # @param [String] sid Session ID.
158
+ def disconnect sid
159
+ pid = @pids[sid].to_i
160
+ @pids.delete(sid)
161
+ @sessions.delete(sid)
162
+ Process.kill(9, pid) if pid > 1000
163
+ rescue Errno::ESRCH
164
+ log "Tried to kill dead process: #{pid}"
165
+ end
166
+
167
+ # Parses the headers from an RTP stream.
168
+ #
169
+ # @param [String] src_ip Multicast IP address of RTP stream.
170
+ # @param [Fixnum] src_port Port of RTP stream.
171
+ # @return [Array<Fixnum>] Sequence number and timestamp
172
+ def parse_sequence_number(src_ip, src_port)
173
+ sock = UDPSocket.new
174
+ ip = IPAddr.new(src_ip).hton + IPAddr.new("0.0.0.0").hton
175
+ sock.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, ip)
176
+ sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
177
+ sock.bind(Socket::INADDR_ANY, src_port)
178
+
179
+ begin
180
+ data = sock.recv_nonblock(1500)
181
+ rescue Errno::EAGAIN
182
+ retry
183
+ end
184
+
185
+ sock.close
186
+ packet = RTP::Packet.read(data)
187
+
188
+ [packet["sequence_number"], packet["timestamp"]]
189
+ end
190
+
191
+ private
192
+
193
+ # Returns the multicast IP on which the streamer will stream.
194
+ #
195
+ # @param [Fixnum] index Stream index.
196
+ # @return [String] Multicast IP.
197
+ def multicast_ip index=1
198
+ @interface_ip ||= find_best_interface_ipaddr @source_ip[index-1]
199
+ multicast_ip = @interface_ip.split "."
200
+ multicast_ip[0] = "239"
201
+
202
+ multicast_ip.join "."
203
+ end
204
+
205
+ # Cleans up defunct child processes
206
+ def cleanup_defunct
207
+ loop do
208
+ begin
209
+ Process.wait 0
210
+ rescue Errno::ECHILD
211
+ sleep 10
212
+ retry
213
+ end
214
+ end
215
+ end
216
+
217
+ # Determine the interface address that best matches an IP address. This
218
+ # is most useful when talking to a remote computer and needing to
219
+ # determine the interface that is being used for the connection.
220
+ #
221
+ # @param [String] device_ip IP address of the remote device you want to
222
+ # talk to.
223
+ # @return [String] IP of the interface that would be used to talk to.
224
+ def find_best_interface_ipaddr device_ip
225
+ UDPSocket.open { |s| s.connect(device_ip, 1); s.addr.last }
226
+ end
227
+
228
+ # Disconnects all streams that are currently streaming.
229
+ def disconnect_all_streams
230
+ @pids.values.each do |pid|
231
+ Process.kill(9, pid.to_i) if pid.to_i > 1000 rescue Errno::ESRCH
232
+ end
233
+
234
+ @sessions.clear
235
+ @pids.clear
236
+ end
237
+
238
+ # Spawns an instance of Socat.
239
+ #
240
+ # @param [String] sid The session ID of the stream.
241
+ # @param [String] command The SOCAT command to be spawned.
242
+ def spawn_socat(sid, command)
243
+ @processes ||= Sys::ProcTable.ps.map { |p| p.cmdline }
244
+
245
+ if command.nil?
246
+ log("SOCAT command for #{sid} was nil", :warn)
247
+ return
248
+ end
249
+
250
+ if @processes.include?(command)
251
+ pid = get_pid(command)
252
+ log "Streamer already running with pid #{pid}" if pid.is_a? Fixnum
253
+ else
254
+ @sessions[sid] = command
255
+
256
+ Thread.start do
257
+ log "Running stream spawner: #{command}"
258
+ @processes << command
259
+ pid = spawn command
260
+ @pids[sid] = pid
261
+ Thread.start { sleep 20; spawn_socat(sid, command) }
262
+ end
263
+ end
264
+ end
265
+
266
+ # Builds a socat stream command based on the source and target
267
+ # IP and ports of the RTP stream.
268
+ #
269
+ # @param [String] target_ip IP address of the remote device you want to
270
+ # talk to.
271
+ # @param [Fixnum] target_port Port on the remote device you want to
272
+ # talk to.
273
+ # @
274
+
275
+ # @return [String] IP of the interface that would be used to talk to.
276
+ def build_socat(target_ip, target_port, server_port, index=1)
277
+ bsd_options = BSD_OPTIONS if OS.mac?
278
+ bsd_options ||= ""
279
+
280
+ "socat -b #{BLOCK_SIZE} UDP-RECV:#{@source_port[index-1]},reuseaddr," +
281
+ "#{bsd_options}"+ SOCAT_OPTIONS + ",ip-add-membership=#{@source_ip[index-1]}:" +
282
+ "#{@interface_ip} UDP:#{target_ip}:#{target_port},sourceport=#{server_port}," +
283
+ SOCAT_OPTIONS
284
+ end
285
+
286
+ # Attempts to find a random bindable port between 50000-65500
287
+ #
288
+ # @param [Boolean] even Return a free even port number if true.
289
+ # @return [Number] A random bindable port between 50000-65500
290
+ # @raise [RuntimeError] When unable to locate port after 1000 attempts.
291
+ def free_port(even=false)
292
+ 1000.times do
293
+ begin
294
+ port = rand(15500) + 50001
295
+ port += 1 if port % 2 != 0 && even
296
+ socket = UDPSocket.new
297
+ socket.bind('', port)
298
+ return port
299
+ rescue
300
+ # Do nothing if bind fails; continue looping
301
+ ensure
302
+ socket.close
303
+ end
304
+ end
305
+
306
+ raise "Unable to locate free port after 1000 attempts."
307
+ end
308
+
309
+
310
+ # Gets the pid for a SOCAT command.
311
+ #
312
+ # @param [String] cmd SOCAT command
313
+ # @return [Fixnum] PID of the process.
314
+ def get_pid cmd
315
+ Sys::ProcTable.ps.each do |p|
316
+ return p.pid.to_i if p.cmdline.include? cmd
317
+ end
318
+ end
319
+ end
320
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'socat_streaming'
2
+ require 'singleton'
3
+
4
+ module RTSP
5
+ class StreamServer
6
+ include Singleton
7
+ include SocatStreaming
8
+
9
+ def initialize
10
+ @stream_module = SocatStreaming
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.
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