em-rtmp 0.0.3
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/Gemfile +3 -0
 - data/README.md +26 -0
 - data/Rakefile +96 -0
 - data/lib/em-rtmp.rb +18 -0
 - data/lib/em-rtmp/buffer.rb +33 -0
 - data/lib/em-rtmp/connect_request.rb +84 -0
 - data/lib/em-rtmp/connection.rb +189 -0
 - data/lib/em-rtmp/connection_delegate.rb +60 -0
 - data/lib/em-rtmp/handshake.rb +94 -0
 - data/lib/em-rtmp/header.rb +193 -0
 - data/lib/em-rtmp/heartbeat.rb +36 -0
 - data/lib/em-rtmp/io_helpers.rb +192 -0
 - data/lib/em-rtmp/logger.rb +48 -0
 - data/lib/em-rtmp/message.rb +96 -0
 - data/lib/em-rtmp/pending_request.rb +48 -0
 - data/lib/em-rtmp/request.rb +101 -0
 - data/lib/em-rtmp/response.rb +108 -0
 - data/lib/em-rtmp/response_router.rb +130 -0
 - data/lib/em-rtmp/rtmp.rb +30 -0
 - data/lib/em-rtmp/uuid.rb +13 -0
 - data/lib/em-rtmp/version.rb +5 -0
 - metadata +146 -0
 
| 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventMachine
         
     | 
| 
      
 2 
     | 
    
         
            +
              module RTMP
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Logger
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                  LEVEL_DEBUG = 0
         
     | 
| 
      
 6 
     | 
    
         
            +
                  LEVEL_INFO = 1
         
     | 
| 
      
 7 
     | 
    
         
            +
                  LEVEL_ERROR = 2
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  @@level = LEVEL_INFO
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 12 
     | 
    
         
            +
                    attr_accessor :level
         
     | 
| 
      
 13 
     | 
    
         
            +
                  end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def self.level(level)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @@level = level
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def self.debug(message, options={})
         
     | 
| 
      
 20 
     | 
    
         
            +
                    return unless @@level <= LEVEL_DEBUG
         
     | 
| 
      
 21 
     | 
    
         
            +
                    print message, {level: "DEBUG", caller: caller}.merge(options)
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  def self.info(message, options={})
         
     | 
| 
      
 25 
     | 
    
         
            +
                    return unless @@level <= LEVEL_INFO
         
     | 
| 
      
 26 
     | 
    
         
            +
                    print message, {level: "INFO", caller: caller}.merge(options)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def self.error(message, options={})
         
     | 
| 
      
 30 
     | 
    
         
            +
                    return unless @@level <= LEVEL_ERROR
         
     | 
| 
      
 31 
     | 
    
         
            +
                    print message, {level: "ERROR", caller: caller}.merge(options)
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  def self.print(message, options={})
         
     | 
| 
      
 35 
     | 
    
         
            +
                    options[:level] ||= "PRINT"
         
     | 
| 
      
 36 
     | 
    
         
            +
                    options[:caller] ||= caller
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                    caller_splat = options[:caller][0].split(":")
         
     | 
| 
      
 39 
     | 
    
         
            +
                    ruby_file = caller_splat[0].split("/").last
         
     | 
| 
      
 40 
     | 
    
         
            +
                    ruby_line = caller_splat[1]
         
     | 
