screencaster-gtk 0.0.3.alpha1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/screencaster +7 -0
- data/lib/screencaster-gtk.rb +462 -0
- data/lib/screencaster-gtk/capture.rb +292 -0
- data/lib/screencaster-gtk/progresstracker.rb +45 -0
- data/lib/screencaster-gtk/savefile.rb +75 -0
- metadata +143 -0
data/bin/screencaster
ADDED
@@ -0,0 +1,462 @@
|
|
1
|
+
require 'gtk2'
|
2
|
+
require 'logger'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'getoptlong'
|
5
|
+
require "screencaster-gtk/capture"
|
6
|
+
require "screencaster-gtk/savefile"
|
7
|
+
|
8
|
+
##########################
|
9
|
+
|
10
|
+
|
11
|
+
class ScreencasterGtk
|
12
|
+
attr_reader :capture_window, :window
|
13
|
+
attr_writer :status_icon
|
14
|
+
|
15
|
+
SCREENCASTER_DIR = File.join(Dir.home, ".screencaster")
|
16
|
+
# Set up logging. Keep 5 log files of a 100K each
|
17
|
+
log_dir = File.join(SCREENCASTER_DIR, 'log')
|
18
|
+
FileUtils.mkpath log_dir
|
19
|
+
LOGGER = Logger.new(File.join(log_dir, 'screencaster.log'), 5, 100000)
|
20
|
+
LOGGER.level = Logger::DEBUG
|
21
|
+
|
22
|
+
PIDFILE = File.join(SCREENCASTER_DIR, "run", "screencaster.pid")
|
23
|
+
|
24
|
+
DEFAULT_SPACE = 10
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
#### Create Main Window
|
28
|
+
|
29
|
+
LOGGER.info "Started"
|
30
|
+
|
31
|
+
@window = Gtk::Window.new
|
32
|
+
@window.signal_connect("delete_event") {
|
33
|
+
LOGGER.debug "delete event occurred"
|
34
|
+
#true
|
35
|
+
self.quit
|
36
|
+
false
|
37
|
+
}
|
38
|
+
|
39
|
+
@window.signal_connect("destroy") {
|
40
|
+
LOGGER.debug "destroy event occurred"
|
41
|
+
}
|
42
|
+
|
43
|
+
# The following gets minimize and restore events, but not iconify and de-iconify
|
44
|
+
@window.signal_connect("window_state_event") { |w, e|
|
45
|
+
puts ("window_state_event #{e.to_s}")
|
46
|
+
}
|
47
|
+
|
48
|
+
@window.border_width = DEFAULT_SPACE
|
49
|
+
|
50
|
+
bottom_columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
51
|
+
|
52
|
+
@select_button = Gtk::Button.new("Select Window to Record")
|
53
|
+
@select_button.signal_connect("clicked") {
|
54
|
+
self.select
|
55
|
+
}
|
56
|
+
bottom_columns.pack_start(@select_button, true, false)
|
57
|
+
|
58
|
+
button = Gtk::Button.new("Quit")
|
59
|
+
button.signal_connect("clicked") {
|
60
|
+
self.quit
|
61
|
+
}
|
62
|
+
bottom_columns.pack_end(button, true, false)
|
63
|
+
|
64
|
+
bottom_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
65
|
+
bottom_row.pack_end(bottom_columns, false)
|
66
|
+
|
67
|
+
control_columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
68
|
+
|
69
|
+
@record_button = Gtk::Button.new("Record")
|
70
|
+
@record_button.sensitive = false
|
71
|
+
@record_button.signal_connect("clicked") {
|
72
|
+
self.record
|
73
|
+
}
|
74
|
+
control_columns.pack_start(@record_button, true, false)
|
75
|
+
|
76
|
+
@pause_button = Gtk::Button.new("Pause")
|
77
|
+
@pause_button.sensitive = false
|
78
|
+
@pause_button.signal_connect("clicked") {
|
79
|
+
self.pause
|
80
|
+
}
|
81
|
+
control_columns.pack_start(@pause_button, true, false)
|
82
|
+
|
83
|
+
@stop_button = Gtk::Button.new("Stop")
|
84
|
+
@stop_button.sensitive = false
|
85
|
+
@stop_button.signal_connect("clicked") {
|
86
|
+
self.stop_recording
|
87
|
+
}
|
88
|
+
control_columns.pack_start(@stop_button, true, false)
|
89
|
+
|
90
|
+
control_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
91
|
+
control_row.pack_start(control_columns, true, false)
|
92
|
+
|
93
|
+
columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
|
94
|
+
@progress_bar = Gtk::ProgressBar.new
|
95
|
+
columns.pack_start(@progress_bar, true, false)
|
96
|
+
|
97
|
+
@cancel_button = Gtk::Button.new("Cancel")
|
98
|
+
@cancel_button.sensitive = false
|
99
|
+
@cancel_button.signal_connect("clicked") {
|
100
|
+
self.stop_encoding
|
101
|
+
}
|
102
|
+
columns.pack_start(@cancel_button, true, false)
|
103
|
+
|
104
|
+
progress_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
|
105
|
+
progress_row.pack_start(columns, true, false)
|
106
|
+
|
107
|
+
the_box = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
|
108
|
+
the_box.pack_end(bottom_row, false, false)
|
109
|
+
the_box.pack_end(progress_row, false, false)
|
110
|
+
the_box.pack_end(control_row, false, false)
|
111
|
+
|
112
|
+
@window.add(the_box)
|
113
|
+
|
114
|
+
##### Done Creating Main Window
|
115
|
+
|
116
|
+
#### Accelerator Group
|
117
|
+
group = Gtk::AccelGroup.new
|
118
|
+
group.connect(Gdk::Keyval::GDK_N, Gdk::Window::CONTROL_MASK|Gdk::Window::MOD1_MASK, Gtk::ACCEL_VISIBLE) do
|
119
|
+
#puts "You pressed 'Ctrl+Alt+n'"
|
120
|
+
end
|
121
|
+
|
122
|
+
#### Pop up menu on right click
|
123
|
+
|
124
|
+
@select = Gtk::ImageMenuItem.new("Select Window")
|
125
|
+
@select.signal_connect('activate'){self.select}
|
126
|
+
|
127
|
+
@record = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_RECORD)
|
128
|
+
@record.signal_connect('activate'){self.record}
|
129
|
+
@record.add_accelerator('activate',
|
130
|
+
group, Gdk::Keyval::GDK_R,
|
131
|
+
Gdk::Window::CONTROL_MASK | Gdk::Window::MOD1_MASK,
|
132
|
+
Gtk::ACCEL_VISIBLE)
|
133
|
+
@pause = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_PAUSE)
|
134
|
+
@pause.signal_connect('activate'){self.pause}
|
135
|
+
@pause.add_accelerator('activate',
|
136
|
+
group, Gdk::Keyval::GDK_P,
|
137
|
+
Gdk::Window::CONTROL_MASK | Gdk::Window::MOD1_MASK,
|
138
|
+
Gtk::ACCEL_VISIBLE)
|
139
|
+
@stop = Gtk::ImageMenuItem.new(Gtk::Stock::MEDIA_STOP)
|
140
|
+
@stop.signal_connect('activate'){self.stop}
|
141
|
+
@stop.add_accelerator('activate',
|
142
|
+
group, Gdk::Keyval::GDK_S,
|
143
|
+
Gdk::Window::CONTROL_MASK | Gdk::Window::MOD1_MASK,
|
144
|
+
Gtk::ACCEL_VISIBLE)
|
145
|
+
@show_hide = Gtk::MenuItem.new("Hide")
|
146
|
+
@show_hide.signal_connect('activate'){self.show_hide_all}
|
147
|
+
|
148
|
+
quit = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
|
149
|
+
quit.signal_connect('activate'){self.quit}
|
150
|
+
|
151
|
+
@menu = Gtk::Menu.new
|
152
|
+
@menu.append(@select)
|
153
|
+
|
154
|
+
@menu.append(Gtk::SeparatorMenuItem.new)
|
155
|
+
@menu.append(@record)
|
156
|
+
@menu.append(@pause)
|
157
|
+
@menu.append(@stop)
|
158
|
+
|
159
|
+
@menu.append(Gtk::SeparatorMenuItem.new)
|
160
|
+
@menu.append(@show_hide)
|
161
|
+
|
162
|
+
@menu.append(Gtk::SeparatorMenuItem.new)
|
163
|
+
@menu.append(quit)
|
164
|
+
|
165
|
+
@menu.show_all
|
166
|
+
|
167
|
+
#### Done Menus
|
168
|
+
|
169
|
+
#### Attach accelerators to window
|
170
|
+
#root = Gdk::Window.default_root_window
|
171
|
+
@window.add_accel_group(group)
|
172
|
+
end
|
173
|
+
|
174
|
+
#### Status Icon
|
175
|
+
def status_icon
|
176
|
+
return @status_icon unless @status_icon.nil?
|
177
|
+
|
178
|
+
@status_icon = Gtk::StatusIcon.new
|
179
|
+
# Some space appears in the tray immediately, so hide it.
|
180
|
+
# The icon doesn't actually appear until you start Gtk.main
|
181
|
+
@status_icon.visible = false
|
182
|
+
@status_icon.stock = Gtk::Stock::MEDIA_RECORD
|
183
|
+
@status_icon.tooltip = 'Screencaster'
|
184
|
+
|
185
|
+
##Show menu on right click
|
186
|
+
@status_icon.signal_connect('popup-menu'){|tray, button, time| @menu.popup(nil, nil, button, time)}
|
187
|
+
|
188
|
+
@status_icon
|
189
|
+
end
|
190
|
+
|
191
|
+
#### Done Status Icon
|
192
|
+
|
193
|
+
def quit
|
194
|
+
LOGGER.debug "Quitting"
|
195
|
+
# self.status_icon.destroy
|
196
|
+
# LOGGER.debug "After status icon destroy."
|
197
|
+
# @window.destroy
|
198
|
+
# LOGGER.debug "After window destroy."
|
199
|
+
# We don't want to destroy here because the object continues to exist
|
200
|
+
# Just hide everything
|
201
|
+
self.hide_all_including_status
|
202
|
+
# self.status_icon.hide doesn't work/exist
|
203
|
+
Gtk.main_quit
|
204
|
+
LOGGER.debug "After main_quit."
|
205
|
+
end
|
206
|
+
|
207
|
+
def select
|
208
|
+
LOGGER.debug "Selecting Window"
|
209
|
+
@capture_window = Capture.new
|
210
|
+
@capture_window.get_window_to_capture
|
211
|
+
@record_button.sensitive = true
|
212
|
+
end
|
213
|
+
|
214
|
+
def record
|
215
|
+
LOGGER.debug "Recording"
|
216
|
+
recording
|
217
|
+
@capture_window.record
|
218
|
+
end
|
219
|
+
|
220
|
+
def pause
|
221
|
+
LOGGER.debug "Pausing"
|
222
|
+
paused
|
223
|
+
@capture_window.pause_recording
|
224
|
+
end
|
225
|
+
|
226
|
+
def stop_recording
|
227
|
+
LOGGER.debug "Stopped"
|
228
|
+
not_recording
|
229
|
+
@capture_window.stop_recording
|
230
|
+
|
231
|
+
SaveFile.get_file_to_save { |filename|
|
232
|
+
encoding
|
233
|
+
@capture_window.encode(filename) { |percent, time_remaining|
|
234
|
+
@progress_bar.fraction = percent
|
235
|
+
@progress_bar.text = time_remaining
|
236
|
+
LOGGER.debug "Did progress #{percent.to_s} time remaining: #{time_remaining}"
|
237
|
+
percent < 1 || stop_encoding
|
238
|
+
}
|
239
|
+
LOGGER.debug "Back from encode"
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
def stop_encoding
|
244
|
+
LOGGER.debug "Cancelled encoding"
|
245
|
+
not_recording
|
246
|
+
@capture_window.stop_encoding
|
247
|
+
end
|
248
|
+
|
249
|
+
def stop
|
250
|
+
case @capture_window.state
|
251
|
+
when :recording || :paused
|
252
|
+
stop_recording
|
253
|
+
when :encoding
|
254
|
+
stop_encoding
|
255
|
+
when :stopped
|
256
|
+
# Do nothing
|
257
|
+
else
|
258
|
+
LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen."
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def toggle_recording
|
263
|
+
LOGGER.debug "Toggle recording"
|
264
|
+
return if @capture_window.nil?
|
265
|
+
case @capture_window.state
|
266
|
+
when :recording
|
267
|
+
pause
|
268
|
+
when :paused
|
269
|
+
record
|
270
|
+
when :encoding
|
271
|
+
stop_encoding
|
272
|
+
when :stopped
|
273
|
+
# Do nothing
|
274
|
+
else
|
275
|
+
LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen."
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def recording
|
280
|
+
self.status_icon.stock = Gtk::Stock::MEDIA_STOP
|
281
|
+
@select.sensitive = @select_button.sensitive = false
|
282
|
+
@pause.sensitive = @pause_button.sensitive = true
|
283
|
+
@stop.sensitive = @stop_button.sensitive = true
|
284
|
+
@record.sensitive = @record_button.sensitive = false
|
285
|
+
@cancel_button.sensitive = false
|
286
|
+
end
|
287
|
+
|
288
|
+
def not_recording
|
289
|
+
self.status_icon.stock = Gtk::Stock::MEDIA_RECORD
|
290
|
+
@select.sensitive = @select_button.sensitive = true
|
291
|
+
@pause.sensitive = @pause_button.sensitive = false
|
292
|
+
@stop.sensitive = @stop_button.sensitive = false
|
293
|
+
@record.sensitive = @record_button.sensitive = ! @capture_window.nil?
|
294
|
+
@cancel_button.sensitive = false
|
295
|
+
end
|
296
|
+
|
297
|
+
def paused
|
298
|
+
self.status_icon.stock = Gtk::Stock::MEDIA_RECORD
|
299
|
+
@select.sensitive = @select_button.sensitive = false
|
300
|
+
@pause.sensitive = @pause_button.sensitive = false
|
301
|
+
@stop.sensitive = @stop_button.sensitive = true
|
302
|
+
@record.sensitive = @record_button.sensitive = ! @capture_window.nil?
|
303
|
+
@cancel_button.sensitive = true
|
304
|
+
end
|
305
|
+
|
306
|
+
def encoding
|
307
|
+
self.status_icon.stock = Gtk::Stock::MEDIA_STOP
|
308
|
+
@pause.sensitive = @pause_button.sensitive = false
|
309
|
+
@stop.sensitive = @stop_button.sensitive = false
|
310
|
+
@record.sensitive = @record_button.sensitive = false
|
311
|
+
@cancel_button.sensitive = true
|
312
|
+
end
|
313
|
+
|
314
|
+
def not_encoding
|
315
|
+
not_recording
|
316
|
+
end
|
317
|
+
|
318
|
+
def show_all
|
319
|
+
@show_hide.label = "Hide"
|
320
|
+
@show_hide_state = :show
|
321
|
+
@window.show_all
|
322
|
+
end
|
323
|
+
|
324
|
+
def hide_all
|
325
|
+
@show_hide.label = "Show"
|
326
|
+
@show_hide_state = :hide
|
327
|
+
@window.hide_all
|
328
|
+
end
|
329
|
+
|
330
|
+
def show_hide_all
|
331
|
+
case @show_hide_state
|
332
|
+
when :show
|
333
|
+
self.hide_all
|
334
|
+
when :hide
|
335
|
+
self.show_all
|
336
|
+
else
|
337
|
+
logger.error("#{__FILE__} (#{__LINE__} @show_hide_state: #{@show_hide_state.to_s}")
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def show_all_including_status
|
342
|
+
self.show_all
|
343
|
+
self.status_icon.visible = true
|
344
|
+
end
|
345
|
+
|
346
|
+
def hide_all_including_status
|
347
|
+
self.hide_all
|
348
|
+
self.status_icon.visible = false
|
349
|
+
end
|
350
|
+
|
351
|
+
def main
|
352
|
+
LOGGER.info "Starting"
|
353
|
+
self.not_recording
|
354
|
+
self.show_all_including_status
|
355
|
+
Gtk.main
|
356
|
+
LOGGER.info "Finished"
|
357
|
+
end
|
358
|
+
|
359
|
+
def set_up
|
360
|
+
# Don't run the program if there's another instance running for the user.
|
361
|
+
# If there's another instance running for the user and the --pause or --start
|
362
|
+
# flags are present, send the USR1 signal to the running instance
|
363
|
+
# and exit.
|
364
|
+
# If there's no other instance running for the user, and the --pause or --start
|
365
|
+
# flags are not present, start normally.
|
366
|
+
|
367
|
+
output_file = "/home/reid/test-key.log"
|
368
|
+
|
369
|
+
ScreencasterGtk::LOGGER.debug("pid_file is #{PIDFILE}")
|
370
|
+
|
371
|
+
if File.exists? PIDFILE
|
372
|
+
begin
|
373
|
+
f = File.new(PIDFILE)
|
374
|
+
ScreencasterGtk::LOGGER.debug("Opened PIDFILE")
|
375
|
+
existing_pid = f.gets
|
376
|
+
existing_pid = existing_pid.to_i
|
377
|
+
f.close
|
378
|
+
ScreencasterGtk::LOGGER.debug("existing_pid = #{existing_pid.to_s}")
|
379
|
+
rescue StandardError
|
380
|
+
LOGGER.error("File to read #{PIDFILE}")
|
381
|
+
exit 1
|
382
|
+
end
|
383
|
+
else
|
384
|
+
existing_pid = nil
|
385
|
+
end
|
386
|
+
|
387
|
+
opts = GetoptLong.new(
|
388
|
+
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
|
389
|
+
[ '--pause', GetoptLong::NO_ARGUMENT ],
|
390
|
+
[ '--start', GetoptLong::NO_ARGUMENT ]
|
391
|
+
)
|
392
|
+
|
393
|
+
opts.each do |opt, arg|
|
394
|
+
case opt
|
395
|
+
when '--help'
|
396
|
+
puts <<-EOF
|
397
|
+
screencaster [OPTION] ...
|
398
|
+
|
399
|
+
-h, --help:
|
400
|
+
show help
|
401
|
+
|
402
|
+
-s, -p, --start, --pause:
|
403
|
+
Pause a running capture, or restart a paused capture
|
404
|
+
EOF
|
405
|
+
exit 0
|
406
|
+
when '--pause' || '--start'
|
407
|
+
if existing_pid then
|
408
|
+
ScreencasterGtk::LOGGER.debug("Got a pause for PID #{existing_pid}")
|
409
|
+
begin
|
410
|
+
Process.kill "USR1", existing_pid
|
411
|
+
exit 0
|
412
|
+
rescue SystemCallError
|
413
|
+
LOGGER.info("Got a pause but PID #{existing_pid} didn't exist")
|
414
|
+
exit 1
|
415
|
+
end
|
416
|
+
else
|
417
|
+
ScreencasterGtk::LOGGER.info("Got a pause but no PID")
|
418
|
+
exit 1
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
# TODO: Check for running process and if not, ignore PIDFILE.
|
424
|
+
(ScreencasterGtk::LOGGER.debug("Can't run two instances at once."); exit 1) if ! existing_pid.nil?
|
425
|
+
|
426
|
+
@chain = Signal.trap("EXIT") {
|
427
|
+
LOGGER.debug "In ScreencasterGtk exit handler @chain: #{@chain}"
|
428
|
+
ScreencasterGtk::LOGGER.debug("Exiting")
|
429
|
+
ScreencasterGtk::LOGGER.debug("unlinking") if File.file?(PIDFILE)
|
430
|
+
File.unlink(PIDFILE) if File.file?(PIDFILE)
|
431
|
+
`gconftool-2 --unset /apps/metacity/keybinding_commands/screencaster_pause`
|
432
|
+
`gconftool-2 --unset /apps/metacity/global_keybindings/run_screencaster_pause`
|
433
|
+
LOGGER.debug "About to call next trap with @chain: #{@chain}"
|
434
|
+
@chain.call unless @chain.nil?
|
435
|
+
}
|
436
|
+
LOGGER.debug "ScreencasterGtk.whatever @chain: #{@chain}"
|
437
|
+
|
438
|
+
`gconftool-2 --set /apps/metacity/keybinding_commands/screencaster_pause --type string "screencaster --pause"`
|
439
|
+
`gconftool-2 --set /apps/metacity/global_keybindings/run_screencaster_pause --type string "<Control><Alt>S"`
|
440
|
+
|
441
|
+
begin
|
442
|
+
FileUtils.mkpath(File.dirname(PIDFILE))
|
443
|
+
f = File.new(PIDFILE, "w")
|
444
|
+
f.puts(Process.pid.to_s)
|
445
|
+
f.close
|
446
|
+
LOGGER.debug("Wrote PID #{Process.pid}")
|
447
|
+
rescue StandardError
|
448
|
+
LOGGER.error("Failed to write #{PIDFILE}")
|
449
|
+
exit 1
|
450
|
+
end
|
451
|
+
|
452
|
+
Signal.trap("USR1") {
|
453
|
+
ScreencasterGtk::LOGGER.debug("Pause/Resume")
|
454
|
+
self.toggle_recording
|
455
|
+
}
|
456
|
+
|
457
|
+
$logger = ScreencasterGtk::LOGGER # TODO: Fix logging
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
#puts "Loaded #{__FILE__}"
|
462
|
+
|
@@ -0,0 +1,292 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'logger'
|
3
|
+
require "screencaster-gtk/progresstracker"
|
4
|
+
|
5
|
+
class Capture
|
6
|
+
include ProgressTracker
|
7
|
+
|
8
|
+
attr_writer :left, :top, :right, :bottom
|
9
|
+
attr_reader :state
|
10
|
+
attr :pid
|
11
|
+
attr :tmp_files
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@tmp_files = []
|
15
|
+
@exit_chain = Signal.trap("EXIT") {
|
16
|
+
self.cleanup
|
17
|
+
$logger.debug "In capture about to chain to trap @exit_chain: #{@exit_chain}"
|
18
|
+
@exit_chain.call unless @exit_chain.nil?
|
19
|
+
}
|
20
|
+
$logger.debug "@exit_chain: #{@exit_chain.to_s}"
|
21
|
+
@state = :stopped
|
22
|
+
end
|
23
|
+
|
24
|
+
def current_tmp_file
|
25
|
+
@tmp_files.last
|
26
|
+
end
|
27
|
+
|
28
|
+
def new_tmp_file
|
29
|
+
@tmp_files << Capture.tmp_file_name(@tmp_files.size)
|
30
|
+
self.current_tmp_file
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.tmp_file_name(index = nil)
|
34
|
+
"/tmp/screencaster_#{$$}" + (index.nil? ? "": "_#{"%04d" % index}") + ".mkv"
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.format_input_files_for_mkvmerge(files)
|
38
|
+
files.drop(1).inject("\"#{files.first}\"") {|a, b| "#{a} + \"#{b}\"" }
|
39
|
+
end
|
40
|
+
|
41
|
+
def width
|
42
|
+
@right - @left
|
43
|
+
end
|
44
|
+
|
45
|
+
def height=(h)
|
46
|
+
@bottom = @top + h
|
47
|
+
h
|
48
|
+
end
|
49
|
+
|
50
|
+
def height
|
51
|
+
@bottom - @top
|
52
|
+
end
|
53
|
+
|
54
|
+
def width=(w)
|
55
|
+
@right = @left + w
|
56
|
+
w
|
57
|
+
end
|
58
|
+
|
59
|
+
# I have to refactor this to make it more testable.
|
60
|
+
# As a general approach, I want to factor out the parts that have human input
|
61
|
+
# so that whatever I have to do for that is as small as possible.
|
62
|
+
|
63
|
+
def get_window_to_capture
|
64
|
+
print "Click in the window you want to capture.\n"
|
65
|
+
info = `xwininfo`
|
66
|
+
|
67
|
+
info =~ /Window id: (0x[[:xdigit:]]+)/
|
68
|
+
window_id = $1
|
69
|
+
|
70
|
+
info =~ /geometry\s+([[:digit:]])+x([[:digit:]]+)\+([[:digit:]]+)-([[:digit:]]+)/
|
71
|
+
|
72
|
+
info =~ /Absolute upper-left X:\s+([[:digit:]]+)/
|
73
|
+
@left = $1.to_i
|
74
|
+
info =~ /Absolute upper-left Y:\s+([[:digit:]]+)/
|
75
|
+
@top = $1.to_i
|
76
|
+
|
77
|
+
info =~ /Width:\s+([[:digit:]]+)/
|
78
|
+
@width = $1.to_i
|
79
|
+
info =~ /Height:\s+([[:digit:]]+)/
|
80
|
+
@height = $1.to_i
|
81
|
+
|
82
|
+
$logger.debug "Before xprop: Capturing #{@left.to_s},#{@top.to_s} to #{(@left+@width).to_s},#{(@top+@height).to_s}. Dimensions #{@width.to_s},#{@height.to_s}.\n"
|
83
|
+
|
84
|
+
# Use xprop on the window to figure out decorations? Maybe...
|
85
|
+
# $logger.debug "Window ID: #{window_id}"
|
86
|
+
# info = `xprop -id #{window_id}`
|
87
|
+
# info =~ /_NET_FRAME_EXTENTS\(CARDINAL\) = ([[:digit:]]+), ([[:digit:]]+), ([[:digit:]]+), ([[:digit:]]+)/
|
88
|
+
# border_left = $1.to_i
|
89
|
+
# border_right = $2.to_i
|
90
|
+
# border_top = $3.to_i
|
91
|
+
# border_bottom = $4.to_i
|
92
|
+
#
|
93
|
+
# $logger.debug "Borders: #{border_left.to_s},#{border_top.to_s},#{border_right.to_s},#{border_bottom.to_s}.\n"
|
94
|
+
#
|
95
|
+
# top += border_top
|
96
|
+
# left += border_left
|
97
|
+
# height -= border_top + border_bottom
|
98
|
+
# width -= border_left + border_right
|
99
|
+
|
100
|
+
@height += @height % 2
|
101
|
+
@width += @width % 2
|
102
|
+
|
103
|
+
$logger.debug "Capturing #{@left},#{@top} to #{@left+@width},#{@top+@height}. Dimensions #{@width},#{@height}.\n"
|
104
|
+
end
|
105
|
+
|
106
|
+
def record
|
107
|
+
if @state != :paused
|
108
|
+
@tmp_files = []
|
109
|
+
self.total_amount = 0.0
|
110
|
+
end
|
111
|
+
record_one_file(self.new_tmp_file)
|
112
|
+
end
|
113
|
+
|
114
|
+
def record_one_file(output_file)
|
115
|
+
@state = :recording
|
116
|
+
capture_fps=24
|
117
|
+
audio_options="-f alsa -ac 1 -ab 192k -i pulse -acodec pcm_s16le"
|
118
|
+
|
119
|
+
# And i should probably popen here, save the pid, then fork and start
|
120
|
+
# reading the input, updating the number of frames saved, or the time
|
121
|
+
# recorded.
|
122
|
+
$logger.debug "Capturing...\n"
|
123
|
+
# @pid = Process.spawn("avconv \
|
124
|
+
# #{audio_options} \
|
125
|
+
# -f x11grab \
|
126
|
+
# -show_region 1 \
|
127
|
+
# -r #{capture_fps} \
|
128
|
+
# -s #{@width}x#{@height} \
|
129
|
+
# -i :0.0+#{@left},#{@top} \
|
130
|
+
# -qscale 0 -vcodec ffv1 \
|
131
|
+
# -y \
|
132
|
+
# #{TMP_FILE}")
|
133
|
+
# Process.detach(@pid)
|
134
|
+
# avconv writes output to stderr, why?
|
135
|
+
# writes with CR and not LF, so it's hard to read
|
136
|
+
# with popen and 2>&1, an extra shell gets created that messed things up.
|
137
|
+
# popen2e helps.
|
138
|
+
vcodec = 'huffyuv' # I used to use ffv1
|
139
|
+
|
140
|
+
cmd_line = "avconv \
|
141
|
+
#{audio_options} \
|
142
|
+
-f x11grab \
|
143
|
+
-show_region 1 \
|
144
|
+
-r #{capture_fps} \
|
145
|
+
-s #{@width}x#{@height} \
|
146
|
+
-i :0.0+#{@left},#{@top} \
|
147
|
+
-qscale 0 -vcodec #{vcodec} \
|
148
|
+
-y \
|
149
|
+
#{output_file}"
|
150
|
+
|
151
|
+
$logger.debug cmd_line
|
152
|
+
|
153
|
+
i, oe, t = Open3.popen2e(cmd_line)
|
154
|
+
@pid = t.pid
|
155
|
+
Process.detach(@pid)
|
156
|
+
|
157
|
+
duration = 0.0
|
158
|
+
Thread.new do
|
159
|
+
while line = oe.gets("\r")
|
160
|
+
$logger.debug "****" + line
|
161
|
+
if (line =~ /time=([0-9]*\.[0-9]*)/)
|
162
|
+
duration = $1.to_f
|
163
|
+
end
|
164
|
+
end
|
165
|
+
self.total_amount += duration
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def stop_recording
|
170
|
+
begin
|
171
|
+
Process.kill("INT", @pid)
|
172
|
+
rescue SystemCallError
|
173
|
+
$logger.error("No recording to stop.") unless @state == :paused
|
174
|
+
end
|
175
|
+
@state = :stopped
|
176
|
+
end
|
177
|
+
|
178
|
+
def pause_recording
|
179
|
+
begin
|
180
|
+
Process.kill("INT", @pid)
|
181
|
+
rescue SystemCallError
|
182
|
+
$logger.error("No recording to pause.")
|
183
|
+
end
|
184
|
+
@state = :paused
|
185
|
+
end
|
186
|
+
|
187
|
+
# Refactoring this to make it more testable and so it works:
|
188
|
+
# Encoding now has two steps: Merge the files (if more than one)
|
189
|
+
# and then encode
|
190
|
+
# Encode takes an optional block that updates a progress bar or other type
|
191
|
+
# of status
|
192
|
+
# I believe I have to split it out so that variables are in scope when I
|
193
|
+
# need them to be, but mainly I need to make this testable, and now is the time.
|
194
|
+
|
195
|
+
def encode(output_file = "output.mp4", &feedback)
|
196
|
+
state = :encoding
|
197
|
+
output_file =~ /.mp4$/ || output_file += ".mp4"
|
198
|
+
|
199
|
+
$logger.debug "Encoding #{Capture.format_input_files_for_mkvmerge(@tmp_files)}...\n"
|
200
|
+
$logger.debug("Total duration #{self.total_amount.to_s}")
|
201
|
+
|
202
|
+
t = self.merge(Capture.tmp_file_name, @tmp_files)
|
203
|
+
t.value
|
204
|
+
self.final_encode(output_file, Capture.tmp_file_name, feedback)
|
205
|
+
end
|
206
|
+
|
207
|
+
# This is ugly.
|
208
|
+
# When you open co-processes, they do get stuck together.
|
209
|
+
# It seems the if I don't read what's coming out of the co-process, it waits.
|
210
|
+
# But if I read it, then it goes right to the end until it returns.
|
211
|
+
|
212
|
+
def merge(output_file, input_files, feedback = proc {} )
|
213
|
+
$logger.debug("Merging #{input_files.size.to_s} files: #{Capture.format_input_files_for_mkvmerge(input_files)}")
|
214
|
+
|
215
|
+
if input_files.size == 1
|
216
|
+
cmd_line = "cp #{input_files[0]} #{output_file}"
|
217
|
+
#cmd_line = "sleep 5"
|
218
|
+
else
|
219
|
+
cmd_line = "mkvmerge -v -o #{output_file} #{Capture.format_input_files_for_mkvmerge(input_files)}"
|
220
|
+
end
|
221
|
+
$logger.debug "Merge command line: #{cmd_line}"
|
222
|
+
i, oe, t = Open3.popen2e(cmd_line)
|
223
|
+
@pid = t.pid
|
224
|
+
Process.detach(@pid)
|
225
|
+
$logger.debug "@pid: #{@pid.to_s}"
|
226
|
+
|
227
|
+
t = Thread.new do
|
228
|
+
# $logger.debug "Sleeping..."
|
229
|
+
# sleep 2
|
230
|
+
# $logger.debug "Awake!"
|
231
|
+
while oe.gets do
|
232
|
+
# $logger.debug "Line"
|
233
|
+
end
|
234
|
+
feedback.call 1.0, "Done"
|
235
|
+
end
|
236
|
+
return t
|
237
|
+
end
|
238
|
+
|
239
|
+
def final_encode(output_file, input_file, feedback = proc {} )
|
240
|
+
encode_fps=24
|
241
|
+
video_encoding_options="-vcodec libx264 -pre:v ultrafast"
|
242
|
+
|
243
|
+
# I think I want to popen here, save the pid, then fork and start
|
244
|
+
# updating progress based on what I read, which the main body
|
245
|
+
# returns and carries on.
|
246
|
+
|
247
|
+
# The following doesn't seem to be necessary
|
248
|
+
# -s #{@width}x#{@height} \
|
249
|
+
cmd_line = "avconv \
|
250
|
+
-i #{input_file} \
|
251
|
+
#{video_encoding_options} \
|
252
|
+
-r #{encode_fps} \
|
253
|
+
-threads 0 \
|
254
|
+
-y \
|
255
|
+
'#{output_file}'"
|
256
|
+
|
257
|
+
$logger.debug cmd_line
|
258
|
+
|
259
|
+
i, oe, t = Open3.popen2e(cmd_line)
|
260
|
+
@pid = t.pid
|
261
|
+
Process.detach(@pid)
|
262
|
+
|
263
|
+
Thread.new do
|
264
|
+
while (line = oe.gets("\r"))
|
265
|
+
$logger.debug "****" + line
|
266
|
+
if (line =~ /time=([0-9]*\.[0-9]*)/)
|
267
|
+
$logger.debug '******' + $1
|
268
|
+
self.current_amount = $1.to_f
|
269
|
+
end
|
270
|
+
$logger.debug "******** #{self.current_amount} #{self.fraction_complete}"
|
271
|
+
feedback.call self.fraction_complete, self.time_remaining_s
|
272
|
+
end
|
273
|
+
$logger.debug "reached end of file"
|
274
|
+
@state = :stopped
|
275
|
+
feedback.call self.fraction_complete = 1, self.time_remaining_s
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def stop_encoding
|
280
|
+
begin
|
281
|
+
Process.kill("INT", @pid)
|
282
|
+
rescue SystemCallError
|
283
|
+
$logger.error("No encoding to stop.")
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def cleanup
|
288
|
+
@tmp_files.each { |f| File.delete(f) if File.exists?(f) }
|
289
|
+
File.delete(Capture.tmp_file_name) if File.exists?(Capture.tmp_file_name)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ProgressTracker
|
2
|
+
attr_reader :start_time
|
3
|
+
attr_writer :start_time, :total_amount
|
4
|
+
|
5
|
+
def percent_complete
|
6
|
+
self.fraction_complete * 100
|
7
|
+
end
|
8
|
+
|
9
|
+
def fraction_complete
|
10
|
+
[ self.current_amount.to_f / self.total_amount.to_f, 1.0 ].min
|
11
|
+
end
|
12
|
+
|
13
|
+
def fraction_complete=(fraction)
|
14
|
+
@current_amount = fraction * self.total_amount
|
15
|
+
end
|
16
|
+
|
17
|
+
def current_amount
|
18
|
+
@current_amount || 0.0
|
19
|
+
end
|
20
|
+
|
21
|
+
def current_amount=(amt)
|
22
|
+
@start_time || @start_time = Time.new
|
23
|
+
|
24
|
+
# puts "Setting current_amount #{amt}"
|
25
|
+
@current_amount = amt
|
26
|
+
end
|
27
|
+
|
28
|
+
def total_amount
|
29
|
+
@total_amount || 1.0
|
30
|
+
end
|
31
|
+
|
32
|
+
def time_remaining
|
33
|
+
(Time.new - @start_time) * (1 - self.fraction_complete) / self.fraction_complete
|
34
|
+
end
|
35
|
+
|
36
|
+
def time_remaining_s(format = "%dh %02dm %02ds remaining")
|
37
|
+
t = self.time_remaining
|
38
|
+
h = (t / 3600).to_i
|
39
|
+
m = ((t - h * 3600) / 60).to_i
|
40
|
+
s = (t % 60).to_i
|
41
|
+
sprintf(format, h, m, s)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'gtk2'
|
2
|
+
|
3
|
+
class SaveFile
|
4
|
+
def self.set_up_dialog(file_name = "output.mp4")
|
5
|
+
@dialog = Gtk::FileChooserDialog.new(
|
6
|
+
"Save File As ...",
|
7
|
+
$window,
|
8
|
+
Gtk::FileChooser::ACTION_SAVE,
|
9
|
+
nil,
|
10
|
+
[ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ],
|
11
|
+
[ Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT ]
|
12
|
+
)
|
13
|
+
# dialog.signal_connect('response') do |w, r|
|
14
|
+
# odg = case r
|
15
|
+
# when Gtk::Dialog::RESPONSE_ACCEPT
|
16
|
+
# filename = dialog.filename
|
17
|
+
# "'ACCEPT' (#{r}) button pressed -- filename is {{ #{filename} }}"
|
18
|
+
# when Gtk::Dialog::RESPONSE_CANCEL; "'CANCEL' (#{r}) button pressed"
|
19
|
+
# else; "Undefined response ID; perhaps Close-x? (#{r})"
|
20
|
+
# end
|
21
|
+
# puts odg
|
22
|
+
# dialog.destroy
|
23
|
+
# end
|
24
|
+
@dialog.current_name = GLib.filename_to_utf8(file_name)
|
25
|
+
@dialog.current_folder = GLib.filename_to_utf8(Dir.getwd)
|
26
|
+
@dialog.do_overwrite_confirmation = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.get_file_to_save
|
30
|
+
@dialog || SaveFile.set_up_dialog
|
31
|
+
result = @dialog.run
|
32
|
+
file_name = @dialog.filename
|
33
|
+
case result
|
34
|
+
when Gtk::Dialog::RESPONSE_ACCEPT
|
35
|
+
@dialog.hide
|
36
|
+
yield file_name
|
37
|
+
when Gtk::Dialog::RESPONSE_CANCEL
|
38
|
+
self.confirm_cancel(@dialog)
|
39
|
+
@dialog.hide
|
40
|
+
else
|
41
|
+
LOGGER.error("Can't happen #{__FILE__} line: #{__LINE__}")
|
42
|
+
@dialog.hide
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.confirm_cancel(parent)
|
47
|
+
dialog = Gtk::Dialog.new(
|
48
|
+
"Confirm Cancel",
|
49
|
+
parent,
|
50
|
+
Gtk::Dialog::MODAL,
|
51
|
+
[ "Discard Recording", Gtk::Dialog::RESPONSE_CANCEL ],
|
52
|
+
[ "Save Recording As...", Gtk::Dialog::RESPONSE_OK ]
|
53
|
+
)
|
54
|
+
dialog.has_separator = false
|
55
|
+
label = Gtk::Label.new("Your recording has not been saved.")
|
56
|
+
image = Gtk::Image.new(Gtk::Stock::DIALOG_WARNING, Gtk::IconSize::DIALOG)
|
57
|
+
|
58
|
+
hbox = Gtk::HBox.new(false, 5)
|
59
|
+
hbox.border_width = 10
|
60
|
+
hbox.pack_start_defaults(image);
|
61
|
+
hbox.pack_start_defaults(label);
|
62
|
+
|
63
|
+
dialog.vbox.add(hbox)
|
64
|
+
dialog.show_all
|
65
|
+
result = dialog.run
|
66
|
+
dialog.destroy
|
67
|
+
|
68
|
+
case result
|
69
|
+
when Gtk::Dialog::RESPONSE_OK
|
70
|
+
self.get_file_to_save
|
71
|
+
else
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: screencaster-gtk
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3.alpha1
|
5
|
+
prerelease: 6
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Larry Reid
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: gdk_pixbuf2
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
- - ! '>='
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 2.0.2
|
25
|
+
type: :runtime
|
26
|
+
prerelease: false
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ~>
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '2.0'
|
33
|
+
- - ! '>='
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 2.0.2
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: cairo
|
38
|
+
requirement: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '1.12'
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.12.6
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.12'
|
55
|
+
- - ! '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 1.12.6
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: glib2
|
60
|
+
requirement: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '2.0'
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.0.2
|
69
|
+
type: :runtime
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '2.0'
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.0.2
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: gtk2
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ~>
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '2.0'
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 2.0.2
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ~>
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '2.0'
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 2.0.2
|
102
|
+
description: A gem for capturing screencasts
|
103
|
+
email: larry.reid@jadesystems.ca
|
104
|
+
executables:
|
105
|
+
- screencaster
|
106
|
+
extensions: []
|
107
|
+
extra_rdoc_files: []
|
108
|
+
files:
|
109
|
+
- lib/screencaster-gtk.rb
|
110
|
+
- lib/screencaster-gtk/savefile.rb
|
111
|
+
- lib/screencaster-gtk/capture.rb
|
112
|
+
- lib/screencaster-gtk/progresstracker.rb
|
113
|
+
- bin/screencaster
|
114
|
+
homepage: http://github.org/lcreid/screencaster
|
115
|
+
licenses:
|
116
|
+
- GPL2
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: 1.9.2
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
none: false
|
129
|
+
requirements:
|
130
|
+
- - ! '>'
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: 1.3.1
|
133
|
+
requirements:
|
134
|
+
- avconv
|
135
|
+
- wmctl
|
136
|
+
- libavcodec-extra-53
|
137
|
+
- mkvtoolnix
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.24
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: Screencaster
|
143
|
+
test_files: []
|