screencaster-gtk 0.0.4.alpha1 → 0.0.5.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.
@@ -1,6 +1,17 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "screencaster-gtk"
4
+ #require "rdoc/usage"
5
+
6
+ =begin rdoc
7
+ screencaster [OPTION] ...
8
+
9
+ -h, --help:
10
+ show help
11
+
12
+ -s, -p, --start, --pause:
13
+ Pause a running capture, or restart a paused capture
14
+ =end
4
15
 
5
16
  app = ScreencasterGtk.new
6
17
  app.set_up
@@ -7,19 +7,27 @@ require "screencaster-gtk/savefile"
7
7
 
8
8
  ##########################
9
9
 
10
-
10
+ =begin rdoc
11
+ A program to capture screencasts -- video from monitor and sound from microphone
12
+ =end
11
13
  class ScreencasterGtk
12
- attr_reader :capture_window, :window
13
- attr_writer :status_icon
14
+ attr_reader :capture_window
15
+
16
+ protected
17
+ attr_reader :window
18
+ # atr_writer :status_icon
14
19
 
15
20
  SCREENCASTER_DIR = File.join(Dir.home, ".screencaster")
16
- # Set up logging. Keep 5 log files of a 100K each
21
+ # Set up logging. Keep 5 log files of 100K each
17
22
  log_dir = File.join(SCREENCASTER_DIR, 'log')
18
23
  FileUtils.mkpath log_dir
19
- LOGGER = Logger.new(File.join(log_dir, 'screencaster.log'), 5, 100000)
20
- LOGGER.level = Logger::DEBUG
24
+ LOGFILE = File.join(log_dir, 'screencaster.log')
25
+ @@logger = Logger.new(LOGFILE, 5, 100000)
26
+ @@logger.level = Logger::DEBUG
21
27
 
22
28
  PIDFILE = File.join(SCREENCASTER_DIR, "run", "screencaster.pid")
29
+
30
+ SOUND_SETTINGS = "/usr/bin/gnome-control-center"
23
31
 
24
32
  DEFAULT_SPACE = 10
25
33
  RECORD_IMAGE = Gtk::Image.new(Gtk::Stock::MEDIA_RECORD, Gtk::IconSize::SMALL_TOOLBAR)
@@ -28,81 +36,54 @@ class ScreencasterGtk
28
36
  CANCEL_IMAGE = Gtk::Image.new(Gtk::Stock::CANCEL, Gtk::IconSize::SMALL_TOOLBAR)
29
37
  QUIT_IMAGE = Gtk::Image.new(Gtk::Stock::QUIT, Gtk::IconSize::SMALL_TOOLBAR)
30
38
 
39
+ public
40
+ def self.logger
41
+ @@logger
42
+ end
43
+
44
+ def self.logger=(log_file)
45
+ @@logger = Logger.new(log_file, 5, 100000)
46
+ end
47
+
31
48
 
32
49
  def initialize
33
50
  #### Create Main Window
34
51
 
35
- LOGGER.info "Started"
52
+ @@logger.info "Started"
36
53
 
37
54
  @window = Gtk::Window.new("Screencaster")
38
55
  @window.signal_connect("delete_event") {
39
- LOGGER.debug "delete event occurred"
56
+ @@logger.debug "delete event occurred"
40
57
  #true
41
58
  self.quit
42
59
  false
43
60
  }
44
61
 
45
62
  @window.signal_connect("destroy") {
46
- LOGGER.debug "destroy event occurred"
63
+ @@logger.debug "destroy event occurred"
47
64
  }
48
65
 
49
66
  # The following gets minimize and restore events, but not iconify and de-iconify
50
67
  @window.signal_connect("window_state_event") { |w, e|
51
- LOGGER.debug "window_state_event #{e.to_s}"
68
+ @@logger.debug "window_state_event #{e.to_s}"
52
69
  }
53
70
 
54
71
  @window.border_width = DEFAULT_SPACE
55
72
 
56
73
  control_bar = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
57
74
 
