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 ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
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