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
|