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