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
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# EM-RTMP
|
2
|
+
|
3
|
+
Asynchronous RTMP client powered by EventMachine.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
require "em-rtmp"
|
9
|
+
# Start the EventMachine
|
10
|
+
EventMachine.run do
|
11
|
+
|
12
|
+
# Establish a connection
|
13
|
+
connection = EventMachine::RTMP.connect 'flashserver.bigmediacompany.com'
|
14
|
+
|
15
|
+
# Issue an RTMP connect after the RTMP handshake
|
16
|
+
connection.on_handshake_complete do
|
17
|
+
EventMachine::RTMP::ConnectRequest.new(connection).send
|
18
|
+
end
|
19
|
+
|
20
|
+
# After the RTMP connect succeeds, continue on
|
21
|
+
connection.on_ready do
|
22
|
+
...
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
desc 'Run all specs in the spec directory'
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
desc 'Load an interactive console in our environment'
|
12
|
+
task :console do
|
13
|
+
require "eventmachine"
|
14
|
+
require "em-rtmp"
|
15
|
+
require "irb"
|
16
|
+
require "irb/completion"
|
17
|
+
module EventMachine
|
18
|
+
module RTMP
|
19
|
+
ARGV.clear
|
20
|
+
IRB.start
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'Start the test'
|
26
|
+
task :test do
|
27
|
+
require "eventmachine"
|
28
|
+
require "em-rtmp"
|
29
|
+
EventMachine.run do
|
30
|
+
connection = EventMachine::RTMP.ssl_connect '216.133.234.22', 2099
|
31
|
+
|
32
|
+
connection.on_handshake_complete do
|
33
|
+
req = EventMachine::RTMP::ConnectRequest.new connection
|
34
|
+
req.swfUrl = 'app:/mod_ser.dat'
|
35
|
+
req.tcUrl = 'rtmps://prod.na1.lol.riotgames.com:2099'
|
36
|
+
req.send
|
37
|
+
end
|
38
|
+
|
39
|
+
connection.on_ready do
|
40
|
+
obj = RocketAMF::Values::RemotingMessage.new
|
41
|
+
obj.destination = "playerStatsService"
|
42
|
+
obj.operation = "getRecentGames"
|
43
|
+
obj.body = [32310898]
|
44
|
+
|
45
|
+
req = EventMachine::RTMP::Request.new connection
|
46
|
+
req.header.channel_id = 3
|
47
|
+
req.header.message_stream_id = 0
|
48
|
+
req.header.message_type_id = 17
|
49
|
+
|
50
|
+
req.message.version = 3
|
51
|
+
req.message.command = nil
|
52
|
+
req.message.values = [obj]
|
53
|
+
|
54
|
+
req.body = req.message.encode
|
55
|
+
|
56
|
+
req.callback do |res|
|
57
|
+
p "I GOT A MOTHERFUCKING CALLBACK!"
|
58
|
+
end
|
59
|
+
|
60
|
+
req.errback do |res|
|
61
|
+
p "Couldn't do it..."
|
62
|
+
end
|
63
|
+
|
64
|
+
req.send
|
65
|
+
end
|
66
|
+
#
|
67
|
+
# connection.on_ready do
|
68
|
+
# obj = RocketAMF::Values::RemotingMessage.new
|
69
|
+
# obj.destination = "playerStatsService"
|
70
|
+
# obj.operation = "getRecentGames"
|
71
|
+
# obj.body = [32310898]
|
72
|
+
#
|
73
|
+
# req = EventMachine::RTMP::Request.new connection
|
74
|
+
# req.header.channel_id = 3
|
75
|
+
# req.header.message_stream_id = 0
|
76
|
+
# req.header.message_type_id = 17
|
77
|
+
#
|
78
|
+
# req.message.version = 3
|
79
|
+
# req.message.command = nil
|
80
|
+
# req.message.values = [obj]
|
81
|
+
#
|
82
|
+
# req.body = req.message.encode
|
83
|
+
#
|
84
|
+
# req.callback do |res|
|
85
|
+
# p "I GOT A MOTHERFUCKING CALLBACK!"
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# req.errback do |res|
|
89
|
+
# p "Couldn't do it..."
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# req.send
|
93
|
+
# end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
data/lib/em-rtmp.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "eventmachine"
|
2
|
+
require "rocketamf"
|
3
|
+
|
4
|
+
require "em-rtmp/buffer"
|
5
|
+
require "em-rtmp/connection"
|
6
|
+
require "em-rtmp/connect_request"
|
7
|
+
require "em-rtmp/handshake"
|
8
|
+
require "em-rtmp/heartbeat"
|
9
|
+
require "em-rtmp/header"
|
10
|
+
require "em-rtmp/logger"
|
11
|
+
require "em-rtmp/message"
|
12
|
+
require "em-rtmp/pending_request"
|
13
|
+
require "em-rtmp/request"
|
14
|
+
require "em-rtmp/response"
|
15
|
+
require "em-rtmp/response_router"
|
16
|
+
require "em-rtmp/rtmp"
|
17
|
+
require "em-rtmp/uuid"
|
18
|
+
require "em-rtmp/version"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "stringio"
|
2
|
+
require "em-rtmp/io_helpers"
|
3
|
+
|
4
|
+
module EventMachine
|
5
|
+
module RTMP
|
6
|
+
class Buffer < StringIO
|
7
|
+
include EventMachine::RTMP::IOHelpers
|
8
|
+
|
9
|
+
# Gets the number of remaining bytes in the buffer
|
10
|
+
#
|
11
|
+
# Returns an Integer representing the number of bytes left
|
12
|
+
def remaining
|
13
|
+
length - pos
|
14
|
+
end
|
15
|
+
|
16
|
+
# Truncate the buffer to nothing and set the position to zero
|
17
|
+
#
|
18
|
+
# Returns the new position (zero)
|
19
|
+
def reset
|
20
|
+
truncate 0
|
21
|
+
seek 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# Append to the end of the string without changing our position
|
25
|
+
#
|
26
|
+
# Returns nothing
|
27
|
+
def append(string)
|
28
|
+
self.string << string
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require "em-rtmp/request"
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module RTMP
|
5
|
+
class ConnectRequest < Request
|
6
|
+
|
7
|
+
DEFAULT_PARAMETERS = {
|
8
|
+
app: '',
|
9
|
+
flashVer: 'WIN 10,1,85,3',
|
10
|
+
swfUrl: '',
|
11
|
+
tcUrl: '',
|
12
|
+
fpad: false,
|
13
|
+
capabilities: 239,
|
14
|
+
audioCodecs: 3191,
|
15
|
+
videoCodecs: 252,
|
16
|
+
videoFunction: 1,
|
17
|
+
pageUrl: nil,
|
18
|
+
objectEncoding: 3
|
19
|
+
}
|
20
|
+
|
21
|
+
attr_accessor :app, :flashVer, :swfUrl, :tcUrl, :fpad, :capabilities,
|
22
|
+
:audioCodecs, :videoCodecs, :videoFunction, :pageUrl, :objectEncoding
|
23
|
+
|
24
|
+
# Initialize the request and set sane defaults for an RTMP connect
|
25
|
+
# request (transaction_id, command, header values)
|
26
|
+
#
|
27
|
+
# Returns nothing
|
28
|
+
def initialize(connection)
|
29
|
+
super connection
|
30
|
+
|
31
|
+
self.header.channel_id = 3
|
32
|
+
self.header.message_type = :amf0
|
33
|
+
self.header
|
34
|
+
|
35
|
+
self.message.version = 0
|
36
|
+
self.message.transaction_id = 1
|
37
|
+
self.message.command = "connect"
|
38
|
+
|
39
|
+
callback do |res|
|
40
|
+
Logger.debug "rtmp connect failed, becoming ready"
|
41
|
+
@connection.change_state :ready
|
42
|
+
end
|
43
|
+
|
44
|
+
errback do |res|
|
45
|
+
Logger.debug "rtmp connect failed, closing connection"
|
46
|
+
@connection.close_connection
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Construct a list of parameters given our class attributes, used
|
51
|
+
# to form the connect message object.
|
52
|
+
#
|
53
|
+
# Returns a Hash of symbols => values
|
54
|
+
def parameters
|
55
|
+
instance_values = Hash[instance_variables.map {|k| [k.to_s[1..-1].to_sym, instance_variable_get(k)]}]
|
56
|
+
DEFAULT_PARAMETERS.merge instance_values.select {|k,v| DEFAULT_PARAMETERS.key? k }
|
57
|
+
end
|
58
|
+
|
59
|
+
def command_message
|
60
|
+
cm = RocketAMF::Values::CommandMessage.new
|
61
|
+
cm.operation = 5
|
62
|
+
cm.correlationId = ""
|
63
|
+
cm.timestamp = 0
|
64
|
+
cm.timeToLive = 0
|
65
|
+
cm.messageId = UUID.random
|
66
|
+
cm.body = {}
|
67
|
+
cm.destination = ""
|
68
|
+
cm.headers = { "DSMessagingVersion" => 1, "DSId" => "my-rtmps" }
|
69
|
+
cm
|
70
|
+
end
|
71
|
+
|
72
|
+
# Given the specific nature of a connect request, we can just set the message
|
73
|
+
# values to our params then encode that as our body before sending.
|
74
|
+
#
|
75
|
+
# Returns the result of super
|
76
|
+
def send
|
77
|
+
self.message.values = [parameters, false, "nil", "", command_message]
|
78
|
+
self.body = message.encode
|
79
|
+
super
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require "em-rtmp/io_helpers"
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module RTMP
|
5
|
+
class Connection < EventMachine::Connection
|
6
|
+
include IOHelpers
|
7
|
+
|
8
|
+
attr_accessor :state, :chunk_size, :channels, :pending_requests
|
9
|
+
|
10
|
+
# Initialize the connection and setup our handshake
|
11
|
+
#
|
12
|
+
# Returns nothing
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
@buffer = Buffer.new
|
16
|
+
@chunk_size = 128
|
17
|
+
@response_router = ResponseRouter.new(self)
|
18
|
+
@handshake = Handshake.new(self)
|
19
|
+
@callbacks = { :handshake_complete => [], :ready => [], :disconnected => [] }
|
20
|
+
@channels = []
|
21
|
+
@pending_requests = {}
|
22
|
+
@state = :connecting
|
23
|
+
end
|
24
|
+
|
25
|
+
# Used to track changes in state
|
26
|
+
#
|
27
|
+
# state - Symbol, new state to enter
|
28
|
+
#
|
29
|
+
# Returns nothing
|
30
|
+
def change_state(state)
|
31
|
+
return if @state == state
|
32
|
+
Logger.info "state changed from #{@state} to #{state}", caller: caller
|
33
|
+
@state = state
|
34
|
+
run_callbacks state
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start the RTMP handshake process
|
38
|
+
#
|
39
|
+
# Returns nothing
|
40
|
+
def begin_rtmp_handshake
|
41
|
+
change_state :handshake
|
42
|
+
@handshake.issue_challenge
|
43
|
+
end
|
44
|
+
|
45
|
+
# Called to add a callback for when the RTMP handshake is
|
46
|
+
# completed. Most useful for issuing an RTMP connect request.
|
47
|
+
#
|
48
|
+
# blk - block to execute
|
49
|
+
#
|
50
|
+
# Returns nothing
|
51
|
+
def on_handshake_complete(&blk)
|
52
|
+
@callbacks[:handshake_complete] << blk
|
53
|
+
end
|
54
|
+
|
55
|
+
# Called to add a callback for when the RTMP connection has
|
56
|
+
# been established and is ready for work.
|
57
|
+
#
|
58
|
+
# blk - block to execute
|
59
|
+
#
|
60
|
+
# Returns nothing
|
61
|
+
def on_ready(&blk)
|
62
|
+
@callbacks[:ready] << blk
|
63
|
+
end
|
64
|
+
|
65
|
+
# Called to add a callback for when the TCP connection has
|
66
|
+
# been disconnected.
|
67
|
+
#
|
68
|
+
# blk - block to execute
|
69
|
+
#
|
70
|
+
# Returns nothing
|
71
|
+
def on_disconnected(&blk)
|
72
|
+
@callbacks[:disconnected] << blk
|
73
|
+
end
|
74
|
+
|
75
|
+
# Called to run the callbacks for a specific event
|
76
|
+
#
|
77
|
+
# event - symbol representing the event to run callbacks for
|
78
|
+
#
|
79
|
+
# Returns nothing
|
80
|
+
def run_callbacks(event)
|
81
|
+
if @callbacks.keys.include? event
|
82
|
+
@callbacks[event].each do |blk|
|
83
|
+
blk.call
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Reads from the buffer, to facilitate IO operations
|
89
|
+
#
|
90
|
+
# Returns the result of the read
|
91
|
+
def read(length)
|
92
|
+
Logger.debug "reading #{length} bytes from buffer"
|
93
|
+
@buffer.read length
|
94
|
+
end
|
95
|
+
|
96
|
+
# Writes data to the EventMachine connection
|
97
|
+
#
|
98
|
+
# Returns nothing
|
99
|
+
def write(data)
|
100
|
+
Logger.debug "sending #{data.length} bytes to stream"
|
101
|
+
send_data data
|
102
|
+
end
|
103
|
+
|
104
|
+
# Obtain the number of bytes waiting to be read in the buffer
|
105
|
+
#
|
106
|
+
# Returns an Integer
|
107
|
+
def bytes_waiting
|
108
|
+
@buffer.remaining
|
109
|
+
end
|
110
|
+
|
111
|
+
# Perform the next step after the connection has been established
|
112
|
+
# Called by the Event Machine
|
113
|
+
#
|
114
|
+
# Returns nothing
|
115
|
+
def connection_completed
|
116
|
+
Logger.info "connection completed, issuing rtmp handshake"
|
117
|
+
begin_rtmp_handshake
|
118
|
+
end
|
119
|
+
|
120
|
+
# Change our state to disconnected if we lose the connection.
|
121
|
+
# Called by the Event Machine
|
122
|
+
#
|
123
|
+
# Returns nothing
|
124
|
+
def unbind
|
125
|
+
Logger.info "disconnected from peer"
|
126
|
+
change_state :disconnected
|
127
|
+
end
|
128
|
+
|
129
|
+
# Receives data and offers it to the appropriate delegate object.
|
130
|
+
# Fires a method call to buffer_changed to take action.
|
131
|
+
# Called by the Event machine
|
132
|
+
#
|
133
|
+
# data - data received
|
134
|
+
#
|
135
|
+
# Returns nothing
|
136
|
+
def receive_data(data)
|
137
|
+
Logger.debug "received #{data.length} bytes"
|
138
|
+
@buffer.append data
|
139
|
+
buffer_changed
|
140
|
+
end
|
141
|
+
|
142
|
+
# Called when the buffer is changed, indicating that we may want
|
143
|
+
# to change state or delegate to another object
|
144
|
+
#
|
145
|
+
# Returns nothing
|
146
|
+
def buffer_changed
|
147
|
+
|
148
|
+
loop do
|
149
|
+
break if bytes_waiting < 1
|
150
|
+
|
151
|
+
case state
|
152
|
+
|
153
|
+
when :handshake
|
154
|
+
if @handshake.buffer_changed == :handshake_complete
|
155
|
+
@handshake = nil
|
156
|
+
change_state :handshake_complete
|
157
|
+
end
|
158
|
+
break
|
159
|
+
|
160
|
+
when :handshake_complete, :ready
|
161
|
+
@response_router.buffer_changed
|
162
|
+
next
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# A secure connection behaves identically except that it delays the
|
171
|
+
# RTMP handshake until after the ssl handshake occurs.
|
172
|
+
class SecureConnection < Connection
|
173
|
+
|
174
|
+
# When the connection is established, make it secure before
|
175
|
+
# starting the RTMP handshake process.
|
176
|
+
def connection_completed
|
177
|
+
Logger.info "connection completed, starting tls"
|
178
|
+
start_tls verify_peer: false
|
179
|
+
end
|
180
|
+
|
181
|
+
# Connection is now secure, issue the RTMP handshake challenge
|
182
|
+
def ssl_handshake_completed
|
183
|
+
Logger.info "ssl handshake completed, issuing rtmp handshake"
|
184
|
+
begin_rtmp_handshake
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
end
|