vncrec 1.0.1

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