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.
@@ -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