58
- @select_button = Gtk::Button.new("Select Window")
59
- @select_button.signal_connect("clicked") {
60
- self.select
61
- }
62
- control_bar.pack_start(@select_button, true, false)
63
-
64
- button = Gtk::Button.new
65
- button.image = QUIT_IMAGE
66
- button.signal_connect("clicked") {
67
- self.quit
68
- }
69
- control_bar.pack_end(button, true, false)
70
-
71
- control_columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
72
-
73
- @record_pause_button = Gtk::Button.new
74
- @record_pause_button.image = RECORD_IMAGE
75
- @record_pause_button.sensitive = false
76
- @record_pause_button.signal_connect("clicked") {
77
- self.record_pause
78
- }
79
- control_bar.pack_start(@record_pause_button, true, false)
80
-
81
- # @pause_button = Gtk::Button.new(Gtk::Stock::MEDIA_PAUSE)
82
- # @pause_button.sensitive = false
83
- # @pause_button.signal_connect("clicked") {
84
- # self.pause
85
- # }
86
- # control_columns.pack_start(@pause_button, true, false)
87
- #
88
- @stop_button = Gtk::Button.new
89
- @stop_button.image = STOP_IMAGE
90
- @stop_button.sensitive = false
91
- @stop_button.signal_connect("clicked") {
92
- self.stop_recording
93
- }
94
- control_bar.pack_start(@stop_button, true, false)
95
-
96
- @cancel_button = Gtk::Button.new
97
- @cancel_button.image = CANCEL_IMAGE
98
- @cancel_button.sensitive = false
99
- @cancel_button.signal_connect("clicked") {
100
- self.stop_encoding
101
- }
102
- control_bar.pack_start(@cancel_button, true, false)
103
-
104
- control_row = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE) # children have different sizes, spaced by DEFAULT_SPACE
105
- control_row.pack_start(control_bar, true, false)
75
+ @select_button = add_button("Select Window", control_bar) { self.select }
76
+ @record_pause_button = add_button(RECORD_IMAGE, control_bar) { self.record_pause }
77
+ @stop_button = add_button(STOP_IMAGE, control_bar) { self.stop_recording }
78
+ @cancel_button = add_button(CANCEL_IMAGE, control_bar) { self.stop_encoding }
79
+ if File.executable? SOUND_SETTINGS
80
+ # There appears to be no stock icon for someting like a volume control.
81
+ #@sound_settings_button.image = AUDIO_VOLUME_MEDIUM
82
+ @sound_settings_button = add_button("Sound", control_bar, true) {
83
+ Thread.new { `#{SOUND_SETTINGS} sound` }
84
+ }
85
+ end
86
+ add_button(QUIT_IMAGE, control_bar, true) { self.quit }
106
87
 
107
88
  columns = Gtk::HBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
108
89
  @progress_bar = Gtk::ProgressBar.new
@@ -114,20 +95,15 @@ class ScreencasterGtk
114
95
 
115
96
  the_box = Gtk::VBox.new(false, ScreencasterGtk::DEFAULT_SPACE)
116
97
  the_box.pack_end(progress_row, false, false)
117
- the_box.pack_end(control_row, false, false)
98
+ the_box.pack_end(control_bar, false, false)
118
99
 
119
100
  @window.add(the_box)
120
101
 
121
102
  ##### Done Creating Main Window
122
103
 
123
- #### Accelerator Group
124
- group = Gtk::AccelGroup.new
125
- group.connect(Gdk::Keyval::GDK_N, Gdk::Window::CONTROL_MASK|Gdk::Window::MOD1_MASK, Gtk::ACCEL_VISIBLE) do
126
- #puts "You pressed 'Ctrl+Alt+n'"
127
- end
128
-
129
104
  #### Pop up menu on right click
130
-
105
+ group = Gtk::AccelGroup.new
106
+
131
107
  @select = Gtk::ImageMenuItem.new("Select Window")
132
108
  @select.signal_connect('activate'){self.select}
133
109
 
@@ -198,74 +174,130 @@ class ScreencasterGtk
198
174
  #### Done Status Icon
199
175
 
200
176
  def quit
201
- LOGGER.debug "Quitting"
177
+ @@logger.debug "Quitting"
202
178
  # self.status_icon.destroy
203
- # LOGGER.debug "After status icon destroy."
179
+ # @@logger.debug "After status icon destroy."
204
180
  # @window.destroy
205
- # LOGGER.debug "After window destroy."
181
+ # @@logger.debug "After window destroy."
206
182
  # We don't want to destroy here because the object continues to exist
207
183
  # Just hide everything
208
184
  self.hide_all_including_status
209
185
  # self.status_icon.hide doesn't work/exist
210
186
  Gtk.main_quit
211
- LOGGER.debug "After main_quit."
187
+ @@logger.debug "After main_quit."
212
188
  end
213
189
 
190
+ public
214
191
  def select
215
- LOGGER.debug "Selecting Window"
192
+ @@logger.debug "Selecting Window"
216
193
  @capture_window = Capture.new
217
194
  @capture_window.get_window_to_capture
218
195
  @record_pause_button.sensitive = true
219
196
  end
220
197
 
198
+ protected
221
199
  def record_pause
222
- LOGGER.debug "Record/Pause state: #{@capture_window.state}"
200
+ @@logger.debug "Record/Pause state: #{@capture_window.state}"
223
201
  case @capture_window.state
224
202
  when :recording
