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 ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "screencaster-gtk"
4
+
5
+ app = ScreencasterGtk.new
6
+ app.set_up
7
+ app.main
@@ -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: []