screencaster-gtk 0.0.3.alpha1
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/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: []
|