em-rtmp 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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