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