225
- LOGGER.debug "Record/Pause -- pause"
203
+ @@logger.debug "Record/Pause -- pause"
226
204
  pause
227
205
  when :paused, :stopped
228
- LOGGER.debug "Record/Pause -- record"
206
+ @@logger.debug "Record/Pause -- record"
229
207
  record
230
208
  else
231
- LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
209
+ @@logger.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
232
210
  end
233
211
  end
234
212
 
235
213
  def record
236
- LOGGER.debug "Recording"
214
+ @@logger.debug "Recording"
237
215
  recording
238
- @capture_window.record { |percent, time_elapsed|
239
- @progress_bar.text = time_elapsed
240
- LOGGER.debug "Did elapsed time: #{time_elapsed}"
241
- }
216
+ spawn_record
242
217
  end
243
218
 
244
219
  def pause
245
- LOGGER.debug "Pausing"
220
+ @@logger.debug "Pausing"
246
221
  paused
247
222
  @capture_window.pause_recording
248
223
  end
249
224
 
225
+ public
250
226
  def stop_recording
251
- LOGGER.debug "Stopped"
227
+ @@logger.debug "Stopped"
252
228
  not_recording
253
229
  @capture_window.stop_recording
254
230
 
255
231
  SaveFile.get_file_to_save { |filename|
256
232
  encoding
257
- @capture_window.encode(filename) { |percent, time_remaining|
258
- @progress_bar.fraction = percent
259
- @progress_bar.text = time_remaining
260
- LOGGER.debug "Did progress #{percent.to_s} time remaining: #{time_remaining}"
261
- percent < 1 || stop_encoding
262
- }
263
- LOGGER.debug "Back from encode"
233
+ spawn_encode(filename)
234
+ @@logger.debug "Encode spawned"
264
235
  }
265
236
  end
266
237
 
238
+ =begin rdoc
239
+ Encode in the background. If the background process fails, be able to pop
240
+ up a window. Give feedback by calling the optional block with | fraction, message |.
241
+ =end
242
+ def spawn_record
243
+ @background = Thread.new do
244
+ @capture_window.record do |percent, time_elapsed|
245
+ @progress_bar.text = time_elapsed
246
+ @progress_bar.pulse
247
+ @@logger.debug "Did elapsed time: #{time_elapsed}"
248
+ end
249
+ end
250
+ @background
251
+ end
252
+
253
+ =begin rdoc
254
+ Encode in the background. If the background process fails, be able to pop
255
+ up a window. Give feedback by calling the optional block with | fraction, message |.
256
+ =end
257
+ def spawn_encode(filename)
258
+ @background = Thread.new do
259
+ @capture_window.encode(filename) do |fraction, time_remaining|
260
+ @progress_bar.pulse if fraction == 0
261
+ @progress_bar.fraction = fraction
262
+ @progress_bar.text = time_remaining
263
+ @@logger.debug "Did progress #{fraction.to_s} time remaining: #{time_remaining}"
264
+ not_encoding if fraction >= 1
265
+ end
266
+ end
267
+ @background
268
+ end
269
+
270
+ =begin rdoc
271
+ Check how the background process is doing.
272
+ If there is none, or it's running fine, return true.
273
+ If there was a background process but it failed, return false and
274
+ ensure that the user doesn't get another
275
+ message about the same condition.
276
+ =end
277
+ def check_background
278
+ # TODO: Partially implemented so far
279
+ return true if @background.nil?
280
+ return true if @background.status
281
+ if ! background_exitstatus
282
+ @background = nil
283
+ return false
284
+ end
285
+ true
286
+ end
287
+
288
+ def background_exitstatus
289
+ return true if @background.nil?
290
+ @@logger.debug "background_exitstatus: #{@background.value.exitstatus}"
291
+ [0, 255].any? { | x | x == @background.value.exitstatus }
292
+ end
293
+
294
+ def idle
295
+ error_dialog_tell_about_log unless check_background
296
+ end
297
+
298
+ protected
267
299
  def stop_encoding
268
- LOGGER.debug "Cancelled encoding"
300
+ @@logger.debug "Cancelled encoding"
269
301
  not_recording
270
302
  @capture_window.stop_encoding
271
303
  end
@@ -279,12 +311,12 @@ class ScreencasterGtk
279
311
  when :stopped
280
312
  # Do nothing
281
313
  else
282
- LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
314
+ @@logger.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
283
315
  end
284
316
  end
285
317
 
286
318
  def toggle_recording
287
- LOGGER.debug "Toggle recording"
319
+ @@logger.debug "Toggle recording"
288
320
  return if @capture_window.nil?
289
321
  case @capture_window.state
290
322
  when :recording
@@ -296,10 +328,12 @@ class ScreencasterGtk
296
328
  when :stopped
297
329
  # Do nothing
