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