vncrec 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +36 -0
- data/Rakefile +8 -0
- data/bin/vncrec +80 -0
- data/examples/exit.rb +8 -0
- data/examples/mp4.rb +10 -0
- data/examples/mp4audio.rb +11 -0
- data/ext/enchex_c/ReadRect.c +215 -0
- data/ext/enchex_c/extconf.rb +5 -0
- data/lib/vncrec.rb +6 -0
- data/lib/vncrec/constants.rb +10 -0
- data/lib/vncrec/recorder.rb +286 -0
- data/lib/vncrec/rfb/enchex.rb +115 -0
- data/lib/vncrec/rfb/encraw.rb +33 -0
- data/lib/vncrec/rfb/enczrle.rb +202 -0
- data/lib/vncrec/rfb/proxy.rb +200 -0
- data/lib/vncrec/version.rb +3 -0
- data/lib/vncrec/writers.rb +209 -0
- data/spec/executable_spec.rb +40 -0
- data/spec/recorder_spec.rb +338 -0
- data/spec/spec_helper.rb +171 -0
- data/spec/writers_spec.rb +121 -0
- data/vncrec.gemspec +34 -0
- metadata +165 -0
data/lib/vncrec.rb
ADDED
@@ -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
|