298
330
  else
299
- LOGGER.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
331
+ @@logger.error "#{__FILE__} #{__LINE__}: Can't happen (state #{@capture_window.state})."
300
332
  end
301
333
  end
302
334
 
335
+ ##### Methods to set sensitivity of controls
336
+
303
337
  def recording
304
338
  self.status_icon.stock = Gtk::Stock::MEDIA_PAUSE
305
339
  @record_pause_button.image = PAUSE_IMAGE
@@ -378,36 +412,45 @@ class ScreencasterGtk
378
412
  self.status_icon.visible = false
379
413
  end
380
414
 
415
+ public
416
+
417
+ # Shows the screencaster window and starts processing events from the user.
418
+ #
419
+ # The hot key toggles between capture and pause.
420
+ # The default hot key is Ctrl+Alt+S.
381
421
  def main
382
- LOGGER.info "Starting"
422
+ @@logger.info "Starting event loop"
383
423
  self.not_recording
384
424
  self.show_all_including_status
425
+ GLib::Idle.add { idle }
385
426
  Gtk.main
386
- LOGGER.info "Finished"
427
+ @@logger.info "Finished"
387
428
  end
388
429
 
430
+ # Process command line arguments, set up the log file, and set up hot keys.
431
+ #
432
+ # * If there's another instance running for the user and the --pause or --start
433
+ # flags are present, send the USR1 signal to the running instance and exit.
434
+ # * Don't run the program if there's another instance running for the user.
435
+ # * If there's no other instance running for the user, and the --pause or --start
436
+ # flags are not present, start normally.
437
+ # * The default hot key is Ctrl+Alt+S.
389
438
  def set_up
390
- # Don't run the program if there's another instance running for the user.
391
- # If there's another instance running for the user and the --pause or --start
392
- # flags are present, send the USR1 signal to the running instance
393
- # and exit.
394
- # If there's no other instance running for the user, and the --pause or --start
395
- # flags are not present, start normally.
396
439
 
397
440
  output_file = "/home/reid/test-key.log"
398
441
 
399
- ScreencasterGtk::LOGGER.debug("pid_file is #{PIDFILE}")
442
+ @@logger.debug("pid_file is #{PIDFILE}")
400
443
 
401
444
  if File.exists? PIDFILE
402
445
  begin
403
446
  f = File.new(PIDFILE)
404
- ScreencasterGtk::LOGGER.debug("Opened PIDFILE")
447
+ @@logger.debug("Opened PIDFILE")
405
448
  existing_pid = f.gets
406
449
  existing_pid = existing_pid.to_i
407
450
  f.close
408
- ScreencasterGtk::LOGGER.debug("existing_pid = #{existing_pid.to_s}")
451
+ @@logger.debug("existing_pid = #{existing_pid.to_s}")
409
452
  rescue StandardError
410
- LOGGER.error("File to read #{PIDFILE}")
453
+ @@logger.error("File to read #{PIDFILE}")
411
454
  exit 1
412
455
  end
413
456
  else
@@ -424,46 +467,57 @@ class ScreencasterGtk
424
467
  case opt
425
468
  when '--help'
426
469
  puts <<-EOF
427
- screencaster [OPTION] ...
428
-
429
- -h, --help:
430
- show help
431
-
432
- -s, -p, --start, --pause:
433
- Pause a running capture, or restart a paused capture
470
+ screencaster [OPTION] ...
471
+
472
+ -h, --help:
473
+ show help
474
+
475
+ -s, -p, --start, --pause:
476
+ Pause a running capture, or restart a paused capture
434
477
  EOF
435
478
  exit 0
436
479
  when '--pause', '--start'
437
480
  if existing_pid then
438
- ScreencasterGtk::LOGGER.debug("Got a pause for PID #{existing_pid}")
481
+ @@logger.debug("Got a pause for PID #{existing_pid}")
439
482
  begin
440
483
  Process.kill "USR1", existing_pid
441
484
  exit 0
442
485
  rescue SystemCallError
443
- LOGGER.info("Got a pause but PID #{existing_pid} didn't exist")
486
+ @@logger.info("Got a pause but PID #{existing_pid} didn't exist")
444
487
  exit 1
445
488
  end
446
489
  else
447
- ScreencasterGtk::LOGGER.info("Got a pause but no PID")
490
+ @@logger.info("Got a pause but no PID")
448
491
  exit 1
449
492
  end
450
493
  end
451
494
  end
452
495
 
453
496
  # TODO: Check for running process and if not, ignore PIDFILE.
454
- (ScreencasterGtk::LOGGER.debug("Can't run two instances at once."); exit 1) if ! existing_pid.nil?
497
+ unless existing_pid.nil?
498
+ error_dialog_tell_about_log("Can't run two instances at once.")
499
+ exit 1
500
+ end
455
501
 