| 
      
 41 
     | 
    
         
            +
                    ruby_method = caller_splat[2].match(/`(.*)'/)[1]
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                    puts "%-10s%-30s%-30s%s" % ["[#{options[:level]}]", "#{ruby_file}:#{ruby_line}", ruby_method, message.encode("UTF-8")]
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,96 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventMachine
         
     | 
| 
      
 2 
     | 
    
         
            +
              module RTMP
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Message
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                  class << self
         
     | 
| 
      
 6 
     | 
    
         
            +
                    attr_accessor :transaction_id
         
     | 
| 
      
 7 
     | 
    
         
            +
                  end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  attr_accessor :_amf_data, :_amf_error, :_amf_unparsed
         
     | 
| 
      
 10 
     | 
    
         
            +
                  attr_accessor :version, :command, :transaction_id, :values
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                  # Initialize, setting attributes as given
         
     | 
| 
      
 13 
     | 
    
         
            +
                  #
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # attrs - Hash of attributes
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 17 
     | 
    
         
            +
                  def initialize(attrs={})
         
     | 
| 
      
 18 
     | 
    
         
            +
                    attrs.each {|k,v| send("#{k}=", v)}
         
     | 
| 
      
 19 
     | 
    
         
            +
                    self.command ||= nil
         
     | 
| 
      
 20 
     | 
    
         
            +
                    self.transaction_id ||= self.class.next_transaction_id
         
     | 
| 
      
 21 
     | 
    
         
            +
                    self.values ||= []
         
     | 
| 
      
 22 
     | 
    
         
            +
                    self.version ||= 0x00
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def amf3?
         
     | 
| 
      
 26 
     | 
    
         
            +
                    version == 0x03
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  # Encode this message with the chosen serializer
         
     | 
| 
      
 30 
     | 
    
         
            +
                  #
         
     | 
| 
      
 31 
     | 
    
         
            +
                  # Returns a string containing an encoded message
         
     | 
| 
      
 32 
     | 
    
         
            +
                  def encode
         
     | 
| 
      
 33 
     | 
    
         
            +
                    Logger.debug "encoding #{self.inspect}"
         
     | 
| 
      
 34 
     | 
    
         
            +
                    class_mapper = RocketAMF::ClassMapper.new
         
     | 
| 
      
 35 
     | 
    
         
            +
                    ser = RocketAMF::Serializer.new class_mapper
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                    if amf3?
         
     | 
| 
      
 38 
     | 
    
         
            +
                      ser.stream << "\x00"
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                    ser.serialize 0, command
         
     | 
| 
      
 42 
     | 
    
         
            +
                    ser.serialize 0, transaction_id
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    if amf3?
         
     | 
| 
      
 45 
     | 
    
         
            +
                      ser.stream << "\x05"
         
     | 
| 
      
 46 
     | 
    
         
            +
                      ser.stream << "\x11"
         
     | 
| 
      
 47 
     | 
    
         
            +
                      ser.serialize 3, values.first
         
     | 
| 
      
 48 
     | 
    
         
            +
                    else
         
     | 
| 
      
 49 
     | 
    
         
            +
                      values.each do |value|
         
     | 
| 
      
 50 
     | 
    
         
            +
                        ser.serialize 0, value
         
     | 
| 
      
 51 
     | 
    
         
            +
                      end
         
     | 
| 
      
 52 
     | 
    
         
            +
                    end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                    ser.stream
         
     | 
| 
      
 55 
     | 
    
         
            +
                  end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                  def decode(string)
         
     | 
| 
      
 58 
     | 
    
         
            +
                    class_mapper = RocketAMF::ClassMapper.new
         
     | 
| 
      
 59 
     | 
    
         
            +
                    io = StringIO.new string
         
     | 
| 
      
 60 
     | 
    
         
            +
                    des = RocketAMF::Deserializer.new class_mapper
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                      if amf3?
         
     | 
| 
      
 65 
     | 
    
         
            +
                        byte = des.deserialize 3, io
         
     | 
| 
      
 66 
     | 
    
         
            +
                        unless byte == nil
         
     | 
| 
      
 67 
     | 
    
         
            +
                          raise AMFException, "wanted amf3 first byte of 0x00, got #{byte}"
         
     | 
| 
      
 68 
     | 
    
         
            +
                        end
         
     | 
| 
      
 69 
     | 
    
         
            +
                      end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                      until io.eof?
         
     | 
| 
      
 72 
     | 
    
         
            +
                        self.values << des.deserialize(0, io)
         
     | 
| 
      
 73 
     | 
    
         
            +
                      end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                    rescue => e
         
     | 
| 
      
 76 
     | 
    
         
            +
                      self._amf_data = string
         
     | 
| 
      
 77 
     | 
    
         
            +
                      self._amf_error = e
         
     | 
| 
      
 78 
     | 
    
         
            +
                      self._amf_unparsed = io.read(100_000)
         
     | 
| 
      
 79 
     | 
    
         
            +
                    end
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                    self.command = values.delete_at(0)
         
     | 
| 
      
 82 
     | 
    
         
            +
                    self.transaction_id = values.delete_at(0)
         
     | 
| 
      
 83 
     | 
    
         
            +
                  end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                  def success?
         
     | 
| 
      
 86 
     | 
    
         
            +
                    (command != "_error") && _amf_error.nil?
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                  def self.next_transaction_id
         
     | 
| 
      
 90 
     | 
    
         
            +
                    self.transaction_id ||= 0
         
     | 
| 
      
 91 
     | 
    
         
            +
                    self.transaction_id += 1
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,48 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventMachine
         
     | 
| 
      
 2 
     | 
    
         
            +
              module RTMP
         
     | 
| 
      
 3 
     | 
    
         
            +
                class PendingRequest
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_accessor :request, :connection
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  # Create a new pending request from a request
         
     | 
| 
      
 7 
     | 
    
         
            +
                  #
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize(request, connection)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    self.request = request
         
     | 
| 
      
 11 
     | 
    
         
            +
                    self.connection = connection
         
     | 
| 
      
 12 
     | 
    
         
            +
                  end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  # Delete the current request from the list of pending requests
         
     | 
| 
      
 15 
     | 
    
         
            +
                  #
         
     | 
| 
      
 16 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 17 
     | 
    
         
            +
                  def delete
         
     | 
| 
      
 18 
     | 
    
         
            +
                    connection.pending_requests[request.header.message_type].delete(request.message.transaction_id.to_i)
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  # Find a request by message type and transaction id
         
     | 
| 
      
 22 
     | 
    
         
            +
                  #
         
     | 
| 
      
 23 
     | 
    
         
            +
                  # message_type - Symbol representing the message type (from header)
         
     | 
| 
      
 24 
     | 
    
         
            +
                  # transaction_id - Integer representing the transaction id
         
     | 
| 
      
 25 
     | 
    
         
            +
                  #
         
     | 
| 
      
 26 
     | 
    
         
            +
                  # Returns the request or nothing
         
     | 
| 
      
 27 
     | 
    
         
            +
                  def self.find(message_type, transaction_id, connection)
         
     | 
| 
      
 28 
     | 
    
         
            +
                    if connection.pending_requests[message_type]
         
     | 
| 
      
 29 
     | 
    
         
            +
                      connection.pending_requests[message_type][transaction_id.to_i]
         
     | 
| 
      
 30 
     | 
    
         
            +
                    end
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  # Create a request and add it to the pending requests hash
         
     | 
| 
      
 34 
     | 
    
         
            +
                  #
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # request - Request to add
         
     | 
| 
      
 36 
     | 
    
         
            +
                  #
         
     | 
| 
      
 37 
     | 
    
         
            +
                  # Returns the request
         
     | 
| 
      
 38 
     | 
    
         
            +
                  def self.create(request, connection)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    message_type = request.header.message_type
         
     | 
| 
      
 40 
     | 
    
         
            +
                    transaction_id = request.message.transaction_id.to_i
         
     | 
| 
      
 41 
     | 
    
         
            +
                    connection.pending_requests[message_type] ||= {}
         
     | 
| 
      
 42 
     | 
    
         
            +
                    connection.pending_requests[message_type][transaction_id] = new(request, connection)
         
     | 
| 
      
 43 
     | 
    
         
            +
                    connection.pending_requests[message_type][transaction_id]
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                end
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,101 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "em-rtmp/connection_delegate"
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module EventMachine
         
     | 
| 
      
 4 
     | 
    
         
            +
              module RTMP
         
     | 
| 
      
 5 
     | 
    
         
            +
                class Request < ConnectionDelegate
         
     | 
| 
      
 6 
     | 
    
         
            +
                  include EventMachine::Deferrable
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                  # An RTMP packet includes a header and a body. Each packet is typically no
         
     | 
| 
      
 9 
     | 
    
         
            +
                  # longer than 128 bytes, including the header. Multiple streams can be
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # ongoing (and interweaving) at the same time, so we track them via their
         
     | 
| 
      
 11 
     | 
    
         
            +
                  # stream id.
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  # The request implementation here references a Header object and a message body.
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # The body can be any object that responds to to_s.
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  attr_accessor :header, :body, :message
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  # Initialize, setting attributes
         
     | 
| 
      
 19 
     | 
    
         
            +
                  #
         
     | 
| 
      
 20 
     | 
    
         
            +
                  # attrs - Hash of attributes to write
         
     | 
| 
      
 21 
     | 
    
         
            +
                  #
         
     | 
| 
      
 22 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 23 
     | 
    
         
            +
                  def initialize(connection)
         
     | 
| 
      
 24 
     | 
    
         
            +
                    super connection
         
     | 
| 
      
 25 
     | 
    
         
            +
                    self.header = Header.new
         
     | 
| 
      
 26 
     | 
    
         
            +
                    self.message = Message.new
         
     | 
| 
      
 27 
     | 
    
         
            +
                    self.body = ""
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  # Updates the header to reflect the actual length of the body
         
     | 
| 
      
 31 
     | 
    
         
            +
                  #
         
     | 
| 
      
 32 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 33 
     | 
    
         
            +
                  def update_header
         
     | 
| 
      
 34 
     | 
    
         
            +
                    header.body_length = body.length
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  # Determines the proper chunk size for each packet we will send
         
     | 
| 
      
 38 
     | 
    
         
            +
                  #
         
     | 
| 
      
 39 
     | 
    
         
            +
                  # Returns the chunk size as an Integer
         
     | 
| 
      
 40 
     | 
    
         
            +
                  def chunk_size
         
     | 
| 
      
 41 
     | 
    
         
            +
                    @connection.chunk_size
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                  # Determines the number of chunks we will send
         
     | 
| 
      
 45 
     | 
    
         
            +
                  #
         
     | 
| 
      
 46 
     | 
    
         
            +
                  # Returns the chunk count as an Integer
         
     | 
| 
      
 47 
     | 
    
         
            +
                  def chunk_count
         
     | 
| 
      
 48 
     | 
    
         
            +
                    (body.length / chunk_size.to_f).ceil
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
                  # Splits the body into chunks for sending
         
     | 
| 
      
 52 
     | 
    
         
            +
                  #
         
     | 
| 
      
 53 
     | 
    
         
            +
                  # Returns an Array of Strings, each a chunk to send
         
     | 
| 
      
 54 
     | 
    
         
            +
                  def chunks
         
     | 
| 
      
 55 
     | 
    
         
            +
                    (0...chunk_count).map do |chunk|
         
     | 
| 
      
 56 
     | 
    
         
            +
                      offset_start = chunk_size * chunk
         
     | 
| 
      
 57 
     | 
    
         
            +
                      offset_end = [offset_start + chunk_size, body.length].min - 1
         
     | 
| 
      
 58 
     | 
    
         
            +
                      body[offset_start..offset_end]
         
     | 
| 
      
 59 
     | 
    
         
            +
                    end
         
     | 
| 
      
 60 
     | 
    
         
            +
                  end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  # Determine the proper header length for a given chunk
         
     | 
| 
      
 63 
     | 
    
         
            +
                  #
         
     | 
| 
      
 64 
     | 
    
         
            +
                  # Returns an Integer
         
     | 
| 
      
 65 
     | 
    
         
            +
                  def header_length_for_chunk(offset)
         
     | 
| 
      
 66 
     | 
    
         
            +
                    offset == 0 ? 12 : 1
         
     | 
| 
      
 67 
     | 
    
         
            +
                  end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                  # Update the header and send each chunk with an appropriate header.
         
     | 
| 
      
 70 
     | 
    
         
            +
                  #
         
     | 
| 
      
 71 
     | 
    
         
            +
                  # Returns the number of bytes written
         
     | 
| 
      
 72 
     | 
    
         
            +
                  def send
         
     | 
| 
      
 73 
     | 
    
         
            +
                    bytes_sent = 0
         
     | 
| 
      
 74 
     | 
    
         
            +
                    update_header
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
                    Logger.info "head: #{header.inspect}"
         
     | 
| 
      
 77 
     | 
    
         
            +
                    Logger.info "body: #{message.inspect}"
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                    for i in 0..(chunk_count-1)
         
     | 
| 
      
 80 
     | 
    
         
            +
                      self.header.header_length = header_length_for_chunk(i)
         
     | 
| 
      
 81 
     | 
    
         
            +
                      bytes_sent += send_chunk chunks[i]
         
     | 
| 
      
 82 
     | 
    
         
            +
                    end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
                    PendingRequest.create self, @connection
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                    bytes_sent
         
     | 
| 
      
 87 
     | 
    
         
            +
                  end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
                  # Send a chunk to the stream
         
     | 
| 
      
 90 
     | 
    
         
            +
                  #
         
     | 
| 
      
 91 
     | 
    
         
            +
                  # body - Body string to write
         
     | 
| 
      
 92 
     | 
    
         
            +
                  #
         
     | 
| 
      
 93 
     | 
    
         
            +
                  # Returns the number of bytes written
         
     | 
| 
      
 94 
     | 
    
         
            +
                  def send_chunk(chunk)
         
     | 
| 
      
 95 
     | 
    
         
            +
                    Logger.debug "sending chunk (#{chunk.length})", indent: 1
         
     | 
| 
      
 96 
     | 
    
         
            +
                    write(header.encode) + write(chunk)
         
     | 
| 
      
 97 
     | 
    
         
            +
                  end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
                end
         
     | 
| 
      
 100 
     | 
    
         
            +
              end
         
     | 
| 
      
 101 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,108 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventMachine
         
     | 
| 
      
 2 
     | 
    
         
            +
              module RTMP
         
     | 
| 
      
 3 
     | 
    
         
            +
                class Response < ConnectionDelegate
         
     | 
| 
      
 4 
     | 
    
         
            +
                  attr_accessor :channel_id, :header, :body, :message, :waiting_on_bytes
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                  # Initialize as a logical stream on a given stream ID
         
     | 
| 
      
 7 
     | 
    
         
            +
                  #
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Returns nothing.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  def initialize(channel_id, connection)
         
     | 
| 
      
 10 
     | 
    
         
            +
                    super connection
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    self.channel_id = channel_id
         
     | 
| 
      
 13 
     | 
    
         
            +
                    self.header = Header.new
         
     | 
| 
      
 14 
     | 
    
         
            +
                    self.body = ""
         
     | 
| 
      
 15 
     | 
    
         
            +
                    self.waiting_on_bytes = 0
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  # Reset the body (leave the header) between successful responses
         
     | 
| 
      
 19 
     | 
    
         
            +
                  #
         
     | 
| 
      
 20 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 21 
     | 
    
         
            +
                  def reset
         
     | 
| 
      
 22 
     | 
    
         
            +
                    self.body = ""
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  # Inherit values from a given header
         
     | 
| 
      
 26 
     | 
    
         
            +
                  #
         
     | 
| 
      
 27 
     | 
    
         
            +
                  # h - Header to add
         
     | 
| 
      
 28 
     | 
    
         
            +
                  #
         
     | 
| 
      
 29 
     | 
    
         
            +
                  # Returns the instance header
         
     | 
| 
      
 30 
     | 
    
         
            +
                  def add_header(header)
         
     | 
| 
      
 31 
     | 
    
         
            +
                    self.header += header
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  # Determines the proper chunk size from the connection
         
     | 
| 
      
 35 
     | 
    
         
            +
                  #
         
     | 
| 
      
 36 
     | 
    
         
            +
                  # Returns the chunk size as an Integer
         
     | 
| 
      
 37 
     | 
    
         
            +
                  def chunk_size
         
     | 
| 
      
 38 
     | 
    
         
            +
                    @connection.chunk_size
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  # Determines the proper amount of data to read this time around
         
     | 
| 
      
 42 
     | 
    
         
            +
                  #
         
     | 
| 
      
 43 
     | 
    
         
            +
                  # Returns the chunk size as an Integer
         
     | 
| 
      
 44 
     | 
    
         
            +
                  def read_size
         
     | 
| 
      
 45 
     | 
    
         
            +
                    if waiting_on_bytes > 0
         
     | 
| 
      
 46 
     | 
    
         
            +
                      waiting_on_bytes
         
     | 
| 
      
 47 
     | 
    
         
            +
                    else
         
     | 
| 
      
 48 
     | 
    
         
            +
                      [header.body_length - body.length, chunk_size].min
         
     | 
| 
      
 49 
     | 
    
         
            +
                    end
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                  # Read the next data chunk from the stream
         
     | 
| 
      
 53 
     | 
    
         
            +
                  #
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # Returns the instance body
         
     | 
| 
      
 55 
     | 
    
         
            +
                  def read_next_chunk
         
     | 
| 
      
 56 
     | 
    
         
            +
                    raise "No more data to read from stream" if header.body_length <= body.length
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    Logger.debug "want #{read_size} (#{body.length}/#{header.body_length})"
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                    desired_size = read_size
         
     | 
| 
      
 61 
     | 
    
         
            +
                    data = read(desired_size)
         
     | 
| 
      
 62 
     | 
    
         
            +
                    data_length = data ? data.length : 0
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                    if data_length > 0
         
     | 
| 
      
 65 
     | 
    
         
            +
                      self.body << data
         
     | 
| 
      
 66 
     | 
    
         
            +
                    end
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
                    if data_length != desired_size
         
     | 
| 
      
 69 
     | 
    
         
            +
                      self.waiting_on_bytes = desired_size - data_length
         
     | 
| 
      
 70 
     | 
    
         
            +
                    else
         
     | 
| 
      
 71 
     | 
    
         
            +
                      self.waiting_on_bytes = 0
         
     | 
| 
      
 72 
     | 
    
         
            +
                    end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                    self.body
         
     | 
| 
      
 75 
     | 
    
         
            +
                  end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                  # Determines whether or not we're in the middle of a chunk waiting for more
         
     | 
| 
      
 78 
     | 
    
         
            +
                  # data, or it's ok to go ahead and peek for a header.
         
     | 
| 
      
 79 
     | 
    
         
            +
                  #
         
     | 
| 
      
 80 
     | 
    
         
            +
                  # Returns true or false
         
     | 
| 
      
 81 
     | 
    
         
            +
                  def waiting_in_chunk?
         
     | 
| 
      
 82 
     | 
    
         
            +
                    waiting_on_bytes > 0
         
     | 
| 
      
 83 
     | 
    
         
            +
                  end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                  # Determine whether or not the stream is complete by checking the length
         
     | 
| 
      
 86 
     | 
    
         
            +
                  # of our body against that we expected from headers
         
     | 
| 
      
 87 
     | 
    
         
            +
                  #
         
     | 
| 
      
 88 
     | 
    
         
            +
                  # Returns true or false
         
     | 
| 
      
 89 
     | 
    
         
            +
                  def complete?
         
     | 
| 
      
 90 
     | 
    
         
            +
                    complete = body.length >= header.body_length
         
     | 
| 
      
 91 
     | 
    
         
            +
                    Logger.debug "response complete? #{complete} (#{body.length}/#{header.body_length})"
         
     | 
| 
      
 92 
     | 
    
         
            +
                    complete
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                  # Find or create a channel by ID
         
     | 
| 
      
 96 
     | 
    
         
            +
                  #
         
     | 
| 
      
 97 
     | 
    
         
            +
                  # channel_id - ID of channel to find or create
         
     | 
| 
      
 98 
     | 
    
         
            +
                  # connection - Connection to attach
         
     | 
| 
      
 99 
     | 
    
         
            +
                  #
         
     | 
| 
      
 100 
     | 
    
         
            +
                  # Returns a Response instance
         
     | 
| 
      
 101 
     | 
    
         
            +
                  def self.find_or_create(channel_id, connection)
         
     | 
| 
      
 102 
     | 
    
         
            +
                    connection.channels[channel_id] ||= Response.new(channel_id, connection)
         
     | 
| 
      
 103 
     | 
    
         
            +
                    connection.channels[channel_id]
         
     | 
| 
      
 104 
     | 
    
         
            +
                  end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
                end
         
     | 
| 
      
 107 
     | 
    
         
            +
              end
         
     | 
| 
      
 108 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,130 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module EventMachine
         
     | 
| 
      
 2 
     | 
    
         
            +
              module RTMP
         
     | 
| 
      
 3 
     | 
    
         
            +
                class ResponseRouter < ConnectionDelegate
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
                  attr_accessor :active_response
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                  # Create a new response router object to delegate to. Start with a state
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # of looking for a fresh header.
         
     | 
| 
      
 9 
     | 
    
         
            +
                  #
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 11 
     | 
    
         
            +
                  def initialize(connection)
         
     | 
| 
      
 12 
     | 
    
         
            +
                    super connection
         
     | 
| 
      
 13 
     | 
    
         
            +
                    @state = :wait_header
         
     | 
| 
      
 14 
     | 
    
         
            +
                  end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  # Called by the connection when the buffer changes and it's appropriate to
         
     | 
| 
      
 17 
     | 
    
         
            +
                  # delegate to the response router. Take action depending on our state.
         
     | 
| 
      
 18 
     | 
    
         
            +
                  #
         
     | 
| 
      
 19 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 20 
     | 
    
         
            +
                  def buffer_changed
         
     | 
| 
      
 21 
     | 
    
         
            +
                    case state
         
     | 
| 
      
 22 
     | 
    
         
            +
                    when :wait_header
         
     | 
| 
      
 23 
     | 
    
         
            +
                      header = Header.read_from_connection(@connection)
         
     | 
| 
      
 24 
     | 
    
         
            +
                      Logger.debug "routing new header channel=#{header.channel_id}, type=#{header.message_type_id} length=#{header.body_length}"
         
     | 
| 
      
 25 
     | 
    
         
            +
                      receive_header header
         
     | 
| 
      
 26 
     | 
    
         
            +
                    when :wait_chunk
         
     | 
| 
      
 27 
     | 
    
         
            +
                      receive_chunk active_response
         
     | 
| 
      
 28 
     | 
    
         
            +
                    end
         
     | 
| 
      
 29 
     | 
    
         
            +
                  end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  # Receive a fresh header, add it to the appropriate response and receive
         
     | 
| 
      
 32 
     | 
    
         
            +
                  # a chunk of data for that response.
         
     | 
| 
      
 33 
     | 
    
         
            +
                  #
         
     | 
| 
      
 34 
     | 
    
         
            +
                  # header - Header to receive and act on
         
     | 
| 
      
 35 
     | 
    
         
            +
                  #
         
     | 
| 
      
 36 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 37 
     | 
    
         
            +
                  def receive_header(header)
         
     | 
| 
      
 38 
     | 
    
         
            +
                    response = Response.find_or_create(header.channel_id, @connection)
         
     | 
| 
      
 39 
     | 
    
         
            +
                    response.add_header header
         
     | 
| 
      
 40 
     | 
    
         
            +
                    receive_chunk response
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
                  # Receive a chunk of data for a given response. Change our state depending
         
     | 
| 
      
 44 
     | 
    
         
            +
                  # on the result of the chunk read. If it was read in full, we'll look for
         
     | 
| 
      
 45 
     | 
    
         
            +
                  # a header next time around. Otherwise, we will continue to read into that
         
     | 
| 
      
 46 
     | 
    
         
            +
                  # chunk until it is satisfied.
         
     | 
| 
      
 47 
     | 
    
         
            +
                  #
         
     | 
| 
      
 48 
     | 
    
         
            +
                  # If the response is completely received, we'll clone it and route that to
         
     | 
| 
      
 49 
     | 
    
         
            +
                  # the appropriate action, then reset that response so that it can receive something
         
     | 
| 
      
 50 
     | 
    
         
            +
                  # else in the future.
         
     | 
| 
      
 51 
     | 
    
         
            +
                  #
         
     | 
| 
      
 52 
     | 
    
         
            +
                  # response - the Response object to act on
         
     | 
| 
      
 53 
     | 
    
         
            +
                  #
         
     | 
| 
      
 54 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 55 
     | 
    
         
            +
                  def receive_chunk(response)
         
     | 
| 
      
 56 
     | 
    
         
            +
                    response.read_next_chunk
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                    if response.waiting_in_chunk?
         
     | 
| 
      
 59 
     | 
    
         
            +
                      self.active_response = response
         
     | 
| 
      
 60 
     | 
    
         
            +
                      change_state :wait_chunk
         
     | 
| 
      
 61 
     | 
    
         
            +
                    else
         
     | 
| 
      
 62 
     | 
    
         
            +
                      self.active_response = nil
         
     | 
| 
      
 63 
     | 
    
         
            +
                      change_state :wait_header
         
     | 
| 
      
 64 
     | 
    
         
            +
                    end
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                    if response.complete?
         
     | 
| 
      
 67 
     | 
    
         
            +
                      Logger.debug "response is complete, routing it!"
         
     | 
| 
      
 68 
     | 
    
         
            +
                      route_response response.dup
         
     | 
| 
      
 69 
     | 
    
         
            +
                      response.reset
         
     | 
| 
      
 70 
     | 
    
         
            +
                    end
         
     | 
| 
      
 71 
     | 
    
         
            +
                  end
         
     | 
| 
      
 72 
     | 
    
         
            +
             
     | 
| 
      
 73 
     | 
    
         
            +
                  # Route any response to its proper destination. AMF responses are routed to their
         
     | 
| 
      
 74 
     | 
    
         
            +
                  # pending request. Chunk size updates the connection, others are ignored for now.
         
     | 
| 
      
 75 
     | 
    
         
            +
                  #
         
     | 
| 
      
 76 
     | 
    
         
            +
                  # response - Response object to route or act on.
         
     | 
| 
      
 77 
     | 
    
         
            +
                  #
         
     | 
| 
      
 78 
     | 
    
         
            +
                  # Returns nothing.
         
     | 
| 
      
 79 
     | 
    
         
            +
                  def route_response(response)
         
     | 
| 
      
 80 
     | 
    
         
            +
                    case response.header.message_type
         
     | 
| 
      
 81 
     | 
    
         
            +
                    when :amf0
         
     | 
| 
      
 82 
     | 
    
         
            +
                      response.message = Message.new version: 0
         
     | 
| 
      
 83 
     | 
    
         
            +
                      response.message.decode response.body
         
     | 
| 
      
 84 
     | 
    
         
            +
                      Logger.info "head: #{response.header.inspect}"
         
     | 
| 
      
 85 
     | 
    
         
            +
                      Logger.info "amf0: #{response.message.inspect}"
         
     | 
| 
      
 86 
     | 
    
         
            +
                      route_amf :amf0, response
         
     | 
| 
      
 87 
     | 
    
         
            +
                    when :amf3
         
     | 
| 
      
 88 
     | 
    
         
            +
                      response.message = Message.new version: 3
         
     | 
| 
      
 89 
     | 
    
         
            +
                      response.message.decode response.body
         
     | 
| 
      
 90 
     | 
    
         
            +
                      Logger.info "head: #{response.header.inspect}"
         
     | 
| 
      
 91 
     | 
    
         
            +
                      Logger.info "amf3: #{response.message.inspect}"
         
     | 
| 
      
 92 
     | 
    
         
            +
                      route_amf :amf3, response
         
     | 
| 
      
 93 
     | 
    
         
            +
                    when :chunk_size
         
     | 
| 
      
 94 
     | 
    
         
            +
                      connection.chunk_size = response.body.unpack('N')[0]
         
     | 
| 
      
 95 
     | 
    
         
            +
                      Logger.info "setting chunk_size to #{chunk_size}"
         
     | 
| 
      
 96 
     | 
    
         
            +
                    when :ack_size
         
     | 
| 
      
 97 
     | 
    
         
            +
                      ack_size = response.body.unpack('N')[0]
         
     | 
| 
      
 98 
     | 
    
         
            +
                      Logger.info "setting ack_size to #{ack_size}"
         
     | 
| 
      
 99 
     | 
    
         
            +
                    when :bandwidth
         
     | 
| 
      
 100 
     | 
    
         
            +
                      bandwidth = response.body[0..3].unpack('N')[0]
         
     | 
| 
      
 101 
     | 
    
         
            +
                      bandwidth_type = response.body[4].unpack('c')[0]
         
     | 
| 
      
 102 
     | 
    
         
            +
                      Logger.info "setting bandwidth to #{bandwidth} (#{bandwidth_type})"
         
     | 
| 
      
 103 
     | 
    
         
            +
                    else
         
     | 
| 
      
 104 
     | 
    
         
            +
                      Logger.info "cannot route unknown response: #{response.inspect}"
         
     | 
| 
      
 105 
     | 
    
         
            +
                    end
         
     | 
| 
      
 106 
     | 
    
         
            +
                  end
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                  # Route an AMF response to it's pending request
         
     | 
| 
      
 109 
     | 
    
         
            +
                  #
         
     | 
| 
      
 110 
     | 
    
         
            +
                  # version - AMF version (:amf0 or :amf3)
         
     | 
| 
      
 111 
     | 
    
         
            +
                  # response - Response object
         
     | 
| 
      
 112 
     | 
    
         
            +
                  #
         
     | 
| 
      
 113 
     | 
    
         
            +
                  # Returns nothing
         
     | 
| 
      
 114 
     | 
    
         
            +
                  def route_amf(version, response)
         
     | 
| 
      
 115 
     | 
    
         
            +
                    Logger.debug "routing #{version} response for tid #{response.message.transaction_id}"
         
     | 
| 
      
 116 
     | 
    
         
            +
                    if pending_request = PendingRequest.find(version, response.message.transaction_id, @connection)
         
     | 
| 
      
 117 
     | 
    
         
            +
                      if response.message.success?
         
     | 
| 
      
 118 
     | 
    
         
            +
                        pending_request.request.succeed(response)
         
     | 
| 
      
 119 
     | 
    
         
            +
                      else
         
     | 
| 
      
 120 
     | 
    
         
            +
                        pending_request.request.fail(response)
         
     | 
| 
      
 121 
     | 
    
         
            +
                      end
         
     | 
| 
      
 122 
     | 
    
         
            +
                      pending_request.delete
         
     | 
| 
      
 123 
     | 
    
         
            +
                    else
         
     | 
| 
      
 124 
     | 
    
         
            +
                      Logger.error "unable to find a matching transaction"
         
     | 
| 
      
 125 
     | 
    
         
            +
                    end
         
     | 
| 
      
 126 
     | 
    
         
            +
                  end
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                end
         
     | 
| 
      
 129 
     | 
    
         
            +
              end
         
     | 
| 
      
 130 
     | 
    
         
            +
            end
         
     |