diff --git a/Gemfile b/Gemfile index d752e02..ff74e60 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ source :rubygems gemspec gem 'sdp', '~> 0.2.2' -gem 'eventmachine', '~> 0.12.10' gem 'simplecov', '>= 0.4.0', :require => false, :group => :test diff --git a/Gemfile.lock b/Gemfile.lock index fb8cc64..c306c4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,9 @@ PATH remote: . specs: rtsp (0.1.0) + eventmachine (~> 0.12.10) + eventmachine (~> 0.12.10) + rtsp sdp (~> 0.2.0) sdp (~> 0.2.2) @@ -34,7 +37,6 @@ GEM erubis (2.6.6) abstract (>= 1.0.0) eventmachine (0.12.10) - eventmachine (0.12.10-java) fattr (2.2.0) flay (1.4.2) ruby_parser (~> 2.0) @@ -131,7 +133,6 @@ DEPENDENCIES bundler (~> 1.0.0) code_statistics (~> 0.2.13) cucumber - eventmachine (~> 0.12.10) infinity_test jeweler (~> 1.5.0) metric_fu diff --git a/Rakefile b/Rakefile index 73d5bd4..059130e 100644 --- a/Rakefile +++ b/Rakefile @@ -17,12 +17,12 @@ rescue Bundler::BundlerError => e exit e.status_code end -#require 'ore/specification' +require 'ore/specification' require 'jeweler' Jeweler::Tasks.new(Ore::Specification.new) -#require 'ore/tasks' -#Ore::Tasks.new +require 'ore/tasks' +Ore::Tasks.new require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) do |t| diff --git a/gemspec.yml b/gemspec.yml index d83767a..851ba9e 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -15,7 +15,6 @@ has_yard: true dependencies: sdp: ~> 0.2.0 - eventmachine: ~>0.12.10 development_dependencies: bundler: ~> 1.0.0 diff --git a/lib/rtsp/capturer.rb b/lib/rtsp/capturer.rb index 25928fc..21c89aa 100644 --- a/lib/rtsp/capturer.rb +++ b/lib/rtsp/capturer.rb @@ -1,24 +1,46 @@ +require_relative 'exception' require 'tempfile' -require 'eventmachine' +require 'socket' module RTSP - class Capturer < EventMachine::Connection - + class Capturer DEFAULT_CAPFILE_NAME = "rtsp_capture.rtsp" - attr_reader :capture_file + attr_accessor :media_file + attr_accessor :port + attr_accessor :protocol + attr_accessor :broadcast_type + + # @param [Symbol] protocol :udp or :tcp + def initialize(protocol=:udp, rtp_port=9000, capture_file=nil) + if protocol == :udp + init_udp_server(rtp_port) + elsif protocol == :tcp + init_tcp_server(rtp_port) + else + raise RTSP::Exception + end - def initialize(capture_file=nil) @capture_file = capture_file || Tempfile.new(DEFAULT_CAPFILE_NAME) end - def post_init - puts "client connected" + def run + loop do + data = @server.recvfrom(MAX_BYTES_TO_RECEIVE).first + puts data.size + @capture_file.write data + end + end + + def init_udp_server(rtp_port) + @server = UDPSocket.open + #opt = [1].pack("i") + #@server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, opt) + @server.bind('0.0.0.0', rtp_port) end - def receive_data data - p data.size - @capture_file.write data + def init_tcp_server(rtp_port) + @server = TCPSocket.new('0.0.0.0', rtp_port) end end -end \ No newline at end of file +end diff --git a/lib/rtsp/client.rb b/lib/rtsp/client.rb index 2ab11b2..b76d116 100644 --- a/lib/rtsp/client.rb +++ b/lib/rtsp/client.rb @@ -2,6 +2,7 @@ require 'tempfile' require 'timeout' +require_relative 'transport_parser' require File.expand_path(File.dirname(__FILE__) + '/capturer') require File.expand_path(File.dirname(__FILE__) + '/exception') require File.expand_path(File.dirname(__FILE__) + '/global') @@ -24,6 +25,8 @@ class Client attr_reader :session attr_reader :supported_methods attr_accessor :tracks + attr_accessor :connection + attr_accessor :capturer # TODO: Break Stream out in to its own class. # See RFC section A.1. @@ -36,6 +39,7 @@ def self.configure # @param [String] rtsp_url URL to the resource to stream. If no scheme is # given, "rtsp" is assumed. If no port is given, 554 is assumed. +=begin def initialize(rtsp_url, args={}) @server_uri = build_resource_uri_from rtsp_url @cseq = 1 @@ -47,10 +51,36 @@ def initialize(rtsp_url, args={}) @capture_data = args[:capture_data] || true @capture_file = args[:capture_file] || Tempfile.new(DEFAULT_CAPFILE_NAME) - @transport = {} - @transport[:client_port] = args[:client_port] || 9000 - @transport[:specifier] = args[:transport_specifier] || "RTP/AVP" - @transport[:routing] = args[:routing] || "unicast" + @transport_request = {} + @transport_request[:client_port_request] = args[:client_port_request] || 9000 + @transport_request[:protocol] = args[:protocol] || "RTP" + @transport_request[:profile] = args[:profile] || "AVP" + @transport_request[:broadcast_type_request] = args[:broadcast_type_request] || "unicast" + end +=end + # TODO: Use server_url everywhere; just use URI to ensure the port & rtspu. + def initialize(server_url=nil) + Struct.new("Connection", :server_url, :timeout, :socket, + :do_capture, :interleave) + @connection = Struct::Connection.new + @capturer = RTSP::Capturer.new + + yield(connection, capturer) if block_given? + + @connection.server_url = server_url || @connection.server_url + @server_uri = build_resource_uri_from(@connection.server_url) + @connection.timeout ||= 30 + @connection.socket ||= TCPSocket.new(@server_uri.host, @server_uri.port) + @connection.do_capture ||= true + @connection.interleave ||= false + @capturer.port ||= 9000 + @capturer.protocol ||= :udp + @capturer.broadcast_type ||= :unicast + @capturer.media_file ||= Tempfile.new(DEFAULT_CAPFILE_NAME) + + @play_thread = nil + @cseq = 1 + reset_state end # The URL for the RTSP server to talk to can change if multiple servers are @@ -71,13 +101,13 @@ def send_message message message.to_s.each_line { |line| RTSP::Client.log line.strip } begin - response = Timeout::timeout(@timeout) do - @socket.send(message.to_s, 0) - socket_data = @socket.recvfrom MAX_BYTES_TO_RECEIVE + response = Timeout::timeout(@connection.timeout) do + @connection.socket.send(message.to_s, 0) + socket_data = @connection.socket.recvfrom MAX_BYTES_TO_RECEIVE RTSP::Response.new socket_data.first end rescue Timeout::Error - raise RTSP::Exception, "Request took more than #{@timeout} seconds to send." + raise RTSP::Exception, "Request took more than #{@connection.timeout} seconds to send." end RTSP::Client.log "Received response:" @@ -142,7 +172,11 @@ def announce(request_url, description, additional_headers={}) request(message) end - # TODO: parse Transport header (http://tools.ietf.org/html/rfc2326#section-12.39) + def request_transport + value = "RTP/AVP;#{@capturer.broadcast_type};client_port=" + value << "#{@capturer.port}-#{@capturer.port + 1}" + end + # TODO: @session numbers are relevant to tracks, and a client can play multiple tracks at the same time. # Sends the SETUP request, then sets @session to the value returned in the # Session header from the server, then sets the @session_state to :ready. @@ -151,12 +185,8 @@ def announce(request_url, description, additional_headers={}) # @param [Hash] additional_headers # @return [RTSP::Response] The response formatted as a Hash. def setup(track, additional_headers={}) - transport_value = "#{@transport[:specifier]};#{@transport[:routing]};" - transport_value << "client_port=#{@transport[:client_port]}-" - transport_value << "#{@transport[:client_port] + 1}" - message = RTSP::Message.setup(track).with_headers({ - cseq: @cseq, transport: transport_value }) + cseq: @cseq, transport: request_transport}) message.add_headers additional_headers request(message) do |response| @@ -165,29 +195,9 @@ def setup(track, additional_headers={}) end @session = response.session - #@transport = parse_transport_from response.transport - end - end - - def parse_transport_from field_string -=begin - fields = field_string.split ";" - transport = {} - specifier = fields.shift - transport[:protocol] = specifier.split("/")[0] - transport[:profile] = specifier.split("/")[1] - transport[:lower_transport] = specifier.split("/")[2].downcase.to_sym || :udp - transport[:network_type] = fields.shift.to_sym || :multicast - - extras = fields.inject({}) do |result, field_and_value_string| - field_and_value_array = field_and_value_string.split "=" - result[field_and_value_array.first.to_sym] = field_and_value_array.last - result + parser = RTSP::TransportParser.new + @transport = parser.parse response.transport end - transport.merge! extras -=end - pattern = /(?\w+)\/(?\w+)(\/(?\w+))?;(?\w+);(?.*)/ - transport_match = pattern.match(field_string) end # Sends the PLAY request and sets @session_state to :playing. @@ -206,16 +216,12 @@ def play(track, additional_headers={}) end end + # TODO: If playback over UDP doesn't result in any data coming in on the socket, + # re-setup with RTP/AVP/TCP;unicast;interleaved=0-1 def start_capture - rtp_port = 9000 - - EventMachine.run { - #EventMachine.connect('0.0.0.0', 9000, RTSP::Capturer) - EventMachine.open_datagram_socket('0.0.0.0', rtp_port, RTSP::Capturer, @capture_file) - EventMachine.add_periodic_timer(1) do - RTSP::Client.log "Waiting for UDP data on port #{rtp_port}..." - end - } + log "Capturing on port #{@transport[:client_port]}" + @capturer = RTSP::Capturer.new(:udp, @transport[:client_port], @capture_file) + @capturer.run end # Sends the PAUSE request and sets @session_state to :ready. @@ -412,4 +418,4 @@ def extract_supported_methods_from method_list method_list.downcase.split(', ').map { |m| m.to_sym } end end -end \ No newline at end of file +end diff --git a/lib/rtsp/transport_parser.rb b/lib/rtsp/transport_parser.rb index afb1c66..6438086 100644 --- a/lib/rtsp/transport_parser.rb +++ b/lib/rtsp/transport_parser.rb @@ -2,6 +2,11 @@ module RTSP class TransportParser < Parslet::Parser + + def initialize + super + end + rule(:transport_specifier) do match('[A-Z]').repeat(3).as(:protocol) >> forward_slash >> match('[A-Z]').repeat(3).as(:profile) >> diff --git a/rtsp.gemspec b/rtsp.gemspec index ffdec22..0d3d2ec 100644 --- a/rtsp.gemspec +++ b/rtsp.gemspec @@ -10,7 +10,6 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 1.3.6") if s.respond_to? :required_rubygems_version= s.authors = ["Steve Loveless, Mike Kirby"] s.date = %q{2011-03-22} - s.default_executable = %q{rtsp} s.description = %q{This library intends to follow the RTSP RFC document (2326) to allow for working with RTSP servers. At this point, it's up to you to parse the data from a play call, but we'll get there. ...eventually. For more information RTSP: http://www.ietf.org/rfc/rfc2326.txt} diff --git a/soma_test.rb b/soma_test.rb index 89bfed4..8a3d536 100644 --- a/soma_test.rb +++ b/soma_test.rb @@ -2,8 +2,19 @@ #RTSP::Client.log = false +cap_file = File.new("soma_cap.rtsp", "wb") url = "rtsp://64.202.98.91/sa.sdp" -client = RTSP::Client.new url +client = RTSP::Client.new(url) +client.capturer.media_file = cap_file +# client = RTSP::Client.new(url) do |client, capturer| +# description = SDP.parse(open("http://test/description.sdp")) +# client.timeout = 30 +# client.socket = TCPSocket.new +# client.interleave = true +# capturer.file = Tempfile.new "test" +# capturer.capture_port = 8555 +# capturer.protocol = :tcp +# end client.options client.describe @@ -16,8 +27,10 @@ client.setup media_track #client.setup media_track, :transport => "RTP/AVP;unicast;client_port=9000-9001" +#client.setup media_track, :transport => "RTP/AVP/TCP;unicast;interleaved=0-1" #client[media_track].setup #client.media_control_tracks.play client.play aggregate_track +sleep 5 #client[aggregate_track].play client.teardown aggregate_track