502
+ # Add application properties for PulseAudio
503
+ # See: http://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/ApplicationProperties/
504
+ # Unfortunately, this doesn't seem to help
505
+ # Perhaps because avconv is a separate program and is setting its own values
506
+ GLib::application_name = "Screencaster"
507
+ Gtk::Window.default_icon_name = "screencaster"
508
+ GLib::setenv("PULSE_PROP_media.role", "video")
509
+
456
510
  @chain = Signal.trap("EXIT") {
457
- LOGGER.debug "In ScreencasterGtk exit handler @chain: #{@chain}"
458
- ScreencasterGtk::LOGGER.debug("Exiting")
459
- ScreencasterGtk::LOGGER.debug("unlinking") if File.file?(PIDFILE)
511
+ @@logger.debug "In ScreencasterGtk exit handler @chain: #{@chain}"
512
+ @@logger.debug("Exiting")
513
+ @@logger.debug("unlinking") if File.file?(PIDFILE)
460
514
  File.unlink(PIDFILE) if File.file?(PIDFILE)
461
515
  `gconftool-2 --unset /apps/metacity/keybinding_commands/screencaster_pause`
462
516
  `gconftool-2 --unset /apps/metacity/global_keybindings/run_screencaster_pause`
463
- LOGGER.debug "About to call next trap with @chain: #{@chain}"
517
+ @@logger.debug "About to call next trap with @chain: #{@chain}"
464
518
  @chain.call unless @chain.nil?
465
519
  }
466
- LOGGER.debug "ScreencasterGtk.whatever @chain: #{@chain}"
520
+ @@logger.debug "ScreencasterGtk.whatever @chain: #{@chain}"
467
521
 
468
522
  `gconftool-2 --set /apps/metacity/keybinding_commands/screencaster_pause --type string "screencaster --pause"`
469
523
  `gconftool-2 --set /apps/metacity/global_keybindings/run_screencaster_pause --type string "<Control><Alt>S"`
@@ -473,18 +527,52 @@ class ScreencasterGtk
473
527
  f = File.new(PIDFILE, "w")
474
528
  f.puts(Process.pid.to_s)
475
529
  f.close
476
- LOGGER.debug("Wrote PID #{Process.pid}")
530
+ @@logger.debug("Wrote PID #{Process.pid}")
477
531
  rescue StandardError
478
- LOGGER.error("Failed to write #{PIDFILE}")
532
+ @@logger.error("Failed to write #{PIDFILE}")
479
533
  exit 1
480
534
  end
481
535
 
482
536
  Signal.trap("USR1") {
483
- ScreencasterGtk::LOGGER.debug("Pause/Resume")
537
+ @@logger.debug("Pause/Resume")
484
538
  self.toggle_recording
485
539
  }
486
540
 
487
- $logger = ScreencasterGtk::LOGGER # TODO: Fix logging
541
+ $logger = @@logger # TODO: Fix logging
542
+ end
543
+
544
+ # Helper functions
545
+ private
546
+ def add_button(label, box, sensitive = false, &callback)
547
+ b = Gtk::Button.new
548
+ b.image = label if label.is_a? Gtk::Image
549
+ b.label = label if label.is_a? String
550
+ b.sensitive = sensitive
551
+ b.signal_connect("clicked") { callback.call }
552
+ box.pack_start(b, true, false)
553
+ b
554
+ end
555
+
556
+ public
557
+ def error_dialog_tell_about_log(msg = "", file = nil, line = nil)
558
+ d = Gtk::MessageDialog.new(@window,
559
+ Gtk::Dialog::DESTROY_WITH_PARENT,
560
+ Gtk::MessageDialog::WARNING,
561
+ Gtk::MessageDialog::BUTTONS_CLOSE,
562
+ "Internal Error")
563
+
564
+ d.secondary_text = msg
565
+ d.secondary_text += " in #{file}" unless file.nil?
566
+ d.secondary_text += ", line: #{line}" unless line.nil?
567
+ d.secondary_text.strip!
568
+
569
+ @@logger.warn(d.secondary_text)
570
+
571
+ d.secondary_text += "\nLook in #{LOGFILE} for further information"
572
+ d.secondary_text.strip!
573
+
574
+ d.run
575
+ d.destroy
488
576
  end
489
577
  end
490
578
 
@@ -2,13 +2,26 @@ require 'open3'
2
2
  require 'logger'
3
3
  require "screencaster-gtk/progresstracker"
4
4
 
5
+ =begin rdoc
6
+ Select a window or area of the screen to capture, capture it, and encode it.
7
+ =end
5
8
  class Capture
6
9
  include ProgressTracker
7
10
 
8
11
  attr_writer :left, :top, :right, :bottom
