vncrec 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/vncrec.rb ADDED
@@ -0,0 +1,6 @@
1
+ module VNCRec
2
+ end
3
+
4
+ require 'vncrec/constants.rb'
5
+ require 'vncrec/recorder.rb'
6
+ require 'vncrec/rfb/proxy.rb'
@@ -0,0 +1,10 @@
1
+ module VNCRec
2
+
3
+ ENC_RAW = 0
4
+ ENC_HEXTILE = 5
5
+ ENC_ZRLE = 16
6
+
7
+ PIX_FMT_BGR8 = { :bpp=> 8, :depth=> 8, :bend=> 0, :tcol=> 0, :rmax=> 0x7, :gmax=> 0x7, :bmax=> 0x3, :rshif=> 5, :gshif=> 2, :bshif=> 0, string: "bgr8" }
8
+ PIX_FMT_BGRA = { :bpp=> 32, :depth=> 24, :bend=> 0, :tcol=> 1, :rmax=> 0xFF, :gmax=> 0xFF, :bmax=> 0xFF, :rshif=> 16, :gshif=> 8, :bshif=> 0, string: "bgra" }
9
+
10
+ end
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ # @encoding "utf-8"
4
+
5
+ require 'socket'
6
+ require 'zlib'
7
+ require 'stringio'
8
+ require 'logger'
9
+ begin
10
+ require 'logger/colors'
11
+ rescue LoadError
12
+ end
13
+
14
+ require 'vncrec/constants.rb'
15
+ require 'vncrec/rfb/proxy.rb'
16
+ require 'vncrec/writers.rb'
17
+
18
+ module VNCRec
19
+ # A recorder itself.
20
+
21
+ class Recorder
22
+ DEFAULTS = {
23
+ :pix_fmt => :BGR8,
24
+ :debug => nil,
25
+ :encoding => VNCRec::ENC_RAW,
26
+ :filename => nil,
27
+ :fps => 6,
28
+ :input => nil,
29
+ :port => 5900
30
+ }
31
+
32
+ # @param geometry [String] geometry of the screen area to capture(+x,y offset is not implemented yet)
33
+ # @param options [Hash] a list of available options:
34
+ # * port
35
+ # * fps
36
+ # * filename (pattern 'DATE' in filename will be substituted by current date_time.)
37
+ # * encoding [ VNCRec::ENC_RAW | VNCRec::ENC_HEXTILE ]"
38
+ # * pix_fmt ["bgr8" | "bgra"] (string || symbol, case insens.)
39
+ # * ffmpeg_iv_opts ffmpeg input video options
40
+ # * ffmpeg_ia_opts ffmpeg input audio options
41
+ # * ffmpeg_out_opts ffmpeg output options
42
+ # * log/logger/logging(bool)
43
+ #
44
+ attr_accessor :on_exit
45
+
46
+ def initialize(options = {})
47
+ options = VNCRec::Recorder::DEFAULTS.merge(options)
48
+ @logging = options[:logging] || options[:logger] || options[:log] || false
49
+ if @logging
50
+ @logger = Logger.new STDERR
51
+ @logger.datetime_format = '% d_%m %H-%M-%S.%6N'
52
+ @logger.info options.inspect
53
+ end
54
+
55
+ @debug = options[:debug]
56
+ $stderr.puts 'Debug mode' if @debug
57
+
58
+ @port = options[:port]
59
+ fail ArgumentError, 'Invalid port value' unless @port.is_a?(Numeric) && (1024..65_535).include?(@port)
60
+
61
+ @host = options[:host]
62
+
63
+ @client = nil
64
+
65
+ @framerate = options[:fps]
66
+ fail ArgumentError if !@framerate.is_a?(Numeric) || @framerate <= 0
67
+
68
+ @filename = options[:filename] || (options[:port].to_s + '.raw')
69
+ fail "Cannot create file #{@filename}" unless system "touch #{@filename}"
70
+
71
+ if options[:geometry]
72
+ @geometry = options[:geometry]
73
+ fail ArgumentError, "Geometry is invalid, expected: <x>x<y>, \
74
+ got: #{@geometry.inspect}" unless valid_geometry?(@geometry)
75
+ end
76
+
77
+ @enc = options[:encoding]
78
+
79
+ pf = options[:pix_fmt].to_s.dup.prepend('PIX_FMT_').upcase.to_sym
80
+ fail ArgumentError, "Unknown pix_fmt #{options[:pix_fmt]}" unless VNCRec.const_defined? pf
81
+ @pix_fmt = VNCRec.const_get(pf)
82
+
83
+ @ffmpeg_iv_opts = options[:ffmpeg_iv_opts]
84
+ @ffmpeg_ia_opts = options[:ffmpeg_ia_opts]
85
+ @ffmpeg_out_opts = options[:ffmpeg_out_opts]
86
+ Thread.abort_on_exception = true
87
+ @on_exit = [:close_file, :close_proxy]
88
+
89
+ @file = nil
90
+ @sleep_time = 0.01
91
+ @recording_starttime = nil
92
+ end
93
+
94
+ # Start routine: wait for connection,
95
+ # perform handshake, get data, write data.
96
+ # Non-blocking.
97
+ def run
98
+ @loop = Thread.new do
99
+ routine
100
+ end
101
+ end
102
+
103
+ # Safely stop any interaction with VNC server, close file.
104
+ # Execute all on_exit hooks.
105
+ # @param error [Integer] exit code
106
+
107
+ def stop
108
+ @loop.kill unless Thread.current == @loop
109
+ @on_exit.each do |bl|
110
+ send bl if bl.is_a? Symbol
111
+ bl.call if bl.respond_to?(:call)
112
+ end
113
+ end
114
+
115
+ # Return current size of file
116
+ # @return size [Integer]
117
+ def filesize
118
+ @file.size
119
+ end
120
+
121
+ # Find out if main loop thread is alive.
122
+ # @return [bool]
123
+ def stopped?
124
+ !(@loop.nil? && @loop.alive?)
125
+ end
126
+
127
+ def running?
128
+ @loop && @loop.alive?
129
+ end
130
+
131
+ private
132
+
133
+ def close_file # !FIXME
134
+ return unless @file
135
+ @file.close unless @file.closed?
136
+ sleep 0.1 until @file.closed?
137
+ substitute_filename
138
+ end
139
+
140
+ def substitute_filename
141
+ File.rename(@filename,
142
+ @filename.gsub('DATE', '%Y_%m_%d_%Hh_%Mm_%Ss')
143
+ ) if @recording_starttime && @filename['DATE']
144
+ end
145
+
146
+ def close_proxy
147
+ @server.close if @server && !@server.closed?
148
+ end
149
+
150
+ def ready_read?
151
+ res = IO.select([@client], nil, nil, 0)
152
+ !res.nil?
153
+ end
154
+
155
+ def routine
156
+
157
+ if @host
158
+ @logger.info "connecting to #{@host}:#{@port}" if @logging
159
+ @server = TCPSocket.new(@host, @port)
160
+ @logger.info 'connection established' if @logging
161
+ else
162
+ @logger.info 'starting server' if @logging
163
+ @server = TCPServer.new(@port).accept
164
+ @logger.info 'got client' if @logging
165
+
166
+ end
167
+ @client = VNCRec::RFB::Proxy.new(@server, 'RFB 003.008', @enc, @pix_fmt)
168
+ @recording_starttime = Time.now if @filename.include?('DATE')
169
+
170
+ w, h, name = @client.handshake
171
+ @geometry ||= "#{w}x#{h}"
172
+ parse_geometry
173
+
174
+ @client.prepare_framebuffer(@w, @h, @pix_fmt[:bpp])
175
+ @logger.info "server geometry: #{w}x#{h}" if @logging
176
+ @logger.info "requested geometry: #{@w}x#{@h}" if @logging
177
+
178
+ @file = VNCRec::Writers.get_writer(
179
+ @filename,
180
+ geometry: @geometry,
181
+ fps: @framerate,
182
+ pix_fmt: @pix_fmt[:string],
183
+ ffmpeg_iv_opts: @ffmpeg_iv_opts,
184
+ ffmpeg_ia_opts: @ffmpeg_ia_opts,
185
+ ffmpeg_out_opts: @ffmpeg_out_opts)
186
+
187
+ if name.nil?
188
+ @logger.error 'Error in handshake' if @logging
189
+ stop
190
+ else
191
+ @logger.info "Ok. Server: #{name}" if @logging
192
+ end
193
+
194
+ unless @client.set_encodings [@enc]
195
+ @logger.error 'Error while setting encoding' if @logging
196
+ stop
197
+ end
198
+ @client.set_pixel_format @pix_fmt
199
+ unresponded_requests = 0
200
+ incremental = 0
201
+ framerate_update_counter = 1
202
+ begin
203
+ loop do
204
+ if IO.select([@client.io], nil, nil, 0.05).nil?
205
+ @client.fb_update_request(incremental, 0, 0, @w, @h) if unresponded_requests < 1
206
+ unresponded_requests += 1
207
+ if unresponded_requests > 250
208
+ @logger.warn '250 unresponded requests' if @logging
209
+ if unresponded_requests > 500
210
+ @logger.error '500 unresponded requests' if @logging
211
+ stop
212
+ end
213
+ @client.fb_update_request(0, 0, 0, @w, @h)
214
+ sleep(0.25 + rand)
215
+ end
216
+ else
217
+ unresponded_requests = 0
218
+ if framerate_update_counter % 25 != 0
219
+ t, data = @client.handle_response
220
+ @logger.info "Got response: type #{t}" if @logger
221
+ else
222
+ adjust_sleep_time { t, data = @client.handle_response }
223
+ framerate_update_counter = 1
224
+ incremental = 0
225
+ end
226
+ case t
227
+ when 0 then
228
+ if data.nil?
229
+ @logger.error 'Failed to read frame' if @logging
230
+ stop
231
+ else
232
+ framerate_update_counter += 1
233
+ incremental = 255 if incremental.zero? && framerate_update_counter > 1
234
+ end
235
+ @file.write data
236
+ sleep @sleep_time
237
+ when 1 then
238
+ @logger.info 'Got colormap' if @logging
239
+ when 2 then next # bell
240
+ when 3 then
241
+ @logger.info "Server cut text: #{data}" if @logging
242
+ else
243
+ @logger.error "Unknown response format: #{t}" if @logging
244
+ stop
245
+ end
246
+ end
247
+ end
248
+ rescue EOFError, IOError
249
+ @logger.error 'Connection lost' if @logging
250
+ stop
251
+ end
252
+ end
253
+
254
+ def adjust_sleep_time(&block)
255
+ # figures out how much does one frame rendering takes
256
+ # and sets instance variable according to this value
257
+ t1 = Time.now.to_f
258
+ block.call
259
+ t2 = Time.now.to_f
260
+ frt = t2 - t1
261
+ @framerate ||= 8
262
+ if frt > 1.0 / @framerate
263
+ @logger.warn 'It takes too much time:' if @logging
264
+ @logger.warn "#{frt} seconds" if @logging
265
+ @logger.warn 'to render one frame' if @logging
266
+ @logger.warn 'Setting idle time to 0' if @logging
267
+ @sleep_time = 0
268
+ return
269
+ end
270
+ @sleep_time = (1.0 / @framerate) - frt - 10e-2
271
+ @sleep_time = 0 if @sleep_time < 0
272
+ @logger.info "Renderding of one frame takes about #{ frt } seconds" if @logging
273
+ @logger.info "Requested framerate: #{@framerate}, sleep time is #{@sleep_time}" if @logging
274
+ end
275
+
276
+ def valid_geometry?(str)
277
+ str.is_a?(String) && str[/(\d+)x(\d+)/]
278
+ end
279
+
280
+ def parse_geometry
281
+ @geometry[/(\d+)x(\d+)/]
282
+ @w = $1.to_i
283
+ @h = $2.to_i
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,115 @@
1
+ begin
2
+ require 'vncrec/rfb/enchex_c'
3
+ rescue
4
+
5
+ module VNCRec
6
+ module RFB
7
+
8
+ module EncHextile
9
+ def EncHextile.read_rect(io,w,h,bitspp)
10
+ bpp = (bitspp.to_f/8.0).to_i
11
+ framebuffer = Array.new(w*h*bpp,0)
12
+ tiles_row_num = (h.to_f/16.0).ceil
13
+ tiles_col_num = (w.to_f/16.0).ceil
14
+ last_tile_w = w % 16
15
+ last_tile_h = h % 16
16
+
17
+ prev_tile_bg = nil
18
+ prev_tile_fg = nil
19
+
20
+ tiles_row_num.times do |i|
21
+
22
+ th = if ((i == tiles_row_num-1) and (last_tile_h > 0))
23
+ last_tile_h
24
+ else
25
+ 16
26
+ end
27
+ ty = 16 * i
28
+
29
+ tiles_col_num.times do |j|
30
+
31
+ tw = if ((j == tiles_col_num-1) and (last_tile_w > 0))
32
+ last_tile_w
33
+ else
34
+ 16
35
+ end
36
+
37
+ tx = 16 * j
38
+
39
+ subenc = io.readbyte
40
+
41
+ raw = subenc & 1 > 0
42
+ bg_spec = subenc & 2 > 0
43
+ fg_spec = subenc & 4 > 0
44
+ any_subr = subenc & 8 > 0
45
+ subr_col = subenc & 16 > 0
46
+
47
+ if raw
48
+ data = io.read(tw*th*bpp).unpack("C*")
49
+ th.times do |ti|
50
+ init = (ty + ti) * w + tx
51
+ init *= bpp
52
+ towrite = data[ti*tw*bpp ... (ti+1)*tw*bpp ]
53
+ framebuffer[ init ... init + tw * bpp] = towrite
54
+ end
55
+ next
56
+ end
57
+
58
+ if bg_spec
59
+
60
+ prev_tile_bg = io.readpartial(bpp).unpack("C"*bpp)
61
+ end
62
+
63
+ th.times do |ti|
64
+ init = (ty + ti)*w + tx
65
+ init *= bpp
66
+ framebuffer[ init ... init + tw * bpp] = prev_tile_bg * tw
67
+ end
68
+
69
+ if fg_spec
70
+ prev_tile_fg = io.readpartial(bpp).unpack("C"*bpp)
71
+ end
72
+
73
+ if any_subr
74
+ subrects_number = io.readpartial(1).unpack("C")[0]
75
+ if subr_col
76
+ subrects_number.times do
77
+ fg = io.readpartial(bpp).unpack("C"*bpp)
78
+ read_subrect_c w, h ,tx, ty, framebuffer, io, fg
79
+ end
80
+ else
81
+ subrects_number.times do
82
+ read_subrect_c w, h ,tx, ty, framebuffer, io, prev_tile_fg
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+
89
+ end #tiles_row_num.times
90
+ end #tiles_col_num.times
91
+
92
+
93
+ return framebuffer.pack("C*")
94
+ end
95
+
96
+ def EncHextile.read_subrect(rw, rh, tx, ty, framebuffer, io, fg)
97
+ bpp = fg.size
98
+ xy, wh = io.read(2).unpack("CC")
99
+ x = (xy & 0xF0) >> 4
100
+ y = xy & 0x0F
101
+ w = ((wh & 0xF0) >> 4) + 1
102
+ h = (wh & 0x0F) + 1
103
+ h.times do |sbry|
104
+ init = (ty+sbry+y)*rw + tx + x
105
+ init *= bpp
106
+ framebuffer[init ... init + w * bpp] = fg*w
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,33 @@
1
+ module VNCRec
2
+ module RFB
3
+ module EncRaw
4
+ def self.read_rect(io, x, y, w, h, bitspp, fb, fbw, fbh)
5
+ bytespp = (bitspp.to_f / 8.0).to_i
6
+ rectsize = w * h * bytespp
7
+ data = io.read(rectsize)
8
+
9
+ if (x + w) * bytespp > fbw
10
+ if x * bytespp > fbw
11
+ return
12
+ else
13
+ w = fbw / bytespp - x
14
+ end
15
+ end
16
+ if (y + h) > fbh
17
+ if y > fbh
18
+ return
19
+ else
20
+ h = fbh - y
21
+ end
22
+ end
23
+
24
+ row = 0
25
+ while row < h
26
+ topleft = fbw * (y + row) + x * bytespp
27
+ fb[topleft ... topleft + w * bytespp] = data[row * w * bytespp ... (row + 1) * w * bytespp]
28
+ row += 1
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end