9
12
  attr_reader :state
10
- attr :pid
11
- attr :tmp_files
13
+ attr_accessor :pid
14
+ attr_accessor :tmp_files
15
+
16
+ attr_accessor :capture_fps
17
+ attr_accessor :encode_fps
18
+ attr_accessor :capture_vcodec
19
+ attr_accessor :qscale
20
+ attr_accessor :encode_vcodec
21
+ attr_accessor :acodec
22
+ attr_accessor :audio_sample_frequency
23
+ attr_accessor :audio_input
24
+
12
25
 
13
26
  def initialize
14
27
  @tmp_files = []
@@ -17,8 +30,30 @@ class Capture
17
30
  $logger.debug "In capture about to chain to trap @exit_chain: #{@exit_chain}"
18
31
  @exit_chain.call unless @exit_chain.nil?
19
32
  }
20
- $logger.debug "@exit_chain: #{@exit_chain.to_s}"
33
+ #$logger.debug "@exit_chain: #{@exit_chain.to_s}"
34
+
21
35
  @state = :stopped
36
+ @coprocess = nil
37
+
38
+ @capture_fps = 30
39
+ @encode_fps = 30 # Don't use this. Just copy through
40
+ @capture_vcodec = 'huffyuv' # I used to use ffv1. This is for capture
41
+ @qscale = '4' # Recommended for fast encoding by avconv docs
42
+ @encode_vcodec = 'libx264'
43
+ @acodec = 'aac' # Youtube prefers "AAC-LC" but I don't see one called "-LC"
44
+ @audio_sample_frequency = "48k"
45
+ @audio_input = "pulse"
46
+ end
47
+
48
+ def wait
49
+ $logger.debug "wait: Current thread #{Thread.current}"
50
+ @coprocess.value
51
+ end
52
+
53
+ def status
54
+ $logger.debug "status: Current thread #{Thread.current}"
55
+ return false if @coprocess.nil?
56
+ @coprocess.status
22
57
  end
23
58
 
24
59
  def current_tmp_file
@@ -79,24 +114,6 @@ class Capture
79
114
  info =~ /Height:\s+([[:digit:]]+)/
80
115
  @height = $1.to_i
81
116
 
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
117
  @height += @height % 2
101
118
  @width += @width % 2
102
119
 
@@ -112,60 +129,44 @@ class Capture
112
129
  output_file = self.new_tmp_file
113
130
 
114
131
  @state = :recording
115
- capture_fps=24
116
- audio_options="-f alsa -ac 1 -ab 192k -i pulse -acodec pcm_s16le"
132
+ audio_options="-f alsa -ac 1 -ab #{@audio_sample_frequency} -i #{@audio_input} -acodec #{@acodec}"
117
133
 
118
134
  # And i should probably popen here, save the pid, then fork and start
119
135
  # reading the input, updating the number of frames saved, or the time
120
136
  # recorded.
121
137
  $logger.debug "Capturing...\n"
122
- # @pid = Process.spawn("avconv \
123
- # #{audio_options} \
124
- # -f x11grab \
125
- # -show_region 1 \
126
- # -r #{capture_fps} \
127
- # -s #{@width}x#{@height} \
128
- # -i :0.0+#{@left},#{@top} \
129
- # -qscale 0 -vcodec ffv1 \
130
- # -y \
131
- # #{TMP_FILE}")
132
- # Process.detach(@pid)
133
- # avconv writes output to stderr, why?
134
- # writes with CR and not LF, so it's hard to read
135
- # with popen and 2>&1, an extra shell gets created that messed things up.
136
- # popen2e helps.
137
- vcodec = 'huffyuv' # I used to use ffv1
138
-
138
+
139
139
  cmd_line = "avconv \
140
140
  #{audio_options} \
141
141
  -f x11grab \
142
142
  -show_region 1 \
143
- -r #{capture_fps} \
143
+ -r #{@capture_fps} \
144
144
  -s #{@width}x#{@height} \
145
145
  -i :0.0+#{@left},#{@top} \
146
- -qscale 0 -vcodec #{vcodec} \
146
+ -qscale #{@qscale} \
147
+ -vcodec #{@capture_vcodec} \
147
148
  -y \
148
149
  #{output_file}"
149
150
 
150
151
  $logger.debug cmd_line
151
152
 
152
- i, oe, t = Open3.popen2e(cmd_line)
153
- @pid = t.pid
154
- Process.detach(@pid)
155
-
156
153
  duration = 0.0
157
- Thread.new do
158
- while line = oe.gets("\r")
159
- $logger.debug "****" + line
160
- if (line =~ /time=([0-9]*\.[0-9]*)/)
161
- duration = $1.to_f
162
- $logger.debug "Recording about to yield #{self.total_amount + duration}"
163
- yield 0.0, ProgressTracker::format_seconds(self.total_amount + duration)
164
- end
154
+ i, oe, @coprocess = Open3.popen2e(cmd_line)
155
+ $logger.debug "record: co-process started #{@coprocess}"
156
+ @pid = @coprocess.pid
157
+
158
+ while line = oe.gets("\r")
159
+ $logger.debug "****" + line
160
+ if (line =~ /time=([0-9]*\.[0-9]*)/)
161
+ duration = $1.to_f
162
+ $logger.debug "Recording about to yield #{self.total_amount + duration}"
163
+ yield 0.0, ProgressTracker::format_seconds(self.total_amount + duration) if block_given?
165
164
  end
166
- self.total_amount += duration
167
- yield 0.0, ProgressTracker::format_seconds(self.total_amount)
168
165
  end
166
+ self.total_amount += duration
167
+ yield 1.0, ProgressTracker::format_seconds(self.total_amount) if block_given?
168
+ $logger.debug "Leaving record"
169
+ @coprocess.value
169
170
  end
170
171
 
171
172
  def stop_recording
@@ -201,9 +202,8 @@ class Capture
201
202
  $logger.debug "Encoding #{Capture.format_input_files_for_mkvmerge(@tmp_files)}...\n"
202
203
  $logger.debug("Total duration #{self.total_amount.to_s}")
203
204
 
204
- t = self.merge(Capture.tmp_file_name, @tmp_files)
205
- t.value
206
- self.final_encode(output_file, Capture.tmp_file_name, feedback)
205
+ merge(Capture.tmp_file_name, @tmp_files, feedback)
206
+ final_encode(output_file, Capture.tmp_file_name, feedback)
207
207
  end
208
208
 
209
209
  # This is ugly.
@@ -222,73 +222,57 @@ class Capture
222
222
  else
223
223
  cmd_line = "mkvmerge -v -o #{output_file} #{Capture.format_input_files_for_mkvmerge(input_files)}"
224
224
  end
225
- $logger.debug "Merge command line: #{cmd_line}"
226
- i, oe, t = Open3.popen2e(cmd_line)
227
- $logger.debug "Thread from popen2e: #{t}"
228
- @pid = t.pid
229
- Process.detach(@pid)
225
+ $logger.debug "merge: command line: #{cmd_line}"
226
+ i, oe, @coprocess = Open3.popen2e(cmd_line)
227
+ $logger.debug "merge: Thread from popen2e: #{@coprocess}"
228
+ @pid = @coprocess.pid
230
229
  $logger.debug "@pid: #{@pid.to_s}"
231
-
232
- t = Thread.new do
233
- $logger.debug "Thread from Thread.new: #{t}"
234
- # $logger.debug "Sleeping..."
235
- # sleep 2
236
- # $logger.debug "Awake!"
237
- while l = oe.gets do
238
- # TODO: Lots for duplicate code in this line to clean up.
239
- if block_given?
240
- yield 0.5, ""
241
- else
242
- feedback.call 0.5, ""
243
- end
244
- end
245
- if block_given?
246
- yield 1.0, "Done"
230
+ while l = oe.gets do
231
+ # TODO: Lots of duplicate code in this line to clean up.
232
+ if block_given?
233
+ yield 0.5, "Merging..."
247
234
  else
248
- feedback.call 1.0, "Done"
235
+ feedback.call 0.5, "Merging..."
249
236
  end
250
237
  end
251
- return t
238
+ if block_given?
239
+ yield 1.0, "Done"
240
+ else
241
+ feedback.call 1.0, "Done"
242
+ end
243
+ @coprocess.value
252
244
  end
253
245
 
254
246
  def final_encode(output_file, input_file, feedback = proc {} )
255
- encode_fps=24
256
- video_encoding_options="-vcodec libx264 -pre:v ultrafast"
257
-
258
247
  # I think I want to popen here, save the pid, then fork and start
259
- # updating progress based on what I read, which the main body
248
+ # updating progress based on what I read, while the main body
260
249
  # returns and carries on.
261
250
 
262
- # The following doesn't seem to be necessary
263
- # -s #{@width}x#{@height} \
264
251
  cmd_line = "avconv \
265
252
  -i #{input_file} \
266
- #{video_encoding_options} \
267
- -r #{encode_fps} \
268
- -threads 0 \
253
+ -vcodec #{@encode_vcodec} \
269
254
  -y \
270
255
  '#{output_file}'"
271
256
 
272
257
  $logger.debug cmd_line
273
258
 
274
- i, oe, t = Open3.popen2e(cmd_line)
275
- @pid = t.pid
276
- Process.detach(@pid)
277
-
278
- Thread.new do
279
- while (line = oe.gets("\r"))
280
- $logger.debug "****" + line
281
- if (line =~ /time=([0-9]*\.[0-9]*)/)
282
- $logger.debug '******' + $1
283
- self.current_amount = $1.to_f
284
- end
285
- $logger.debug "******** #{self.current_amount} #{self.fraction_complete}"
286
- feedback.call self.fraction_complete, self.time_remaining_s
259
+ i, oe, @coprocess = Open3.popen2e(cmd_line)
260
+ @pid = @coprocess.pid
261
+
262
+ while (line = oe.gets("\r"))
263
+ $logger.debug "****" + line
264
+ if (line =~ /time=([0-9]*\.[0-9]*)/)
265
+ $logger.debug '******' + $1
266
+ self.current_amount = $1.to_f
287
267
  end
288
- $logger.debug "reached end of file"
289
- @state = :stopped
290
- feedback.call self.fraction_complete = 1, self.time_remaining_s
268
+ $logger.debug "******** #{self.current_amount} #{self.fraction_complete}"
269
+ feedback.call self.fraction_complete, self.time_remaining_s
291
270
  end
271
+ $logger.debug "reached end of file"
272
+ @state = :stopped
273
+ self.fraction_complete = 1
274
+ feedback.call self.fraction_complete, self.time_remaining_s
275
+ @coprocess.value # A little bit of a head game here. Either return this, or maybe have to do t.value.value in caller
292
276
  end
293
277
 
294
278
  def stop_encoding
@@ -1,7 +1,10 @@
1
1
  module ProgressTracker
2
- attr_reader :start_time
3
2
  attr_writer :start_time, :total_amount
4
3
 
4
+ def start_time
5
+ @start_time || @start_time = Time.now
6
+ end
7
+
5
8
  def percent_complete
6
9
  self.fraction_complete * 100
7
10
  end
@@ -19,9 +22,7 @@ module ProgressTracker
19
22
  end
20
23
 
21
24
  def current_amount=(amt)
22
- @start_time || @start_time = Time.new
23
-
24
- # puts "Setting current_amount #{amt}"
25
+ self.start_time
25
26
  @current_amount = amt
26
27
  end
27
28
 
@@ -30,7 +31,11 @@ module ProgressTracker
30
31
  end
31
32
 
32
33
  def time_remaining
33
- (Time.new - @start_time) * (1 - self.fraction_complete) / self.fraction_complete
34
+ if self.fraction_complete == 0.0
35
+ 1.0
36
+ else
37
+ (Time.new - self.start_time) * (1 - self.fraction_complete) / self.fraction_complete
38
+ end
34
39
  end
35
40
 
36
41
  def time_remaining_s(format = "%dh %02dm %02ds remaining")
@@ -1,6 +1,13 @@
1
1
  require 'gtk2'
2
2
 
3
+ =begin rdoc
4
+ Run the standard save file dialog for screencaster
5
+ =end
3
6
  class SaveFile
7
+
8
+ =begin rdoc
9
+ Create the file chooser dialogue with a default file name.
10
+ =end
4
11
  def self.set_up_dialog(file_name = "output.mp4")
5
12
  @dialog = Gtk::FileChooserDialog.new(
6
13
  "Save File As ...",
@@ -10,22 +17,15 @@ class SaveFile
10
17
  [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ],
11
18
  [ Gtk::Stock::SAVE, Gtk::Dialog::RESPONSE_ACCEPT ]
12
19
  )
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
20
  @dialog.current_name = GLib.filename_to_utf8(file_name)
25
21
  @dialog.current_folder = GLib.filename_to_utf8(Dir.getwd)
26
22
  @dialog.do_overwrite_confirmation = true
27
23
  end
28
24
 
25
+ =begin rdoc
26
+ Do the workflow around saving a file, warning the user before allow
27
+ them to abandon their capture.
28
+ =end
29
29
  def self.get_file_to_save
30
30
  @dialog || SaveFile.set_up_dialog
31
31
  result = @dialog.run
@@ -43,6 +43,10 @@ class SaveFile
43
43
  end
44
44
  end
45
45
 
46
+ =begin rdoc
47
+ Confirm cancellation when the user has captured something but not
48
+ saved it.
49
+ =end
46
50
  def self.confirm_cancel(parent)
47
51
  dialog = Gtk::Dialog.new(
48
52
  "Confirm Cancel",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: screencaster-gtk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4.alpha1
4
+ version: 0.0.5.alpha1
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-11-29 00:00:00.000000000 Z
12
+ date: 2013-12-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: gdk_pixbuf2
@@ -141,3 +141,4 @@ signing_key:
141
141
  specification_version: 3
142
142
  summary: Screencaster
143
143
  test_files: []
144
+ has_rdoc: